pioneer/src/WorldView.cpp

633 lines
18 KiB
C++

// Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
// Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
#include "WorldView.h"
#include "Frame.h"
#include "Game.h"
#include "GameConfig.h"
#include "GameSaveError.h"
#include "HudTrail.h"
#include "HyperspaceCloud.h"
#include "Input.h"
#include "Lang.h"
#include "Pi.h"
#include "Player.h"
#include "SectorView.h"
#include "Sensors.h"
#include "SpeedLines.h"
#include "StringF.h"
#include "graphics/Frustum.h"
#include "graphics/Graphics.h"
#include "graphics/Renderer.h"
#include "matrix4x4.h"
#include "ship/PlayerShipController.h"
#include "ship/ShipViewController.h"
#include "sound/Sound.h"
namespace {
static const float HUD_CROSSHAIR_SIZE = 8.0f;
static const Color green(0, 255, 0, 204);
static const Color red(255, 0, 0, 128);
} // namespace
REGISTER_INPUT_BINDING(WorldView)
{
using namespace InputBindings;
Input::BindingGroup *group = input->GetBindingPage("General")->GetBindingGroup("Miscellaneous");
input->AddActionBinding("BindToggleHudMode", group, Action({ SDLK_TAB }));
input->AddActionBinding("BindIncreaseTimeAcceleration", group, Action({ SDLK_PAGEUP }));
input->AddActionBinding("BindDecreaseTimeAcceleration", group, Action({ SDLK_PAGEDOWN }));
}
void WorldView::InputBinding::RegisterBindings()
{
toggleHudMode = AddAction("BindToggleHudMode");
increaseTimeAcceleration = AddAction("BindIncreaseTimeAcceleration");
decreaseTimeAcceleration = AddAction("BindDecreaseTimeAcceleration");
}
WorldView::WorldView(Game *game) :
PiGuiView("WorldView"),
m_game(game),
InputBindings(Pi::input)
{
InitObject();
}
WorldView::WorldView(const Json &jsonObj, Game *game) :
PiGuiView("WorldView"),
m_game(game),
InputBindings(Pi::input)
{
if (!jsonObj["world_view"].is_object()) throw SavedGameCorruptException();
Json worldViewObj = jsonObj["world_view"];
InitObject();
shipView->LoadFromJson(worldViewObj);
}
void WorldView::InitObject()
{
m_labelsOn = true;
SetTransparency(true);
Graphics::RenderStateDesc rsd;
rsd.blendMode = Graphics::BLEND_ALPHA;
rsd.depthWrite = false;
rsd.depthTest = false;
m_blendState = Pi::renderer->CreateRenderState(rsd); //XXX m_renderer not set yet
/*
NEW UI
*/
m_speedLines.reset(new SpeedLines(Pi::player));
//get near & far clipping distances
//XXX m_renderer not set yet
float znear;
float zfar;
Pi::renderer->GetNearFarRange(znear, zfar);
const float fovY = Pi::config->Float("FOVVertical");
m_cameraContext.Reset(new CameraContext(Graphics::GetScreenWidth(), Graphics::GetScreenHeight(), fovY, znear, zfar));
m_camera.reset(new Camera(m_cameraContext, Pi::renderer));
InputBindings.RegisterBindings();
shipView.reset(new ShipViewController(this));
shipView->Init();
SetViewController(shipView.get());
m_onToggleHudModeCon = InputBindings.toggleHudMode->onPressed.connect(sigc::mem_fun(this, &WorldView::OnToggleLabels));
m_onIncTimeAccelCon = InputBindings.increaseTimeAcceleration->onPressed.connect(sigc::mem_fun(this, &WorldView::OnRequestTimeAccelInc));
m_onDecTimeAccelCon = InputBindings.decreaseTimeAcceleration->onPressed.connect(sigc::mem_fun(this, &WorldView::OnRequestTimeAccelDec));
}
WorldView::~WorldView()
{
m_onToggleHudModeCon.disconnect();
m_onIncTimeAccelCon.disconnect();
m_onDecTimeAccelCon.disconnect();
}
void WorldView::SaveToJson(Json &jsonObj)
{
Json worldViewObj = Json::object(); // Create JSON object to contain world view data.
shipView->SaveToJson(worldViewObj);
jsonObj["world_view"] = worldViewObj; // Add world view object to supplied object.
}
void WorldView::OnRequestTimeAccelInc()
{
// requests an increase in time acceleration
Pi::game->RequestTimeAccelInc();
}
void WorldView::OnRequestTimeAccelDec()
{
// requests a decrease in time acceleration
Pi::game->RequestTimeAccelDec();
}
void WorldView::SetViewController(ViewController *newView)
{
m_viewController = newView;
}
void WorldView::Draw3D()
{
PROFILE_SCOPED()
assert(m_game);
assert(Pi::player);
assert(!Pi::player->IsDead());
m_cameraContext->ApplyDrawTransforms(m_renderer);
m_camera->Draw();
// NB: Do any screen space rendering after here:
// Things like the cockpit and AR features like hudtrails, space dust etc.
m_viewController->Draw(m_camera.get());
// Draw 3D HUD
// Speed lines
if (Pi::AreSpeedLinesDisplayed())
m_speedLines->Render(m_renderer);
// Contact trails
if (Pi::AreHudTrailsDisplayed()) {
for (auto it = Pi::player->GetSensors()->GetContacts().begin(); it != Pi::player->GetSensors()->GetContacts().end(); ++it)
it->trail->Render(m_renderer);
}
m_cameraContext->EndFrame();
}
void WorldView::OnToggleLabels()
{
if (Pi::GetView() == this) {
if (Pi::DrawGUI && m_labelsOn) {
m_labelsOn = false;
} else if (Pi::DrawGUI && !m_labelsOn) {
Pi::DrawGUI = false;
} else if (!Pi::DrawGUI) {
Pi::DrawGUI = true;
m_labelsOn = true;
}
}
}
void WorldView::Update()
{
PROFILE_SCOPED()
assert(m_game);
assert(Pi::player);
assert(!Pi::player->IsDead());
m_viewController->Update();
m_cameraContext->BeginFrame();
m_camera->Update();
UpdateProjectedObjects();
FrameId playerFrameId = Pi::player->GetFrame();
FrameId camFrameId = m_cameraContext->GetTempFrame();
//speedlines and contact trails need camFrame for transform, so they
//must be updated here
if (Pi::AreSpeedLinesDisplayed()) {
m_speedLines->Update(m_game->GetTimeStep());
matrix4x4d trans;
Frame::GetFrameTransform(playerFrameId, camFrameId, trans);
if (m_speedLines.get() && Pi::AreSpeedLinesDisplayed()) {
m_speedLines->Update(m_game->GetTimeStep());
trans[12] = trans[13] = trans[14] = 0.0;
trans[15] = 1.0;
m_speedLines->SetTransform(trans);
}
}
if (Pi::AreHudTrailsDisplayed()) {
matrix4x4d trans;
Frame::GetFrameTransform(playerFrameId, camFrameId, trans);
for (auto it = Pi::player->GetSensors()->GetContacts().begin(); it != Pi::player->GetSensors()->GetContacts().end(); ++it)
it->trail->SetTransform(trans);
} else {
for (auto it = Pi::player->GetSensors()->GetContacts().begin(); it != Pi::player->GetSensors()->GetContacts().end(); ++it)
it->trail->Reset(playerFrameId);
}
}
void WorldView::OnSwitchTo()
{
if (m_viewController)
m_viewController->Activated();
Pi::input->AddInputFrame(&InputBindings);
}
void WorldView::OnSwitchFrom()
{
if (m_viewController)
m_viewController->Deactivated();
Pi::input->RemoveInputFrame(&InputBindings);
Pi::DrawGUI = true;
}
// XXX paying fine remotely can't really be done until crime and
// worldview are in Lua. I'm leaving this code here so its not
// forgotten
/*
static void PlayerPayFine()
{
Sint64 crime, fine;
Polit::GetCrime(&crime, &fine);
if (Pi::player->GetMoney() == 0) {
m_game->log->Add(Lang::YOU_NO_MONEY);
} else if (fine > Pi::player->GetMoney()) {
Polit::AddCrime(0, -Pi::player->GetMoney());
Polit::GetCrime(&crime, &fine);
m_game->log->Add(stringf(
Lang::FINE_PAID_N_BUT_N_REMAINING,
formatarg("paid", format_money(Pi::player->GetMoney())),
formatarg("fine", format_money(fine))));
Pi::player->SetMoney(0);
} else {
Pi::player->SetMoney(Pi::player->GetMoney() - fine);
m_game->log->Add(stringf(Lang::FINE_PAID_N,
formatarg("fine", format_money(fine))));
Polit::AddCrime(0, -fine);
}
}
*/
int WorldView::GetActiveWeapon() const
{
using CamType = ShipViewController::CamType;
switch (shipView->GetCamType()) {
case CamType::CAM_INTERNAL:
return shipView->m_internalCameraController->GetMode() == InternalCameraController::MODE_REAR ? 1 : 0;
case CamType::CAM_EXTERNAL:
case CamType::CAM_SIDEREAL:
case CamType::CAM_FLYBY:
default:
return 0;
}
}
static inline bool project_to_screen(const vector3d &in, vector3d &out, const Graphics::Frustum &frustum, const int guiSize[2])
{
if (!frustum.ProjectPoint(in, out)) return false;
out.x *= guiSize[0];
out.y = Gui::Screen::GetHeight() - out.y * guiSize[1];
return true;
}
void WorldView::UpdateProjectedObjects()
{
const Frame *cam_frame = Frame::GetFrame(m_cameraContext->GetTempFrame());
matrix3x3d cam_rot = cam_frame->GetOrient().Inverse() * Pi::player->GetOrient();
// later we might want non-ship enemies (e.g., for assaults on military bases)
assert(!Pi::player->GetCombatTarget() || Pi::player->GetCombatTarget()->IsType(ObjectType::SHIP));
// update combat HUD
Ship *enemy = static_cast<Ship *>(Pi::player->GetCombatTarget());
if (enemy) {
const vector3d targScreenPos = enemy->GetInterpPositionRelTo(cam_frame->GetId());
UpdateIndicator(m_combatTargetIndicator, targScreenPos);
// calculate firing solution and relative velocity along our z axis
int laser = -1;
if (shipView->GetCamType() == ShipViewController::CAM_INTERNAL) {
switch (shipView->m_internalCameraController->GetMode()) {
case InternalCameraController::MODE_FRONT: laser = 0; break;
case InternalCameraController::MODE_REAR: laser = 1; break;
default: break;
}
}
if (laser >= 0 && Pi::player->GetFixedGuns()->IsGunMounted(laser)) {
UpdateIndicator(m_targetLeadIndicator, cam_rot * Pi::player->GetFixedGuns()->GetTargetLeadPos());
if ((m_targetLeadIndicator.side != INDICATOR_ONSCREEN) || (m_combatTargetIndicator.side != INDICATOR_ONSCREEN))
HideIndicator(m_targetLeadIndicator);
} else {
HideIndicator(m_targetLeadIndicator);
}
} else {
HideIndicator(m_combatTargetIndicator);
HideIndicator(m_targetLeadIndicator);
}
}
void WorldView::UpdateIndicator(Indicator &indicator, const vector3d &cameraSpacePos)
{
const int guiSize[2] = { Gui::Screen::GetWidth(), Gui::Screen::GetHeight() };
const Graphics::Frustum frustum = m_cameraContext->GetFrustum();
const float BORDER = 10.0;
const float BORDER_BOTTOM = 90.0;
// XXX BORDER_BOTTOM is 10+the control panel height and shouldn't be needed at all
const float w = Gui::Screen::GetWidth();
const float h = Gui::Screen::GetHeight();
if (cameraSpacePos.LengthSqr() < 1e-6) { // length < 1e-3
indicator.pos.x = w / 2.0f;
indicator.pos.y = h / 2.0f;
indicator.side = INDICATOR_ONSCREEN;
return;
}
vector3d proj;
bool success = project_to_screen(cameraSpacePos, proj, frustum, guiSize);
if (!success)
proj = vector3d(w / 2.0, h / 2.0, 0.0);
indicator.realpos.x = int(proj.x);
indicator.realpos.y = int(proj.y);
bool onscreen =
(cameraSpacePos.z < 0.0) &&
(proj.x >= BORDER) && (proj.x < w - BORDER) &&
(proj.y >= BORDER) && (proj.y < h - BORDER_BOTTOM);
if (onscreen) {
indicator.pos.x = int(proj.x);
indicator.pos.y = int(proj.y);
indicator.side = INDICATOR_ONSCREEN;
} else {
// homogeneous 2D points and lines are really useful
const vector3d ptCentre(w / 2.0, h / 2.0, 1.0);
const vector3d ptProj(proj.x, proj.y, 1.0);
const vector3d lnDir = ptProj.Cross(ptCentre);
indicator.side = INDICATOR_TOP;
// this fallback is used if direction is close to (0, 0, +ve)
indicator.pos.x = w / 2.0;
indicator.pos.y = BORDER;
if (cameraSpacePos.x < -1e-3) {
vector3d ptLeft = lnDir.Cross(vector3d(-1.0, 0.0, BORDER));
ptLeft /= ptLeft.z;
if (ptLeft.y >= BORDER && ptLeft.y < h - BORDER_BOTTOM) {
indicator.pos.x = ptLeft.x;
indicator.pos.y = ptLeft.y;
indicator.side = INDICATOR_LEFT;
}
} else if (cameraSpacePos.x > 1e-3) {
vector3d ptRight = lnDir.Cross(vector3d(-1.0, 0.0, w - BORDER));
ptRight /= ptRight.z;
if (ptRight.y >= BORDER && ptRight.y < h - BORDER_BOTTOM) {
indicator.pos.x = ptRight.x;
indicator.pos.y = ptRight.y;
indicator.side = INDICATOR_RIGHT;
}
}
if (cameraSpacePos.y < -1e-3) {
vector3d ptBottom = lnDir.Cross(vector3d(0.0, -1.0, h - BORDER_BOTTOM));
ptBottom /= ptBottom.z;
if (ptBottom.x >= BORDER && ptBottom.x < w - BORDER) {
indicator.pos.x = ptBottom.x;
indicator.pos.y = ptBottom.y;
indicator.side = INDICATOR_BOTTOM;
}
} else if (cameraSpacePos.y > 1e-3) {
vector3d ptTop = lnDir.Cross(vector3d(0.0, -1.0, BORDER));
ptTop /= ptTop.z;
if (ptTop.x >= BORDER && ptTop.x < w - BORDER) {
indicator.pos.x = ptTop.x;
indicator.pos.y = ptTop.y;
indicator.side = INDICATOR_TOP;
}
}
}
}
void WorldView::HideIndicator(Indicator &indicator)
{
indicator.side = INDICATOR_HIDDEN;
indicator.pos = vector2f(0.0f, 0.0f);
}
void WorldView::Draw()
{
assert(m_game);
assert(Pi::player);
m_renderer->ClearDepthBuffer();
View::Draw();
// don't draw crosshairs etc in hyperspace
if (Pi::player->GetFlightState() == Ship::HYPERSPACE) return;
// combat target indicator
DrawCombatTargetIndicator(m_combatTargetIndicator, m_targetLeadIndicator, red);
// glLineWidth(1.0f);
m_renderer->CheckRenderErrors(__FUNCTION__, __LINE__);
}
void WorldView::DrawCombatTargetIndicator(const Indicator &target, const Indicator &lead, const Color &c)
{
if (target.side == INDICATOR_HIDDEN) return;
if (target.side == INDICATOR_ONSCREEN) {
const float x1 = target.pos.x, y1 = target.pos.y;
const float x2 = lead.pos.x, y2 = lead.pos.y;
float xd = x2 - x1, yd = y2 - y1;
if (lead.side != INDICATOR_ONSCREEN) {
xd = 1.0f;
yd = 0.0f;
} else {
float len = xd * xd + yd * yd;
if (len < 1e-6) {
xd = 1.0f;
yd = 0.0f;
} else {
len = sqrt(len);
xd /= len;
yd /= len;
}
}
const vector3f vts[] = {
// target crosshairs
vector3f(x1 + 10 * xd, y1 + 10 * yd, 0.0f),
vector3f(x1 + 20 * xd, y1 + 20 * yd, 0.0f),
vector3f(x1 - 10 * xd, y1 - 10 * yd, 0.0f),
vector3f(x1 - 20 * xd, y1 - 20 * yd, 0.0f),
vector3f(x1 - 10 * yd, y1 + 10 * xd, 0.0f),
vector3f(x1 - 20 * yd, y1 + 20 * xd, 0.0f),
vector3f(x1 + 10 * yd, y1 - 10 * xd, 0.0f),
vector3f(x1 + 20 * yd, y1 - 20 * xd, 0.0f),
// lead crosshairs
vector3f(x2 - 10 * xd, y2 - 10 * yd, 0.0f),
vector3f(x2 + 10 * xd, y2 + 10 * yd, 0.0f),
vector3f(x2 - 10 * yd, y2 + 10 * xd, 0.0f),
vector3f(x2 + 10 * yd, y2 - 10 * xd, 0.0f),
// line between crosshairs
vector3f(x1 + 20 * xd, y1 + 20 * yd, 0.0f),
vector3f(x2 - 10 * xd, y2 - 10 * yd, 0.0f)
};
if (lead.side == INDICATOR_ONSCREEN) {
m_indicator.SetData(14, vts, c);
} else {
m_indicator.SetData(8, vts, c);
}
m_indicator.Draw(m_renderer, m_blendState);
} else {
DrawEdgeMarker(target, c);
}
}
void WorldView::DrawEdgeMarker(const Indicator &marker, const Color &c)
{
const vector2f screenCentre(Gui::Screen::GetWidth() / 2.0f, Gui::Screen::GetHeight() / 2.0f);
vector2f dir = screenCentre - marker.pos;
float len = dir.Length();
dir *= HUD_CROSSHAIR_SIZE / len;
m_edgeMarker.SetColor(c);
m_edgeMarker.SetStart(vector3f(marker.pos, 0.0f));
m_edgeMarker.SetEnd(vector3f(marker.pos + dir, 0.0f));
m_edgeMarker.Draw(m_renderer, m_blendState);
}
// project vector vec onto plane (normal must be normalized)
static vector3d projectVecOntoPlane(const vector3d &vec, const vector3d &normal)
{
return (vec - vec.Dot(normal) * normal);
}
static double wrapAngleToPositive(const double theta)
{
return (theta >= 0.0 ? theta : M_PI * 2 + theta);
}
/*
heading range: 0 - 359 deg
heading 0 - north
heading 90 - east
pitch range: -90 - +90 deg
pitch 0 - level with surface
pitch 90 - up
*/
std::tuple<double, double, double> WorldView::CalculateHeadingPitchRoll(PlaneType pt)
{
FrameId frameId = Pi::player->GetFrame();
if (pt == ROTATIONAL)
frameId = Frame::GetFrame(frameId)->GetRotFrame();
else if (pt == PARENT)
frameId = Frame::GetFrame(frameId)->GetNonRotFrame();
// construct a frame of reference aligned with the ground plane
// and with lines of longitude and latitude
const vector3d up = Pi::player->GetPositionRelTo(frameId).NormalizedSafe();
const vector3d north = projectVecOntoPlane(vector3d(0, 1, 0), up).NormalizedSafe();
const vector3d east = north.Cross(up);
// find the direction that the ship is facing
const auto shpRot = Pi::player->GetOrientRelTo(frameId);
const vector3d hed = -shpRot.VectorZ();
const vector3d right = shpRot.VectorX();
const vector3d groundHed = projectVecOntoPlane(hed, up).NormalizedSafe();
const double pitch = asin(up.Dot(hed));
const double hedNorth = groundHed.Dot(north);
const double hedEast = groundHed.Dot(east);
const double heading = wrapAngleToPositive(atan2(hedEast, hedNorth));
const double roll = (acos(right.Dot(up.Cross(hed).Normalized())) - M_PI) * (right.Dot(up) >= 0 ? -1 : 1);
return std::make_tuple(
std::isnan(heading) ? 0.0 : heading,
std::isnan(pitch) ? 0.0 : pitch,
std::isnan(roll) ? 0.0 : roll);
}
static vector3d projectToScreenSpace(const vector3d &pos, RefCountedPtr<CameraContext> cameraContext, const bool adjustZ = true)
{
const Graphics::Frustum &frustum = cameraContext->GetFrustum();
const float h = Graphics::GetScreenHeight();
const float w = Graphics::GetScreenWidth();
vector3d proj;
if (!frustum.ProjectPoint(pos, proj)) {
return vector3d(w / 2, h / 2, 0);
}
// convert NDC to top-left screen coordinates
proj.x *= w;
proj.y = h - proj.y * h;
// linearize depth coordinate
// see https://thxforthefish.com/posts/reverse_z/
float znear;
float zfar;
Pi::renderer->GetNearFarRange(znear, zfar);
proj.z = -znear / proj.z;
// set z to -1 if in front of camera, 1 else
if (adjustZ)
proj.z = pos.z < 0 ? -1 : 1;
return proj;
}
// project a body in world-space to a screen-space location
vector3d WorldView::WorldSpaceToScreenSpace(const Body *body) const
{
if (body->IsType(ObjectType::PLAYER) && !shipView->IsExteriorView())
return vector3d(0, 0, 0);
vector3d pos = body->GetInterpPositionRelTo(m_cameraContext->GetCameraFrame());
return WorldSpaceToScreenSpace(pos);
}
// position is relative to the parent frame of the camera
// use the Body* overload to convert from other frames' spaces
vector3d WorldView::WorldSpaceToScreenSpace(const vector3d &position) const
{
vector3d pos = (position - m_cameraContext->GetCameraPos()) * m_cameraContext->GetCameraOrient();
return projectToScreenSpace(pos, m_cameraContext);
}
// convert a direction in world-space coordinates to a position in screen-space coordinates
vector3d WorldView::WorldDirToScreenSpace(const vector3d &pos) const
{
// rotate world-space coordinates into the camera frame orientation
return projectToScreenSpace(pos * m_cameraContext->GetCameraOrient(), m_cameraContext, false);
}
vector3d WorldView::CameraSpaceToScreenSpace(const vector3d &pos) const
{
return projectToScreenSpace(pos, m_cameraContext);
}
vector3d WorldView::GetTargetIndicatorScreenPosition(const Body *body) const
{
if (body->IsType(ObjectType::PLAYER) && !shipView->IsExteriorView())
return vector3d(0, 0, 0);
// get the target indicator position in body-local coordinates
vector3d pos = body->GetInterpPositionRelTo(m_cameraContext->GetCameraFrame());
pos += body->GetInterpOrientRelTo(m_cameraContext->GetCameraFrame()) * body->GetTargetIndicatorPosition();
return WorldSpaceToScreenSpace(pos);
}