mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2026-03-25 23:41:15 -05:00
586 lines
20 KiB
C++
586 lines
20 KiB
C++
/*****************************************************************************************
|
|
* *
|
|
* OpenSpace *
|
|
* *
|
|
* Copyright (c) 2014-2020 *
|
|
* *
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
|
|
* software and associated documentation files (the "Software"), to deal in the Software *
|
|
* without restriction, including without limitation the rights to use, copy, modify, *
|
|
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
|
|
* permit persons to whom the Software is furnished to do so, subject to the following *
|
|
* conditions: *
|
|
* *
|
|
* The above copyright notice and this permission notice shall be included in all copies *
|
|
* or substantial portions of the Software. *
|
|
* *
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
|
|
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
|
|
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
|
|
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
|
|
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
|
|
****************************************************************************************/
|
|
|
|
#include <openspace/rendering/loadingscreen.h>
|
|
|
|
#include <openspace/engine/globals.h>
|
|
#include <openspace/engine/windowdelegate.h>
|
|
#include <openspace/rendering/helper.h>
|
|
#include <ghoul/fmt.h>
|
|
#include <ghoul/filesystem/filesystem.h>
|
|
#include <ghoul/font/font.h>
|
|
#include <ghoul/font/fontmanager.h>
|
|
#include <ghoul/font/fontrenderer.h>
|
|
#include <ghoul/io/texture/texturereader.h>
|
|
#include <ghoul/logging/logmanager.h>
|
|
#include <ghoul/misc/profiling.h>
|
|
#include <ghoul/misc/stringconversion.h>
|
|
#include <ghoul/opengl/programobject.h>
|
|
#include <ghoul/opengl/texture.h>
|
|
#include <ghoul/opengl/textureunit.h>
|
|
#include <random>
|
|
#include <thread>
|
|
|
|
namespace {
|
|
constexpr const float LoadingFontSize = 25.f;
|
|
constexpr const float MessageFontSize = 22.f;
|
|
constexpr const float ItemFontSize = 10.f;
|
|
|
|
constexpr const glm::vec2 LogoCenter = glm::vec2(0.f, 0.525f); // in NDC
|
|
constexpr const glm::vec2 LogoSize = glm::vec2(0.275f, 0.275); // in NDC
|
|
|
|
constexpr const glm::vec2 ProgressbarCenter = glm::vec2(0.f, -0.75f); // in NDC
|
|
constexpr const glm::vec2 ProgressbarSize = glm::vec2(0.7f, 0.0075f); // in NDC
|
|
constexpr const float ProgressbarLineWidth = 0.0025f; // in NDC
|
|
|
|
constexpr const glm::vec4 ProgressbarOutlineColor =
|
|
glm::vec4(0.9f, 0.9f, 0.9f, 1.f);
|
|
|
|
constexpr const glm::vec4 PhaseColorConstruction = glm::vec4(0.7f, 0.7f, 0.f, 1.f);
|
|
constexpr const glm::vec4 PhaseColorSynchronization =
|
|
glm::vec4(0.9f, 0.9f, 0.9f, 1.f);
|
|
constexpr const glm::vec4 PhaseColorInitialization =
|
|
glm::vec4(0.1f, 0.75f, 0.1f, 1.f);
|
|
|
|
constexpr const glm::vec4 ItemStatusColorStarted = glm::vec4(0.5f, 0.5f, 0.5f, 1.f);
|
|
constexpr const glm::vec4 ItemStatusColorInitializing =
|
|
glm::vec4(0.7f, 0.7f, 0.f, 1.f);
|
|
constexpr const glm::vec4 ItemStatusColorFinished = glm::vec4(0.1f, 0.75f, 0.1f, 1.f);
|
|
constexpr const glm::vec4 ItemStatusColorFailed = glm::vec4(0.8f, 0.1f, 0.1f, 1.f);
|
|
|
|
constexpr const float ItemStandoffDistance = 5.f; // in pixels
|
|
|
|
constexpr const float LoadingTextPosition = 0.275f; // in NDC
|
|
constexpr const float StatusMessageOffset = 0.225f; // in NDC
|
|
|
|
constexpr const int MaxNumberLocationSamples = 1000;
|
|
|
|
constexpr const std::chrono::milliseconds TTL(5000);
|
|
|
|
constexpr const std::chrono::milliseconds RefreshRate(16);
|
|
|
|
bool rectOverlaps(glm::vec2 lhsLl, glm::vec2 lhsUr, glm::vec2 rhsLl, glm::vec2 rhsUr)
|
|
{
|
|
lhsLl -= glm::vec2(ItemStandoffDistance / 2.f);
|
|
lhsUr += glm::vec2(ItemStandoffDistance / 2.f);
|
|
|
|
rhsLl -= glm::vec2(ItemStandoffDistance / 2.f);
|
|
rhsUr += glm::vec2(ItemStandoffDistance / 2.f);
|
|
|
|
return !(
|
|
lhsUr.x < rhsLl.x ||
|
|
lhsLl.x > rhsUr.x ||
|
|
lhsUr.y < rhsLl.y ||
|
|
lhsLl.y > rhsUr.y
|
|
);
|
|
}
|
|
|
|
glm::vec2 ndcToScreen(glm::vec2 ndc, glm::ivec2 res) {
|
|
ndc.x = (ndc.x + 1.f) / 2.f * res.x;
|
|
ndc.y = (ndc.y + 1.f) / 2.f * res.y;
|
|
return ndc;
|
|
}
|
|
} // namespace
|
|
|
|
namespace openspace {
|
|
|
|
LoadingScreen::LoadingScreen(ShowMessage showMessage, ShowNodeNames showNodeNames,
|
|
ShowProgressbar showProgressbar)
|
|
: _showMessage(showMessage)
|
|
, _showNodeNames(showNodeNames)
|
|
, _showProgressbar(showProgressbar)
|
|
, _randomEngine(_randomDevice())
|
|
{
|
|
_loadingFont = global::fontManager.font(
|
|
"Loading",
|
|
LoadingFontSize,
|
|
ghoul::fontrendering::FontManager::Outline::No,
|
|
ghoul::fontrendering::FontManager::LoadGlyphs::No
|
|
);
|
|
|
|
if (_showMessage) {
|
|
_messageFont = global::fontManager.font(
|
|
"Loading",
|
|
MessageFontSize,
|
|
ghoul::fontrendering::FontManager::Outline::No,
|
|
ghoul::fontrendering::FontManager::LoadGlyphs::No
|
|
);
|
|
}
|
|
|
|
if (_showNodeNames) {
|
|
_itemFont = global::fontManager.font(
|
|
"Loading",
|
|
ItemFontSize,
|
|
ghoul::fontrendering::FontManager::Outline::No,
|
|
ghoul::fontrendering::FontManager::LoadGlyphs::No
|
|
);
|
|
}
|
|
|
|
{
|
|
// Logo stuff
|
|
_logoTexture = ghoul::io::TextureReader::ref().loadTexture(
|
|
absPath("${DATA}/openspace-logo.png")
|
|
);
|
|
_logoTexture->uploadTexture();
|
|
}
|
|
}
|
|
|
|
LoadingScreen::~LoadingScreen() {
|
|
_logoTexture = nullptr;
|
|
|
|
_loadingFont = nullptr;
|
|
_messageFont = nullptr;
|
|
_itemFont = nullptr;
|
|
}
|
|
|
|
void LoadingScreen::render() {
|
|
ZoneScoped
|
|
FrameMarkStart("Loading")
|
|
|
|
if (_phase == Phase::PreStart) {
|
|
return;
|
|
}
|
|
|
|
// We have to recalculate the positions here because we will not be informed about a
|
|
// window size change
|
|
|
|
const glm::vec2 dpiScaling = global::windowDelegate.dpiScaling();
|
|
const glm::ivec2 res =
|
|
glm::vec2(global::windowDelegate.currentSubwindowSize()) * dpiScaling;
|
|
|
|
float screenAspectRatio = static_cast<float>(res.x) / static_cast<float>(res.y);
|
|
|
|
float textureAspectRatio = static_cast<float>(_logoTexture->dimensions().x) /
|
|
static_cast<float>(_logoTexture->dimensions().y);
|
|
|
|
ghoul::fontrendering::FontRenderer::defaultRenderer().setFramebufferSize(res);
|
|
|
|
const glm::vec2 size = {
|
|
LogoSize.x,
|
|
LogoSize.y * textureAspectRatio * screenAspectRatio
|
|
};
|
|
|
|
//
|
|
// Clear background
|
|
//
|
|
glClearColor(0.f, 0.f, 0.f, 1.f);
|
|
glClear(ClearBufferMask::GL_COLOR_BUFFER_BIT);
|
|
glViewport(0, 0, res.x, res.y);
|
|
glDisable(GL_CULL_FACE);
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
glDisable(GL_DEPTH_TEST);
|
|
|
|
//
|
|
// Render logo
|
|
//
|
|
rendering::helper::renderBox(
|
|
glm::vec2(1.f) - ((LogoCenter + glm::vec2(1.f)) / 2.f),
|
|
size,
|
|
glm::vec4(1.f),
|
|
*_logoTexture,
|
|
rendering::helper::Anchor::Center
|
|
);
|
|
|
|
//
|
|
// Render progress bar
|
|
//
|
|
const glm::vec2 progressbarSize = {
|
|
ProgressbarSize.x,
|
|
ProgressbarSize.y * screenAspectRatio
|
|
};
|
|
|
|
if (_showProgressbar) {
|
|
const float progress = _nItems != 0 ?
|
|
static_cast<float>(_iProgress) / static_cast<float>(_nItems) :
|
|
0.f;
|
|
|
|
const float w = ProgressbarLineWidth / screenAspectRatio;
|
|
const float h = ProgressbarLineWidth;
|
|
rendering::helper::renderBox(
|
|
glm::vec2(1.f) - ((ProgressbarCenter + glm::vec2(1.f)) / 2.f),
|
|
progressbarSize + glm::vec2(2 * w, 2 * h),
|
|
ProgressbarOutlineColor,
|
|
rendering::helper::Anchor::Center
|
|
);
|
|
|
|
rendering::helper::renderBox(
|
|
glm::vec2(1.f) - ((ProgressbarCenter + glm::vec2(1.f)) / 2.f),
|
|
progressbarSize,
|
|
glm::vec4(0.f, 0.f, 0.f, 1.f),
|
|
rendering::helper::Anchor::Center
|
|
);
|
|
|
|
glm::vec4 color = glm::vec4(0.f);
|
|
switch (_phase) {
|
|
case Phase::PreStart:
|
|
break;
|
|
case Phase::Construction:
|
|
color = PhaseColorConstruction;
|
|
break;
|
|
case Phase::Synchronization:
|
|
color = PhaseColorSynchronization;
|
|
break;
|
|
case Phase::Initialization:
|
|
color = PhaseColorInitialization;
|
|
break;
|
|
}
|
|
|
|
glm::vec2 p = glm::vec2(1.f) - ((ProgressbarCenter + glm::vec2(1.f)) / 2.f);
|
|
rendering::helper::renderBox(
|
|
p - progressbarSize / 2.f,
|
|
progressbarSize * glm::vec2(progress, 1.f),
|
|
color,
|
|
rendering::helper::Anchor::NW
|
|
);
|
|
}
|
|
|
|
//
|
|
// "Loading" text
|
|
//
|
|
using FR = ghoul::fontrendering::FontRenderer;
|
|
const FR& renderer = FR::defaultRenderer();
|
|
|
|
const std::string headline =
|
|
_hasCatastrophicErrorOccurred ?
|
|
"Failure":
|
|
"Loading...";
|
|
// We use "Loading" to center the text, but render "Loading..." to make it look more
|
|
// pleasing
|
|
const glm::vec2 bbox = _loadingFont->boundingBox(
|
|
headline.substr(0, headline.size() - 2)
|
|
);
|
|
|
|
const glm::vec2 loadingLl = glm::vec2(
|
|
res.x / 2.f - bbox.x / 2.f,
|
|
res.y * LoadingTextPosition
|
|
);
|
|
const glm::vec2 loadingUr = loadingLl + bbox;
|
|
|
|
renderer.render(*_loadingFont, loadingLl, headline);
|
|
|
|
glm::vec2 messageLl = glm::vec2(0.f);
|
|
glm::vec2 messageUr = glm::vec2(0.f);
|
|
if (_showMessage) {
|
|
std::lock_guard<std::mutex> guard(_messageMutex);
|
|
|
|
const glm::vec2 bboxMessage = _messageFont->boundingBox(_message);
|
|
|
|
messageLl = glm::vec2(
|
|
res.x / 2.f - bboxMessage.x / 2.f,
|
|
res.y * StatusMessageOffset
|
|
);
|
|
messageUr = messageLl + bboxMessage;
|
|
|
|
|
|
renderer.render(*_messageFont, messageLl, _message);
|
|
}
|
|
|
|
if (_showNodeNames) {
|
|
std::lock_guard<std::mutex> guard(_itemsMutex);
|
|
|
|
std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
|
|
|
|
const glm::vec2 logoLl = { LogoCenter.x - size.x, LogoCenter.y - size.y };
|
|
const glm::vec2 logoUr = { LogoCenter.x + size.x, LogoCenter.y + size.y };
|
|
|
|
const glm::vec2 progressbarLl = {
|
|
ProgressbarCenter.x - progressbarSize.x,
|
|
ProgressbarCenter.y - progressbarSize.y
|
|
};
|
|
const glm::vec2 progressbarUr = {
|
|
ProgressbarCenter.x + progressbarSize.x ,
|
|
ProgressbarCenter.y + progressbarSize.y
|
|
};
|
|
|
|
for (Item& item : _items) {
|
|
if (!item.hasLocation) {
|
|
// Compute a new location
|
|
|
|
const glm::vec2 b = _itemFont->boundingBox(
|
|
(item.name + " 100%\n99999999/99999999")
|
|
);
|
|
|
|
// The maximum count is in here since we can't control the amount of
|
|
// screen estate and the number of nodes. Rather than looping forever
|
|
// we make use with an overlap in the worst case
|
|
bool foundSpace = false;
|
|
|
|
glm::vec2 ll = glm::vec2(0.f);
|
|
glm::vec2 ur = glm::vec2(0.f);
|
|
int i = 0;
|
|
for (; i < MaxNumberLocationSamples && !foundSpace; ++i) {
|
|
std::uniform_int_distribution<int> distX(
|
|
15,
|
|
static_cast<int>(res.x - b.x - 15)
|
|
);
|
|
std::uniform_int_distribution<int> distY(
|
|
15,
|
|
static_cast<int>(res.y - b.y - 15)
|
|
);
|
|
|
|
ll = { distX(_randomEngine), distY(_randomEngine) };
|
|
ur = ll + b;
|
|
|
|
// Test against logo and text
|
|
const bool logoOverlap = rectOverlaps(
|
|
ndcToScreen(logoLl, res), ndcToScreen(logoUr, res),
|
|
ll, ur
|
|
);
|
|
|
|
const bool loadingOverlap = rectOverlaps(
|
|
loadingLl, loadingUr,
|
|
ll, ur
|
|
);
|
|
|
|
const bool messageOverlap = _showMessage ?
|
|
rectOverlaps(messageLl, messageUr, ll, ur) :
|
|
false;
|
|
|
|
const bool barOverlap = _showProgressbar ?
|
|
rectOverlaps(
|
|
ndcToScreen(progressbarLl, res),
|
|
ndcToScreen(progressbarUr, res),
|
|
ll,
|
|
ur
|
|
) :
|
|
false;
|
|
|
|
if (logoOverlap || loadingOverlap || messageOverlap || barOverlap) {
|
|
// We never want to have an overlap with these, so this try didn't
|
|
// count against the maximum, thus ensuring that (if there has to
|
|
// be an overlap, it's over other text that might disappear before
|
|
// this one)
|
|
--i;
|
|
continue;
|
|
}
|
|
|
|
// Test against all other boxes
|
|
bool overlap = false;
|
|
for (const Item& j : _items) {
|
|
overlap |= rectOverlaps(j.ll, j.ur, ll, ur);
|
|
|
|
if (overlap) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!overlap) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
item.ll = ll;
|
|
item.ur = ur;
|
|
|
|
item.hasLocation = true;
|
|
|
|
#ifdef LOADINGSCREEN_DEBUGGING
|
|
item.exhaustedSearch = (i == MaxNumberLocationSamples);
|
|
#endif // LOADINGSCREEN_DEBUGGING
|
|
|
|
}
|
|
|
|
glm::vec4 color = [status = item.status]() {
|
|
switch (status) {
|
|
case ItemStatus::Started:
|
|
return ItemStatusColorStarted;
|
|
case ItemStatus::Initializing:
|
|
return ItemStatusColorInitializing;
|
|
case ItemStatus::Finished:
|
|
return ItemStatusColorFinished;
|
|
case ItemStatus::Failed:
|
|
return ItemStatusColorFailed;
|
|
default:
|
|
return glm::vec4(1.f);
|
|
}
|
|
}();
|
|
|
|
if (item.status == ItemStatus::Finished) {
|
|
const auto t = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
now - item.finishedTime
|
|
);
|
|
|
|
color.a = 1.f - static_cast<float>(t.count()) /
|
|
static_cast<float>(TTL.count());
|
|
}
|
|
|
|
#ifdef LOADINGSCREEN_DEBUGGING
|
|
if (item.exhaustedSearch) {
|
|
color = glm::vec4(0.f, 1.f, 1.f, 1.f);
|
|
}
|
|
#endif // LOADINGSCREEN_DEBUGGING
|
|
|
|
std::string text = item.name;
|
|
if (item.status == ItemStatus::Started && item.progress.progress > 0) {
|
|
ProgressInfo& info = item.progress;
|
|
bool hasSecondLine = (info.totalSize != -1 && info.currentSize != -1);
|
|
|
|
int p = static_cast<int>(std::round(info.progress * 100));
|
|
if (hasSecondLine) {
|
|
if (info.totalSize < 1024 * 1024) { // 1MB
|
|
text = fmt::format(
|
|
"{} ({}%)\n{}/{} {}",
|
|
text, p, info.currentSize, info.totalSize, "bytes"
|
|
);
|
|
}
|
|
else {
|
|
float curr = info.currentSize / (1024.f * 1024.f);
|
|
float total = info.totalSize / (1024.f * 1024.f);
|
|
|
|
text = fmt::format(
|
|
"{} ({}%)\n{:.3f}/{:.3f} {}",
|
|
text, p, curr, total, "MB"
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
text = fmt::format("{} ({}%)", text, p);
|
|
}
|
|
}
|
|
|
|
renderer.render(*_itemFont, item.ll, text, color);
|
|
}
|
|
|
|
_items.erase(
|
|
std::remove_if(
|
|
_items.begin(),
|
|
_items.end(),
|
|
[now](const Item& i) {
|
|
if (i.status == ItemStatus::Finished) {
|
|
return i.finishedTime > now + TTL;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
),
|
|
_items.end()
|
|
);
|
|
|
|
}
|
|
|
|
glEnable(GL_CULL_FACE);
|
|
glEnable(GL_DEPTH_TEST);
|
|
|
|
std::this_thread::sleep_for(RefreshRate);
|
|
global::windowDelegate.swapBuffer();
|
|
FrameMarkEnd("Loading")
|
|
}
|
|
|
|
void LoadingScreen::postMessage(std::string message) {
|
|
std::lock_guard<std::mutex> guard(_messageMutex);
|
|
_message = std::move(message);
|
|
}
|
|
|
|
void LoadingScreen::setCatastrophicError(CatastrophicError catastrophicError) {
|
|
_hasCatastrophicErrorOccurred = catastrophicError;
|
|
}
|
|
|
|
void LoadingScreen::finalize() {
|
|
_items.erase(
|
|
std::remove_if(
|
|
_items.begin(),
|
|
_items.end(),
|
|
[](const Item& i) {
|
|
return i.status != ItemStatus::Failed;
|
|
}
|
|
),
|
|
_items.end()
|
|
);
|
|
|
|
render();
|
|
}
|
|
|
|
|
|
void LoadingScreen::setItemNumber(int nItems) {
|
|
_nItems = nItems;
|
|
}
|
|
|
|
int LoadingScreen::itemNumber() {
|
|
return _nItems;
|
|
}
|
|
|
|
void LoadingScreen::tickItem() {
|
|
++_iProgress;
|
|
}
|
|
|
|
void LoadingScreen::setPhase(Phase phase) {
|
|
_phase = phase;
|
|
_iProgress = 0;
|
|
}
|
|
|
|
void LoadingScreen::updateItem(const std::string& itemIdentifier,
|
|
const std::string& itemName, ItemStatus newStatus,
|
|
ProgressInfo progressInfo)
|
|
{
|
|
if (!_showNodeNames) {
|
|
// If we don't want to show the node names, we can disable the updating which
|
|
// also would create any of the text information
|
|
return;
|
|
}
|
|
std::lock_guard<std::mutex> guard(_itemsMutex);
|
|
|
|
auto it = std::find_if(
|
|
_items.begin(),
|
|
_items.end(),
|
|
[&itemIdentifier](const Item& i) {
|
|
return i.identifier == itemIdentifier;
|
|
}
|
|
);
|
|
if (it != _items.end()) {
|
|
it->status = newStatus;
|
|
it->progress = std::move(progressInfo);
|
|
if (newStatus == ItemStatus::Finished) {
|
|
it->finishedTime = std::chrono::system_clock::now();
|
|
}
|
|
}
|
|
else {
|
|
ghoul_assert(
|
|
newStatus == ItemStatus::Started,
|
|
fmt::format(
|
|
"Item '{}' did not exist and first message was not 'Started'",
|
|
itemIdentifier
|
|
)
|
|
);
|
|
// We are not computing the location in here since doing it this way might stall
|
|
// the main thread while trying to find a position for the new item
|
|
_items.push_back({
|
|
itemIdentifier,
|
|
itemName,
|
|
ItemStatus::Started,
|
|
std::move(progressInfo),
|
|
false,
|
|
#ifdef LOADINGSCREEN_DEBUGGING
|
|
false,
|
|
#endif // LOADINGSCREEN_DEBUGGING
|
|
{},
|
|
{},
|
|
std::chrono::system_clock::from_time_t(0)
|
|
});
|
|
}
|
|
}
|
|
|
|
} // namespace openspace
|