mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2025-12-31 00:10:44 -06:00
516 lines
22 KiB
C++
516 lines
22 KiB
C++
/*****************************************************************************************
|
|
* *
|
|
* OpenSpace *
|
|
* *
|
|
* Copyright (c) 2014-2022 *
|
|
* *
|
|
* 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 <modules/base/rendering/renderabletrailorbit.h>
|
|
|
|
#include <openspace/documentation/documentation.h>
|
|
#include <openspace/documentation/verifier.h>
|
|
#include <openspace/scene/translation.h>
|
|
#include <openspace/util/updatestructures.h>
|
|
#include <openspace/engine/globals.h>
|
|
#include <openspace/events/event.h>
|
|
#include <openspace/events/eventengine.h>
|
|
#include <openspace/rendering/renderengine.h>
|
|
#include <openspace/scripting/scriptengine.h>
|
|
#include <openspace/scene/scene.h>
|
|
|
|
#include <ghoul/opengl/programobject.h>
|
|
#include <numeric>
|
|
#include <optional>
|
|
|
|
// This class is using a VBO ring buffer + a constantly updated point as follows:
|
|
// Structure of the array with a _resolution of 16. FF denotes the floating position that
|
|
// is updated every frame:
|
|
// ---------------------------------------------------------------------------------
|
|
// | FF | | | | | | | | | | | | | | | |
|
|
// ---------------------------------------------------------------------------------
|
|
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
|
// <------ newer in time oldest
|
|
//
|
|
// In the begining the floating value starts at 0; this means that array element 0 is
|
|
// updated and uploaded to the GPU at every frame. The FF+1 element is the newest fixed
|
|
// location and FF-1 element is the oldest fixed location (including wrapping around the
|
|
// array) with the times of _lastPointTime and _firstPointTime.
|
|
//
|
|
// If the time progresses forwards and abs(time - _lastPointTime) becomes big enough, the
|
|
// oldest point is removed and a new fixed location is added. In the ring buffer this
|
|
// would be represented as:
|
|
// ---------------------------------------------------------------------------------
|
|
// | | | | | | | | | | | | | | | | FF |
|
|
// ---------------------------------------------------------------------------------
|
|
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
|
// <------ newer in time oldest
|
|
//
|
|
// Thus making the floating point traverse backwards through the array and element 0 being
|
|
// the newest fixed point. If the time processes backwards, the floating point moves
|
|
// towards the upper areas of the array instead.
|
|
// In both cases, only the values that have been changed will be uploaded to the GPU.
|
|
//
|
|
// For the rendering, this is achieved by using an index buffer that is twice the size of
|
|
// the vertex buffer containing identical two sequences indexing the vertex array.
|
|
// In an example of size 8:
|
|
// ---------------------------------------------------------------------------------------
|
|
// |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15| 0| 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|13|14|15|
|
|
// ---------------------------------------------------------------------------------------
|
|
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
|
//
|
|
// The rendering step needs to know only the offset into the array (denoted by FF as the
|
|
// floating position above) and use the index array from the position. Since the indices
|
|
// in this array wrap around, so will the rendering of the vertices. Example:
|
|
// FF := 10
|
|
// Rendering 16 elements will 'generate' the index buffer:
|
|
// 10 11 12 13 14 15 00 01 02 03 04 05 06 07 08 09
|
|
//
|
|
//
|
|
// NB: This method was implemented without a ring buffer before by manually shifting the
|
|
// items in memory as was shown to be much slower than the current system. ---abock
|
|
|
|
namespace {
|
|
constexpr openspace::properties::Property::PropertyInfo PeriodInfo = {
|
|
"Period",
|
|
"Period (in days)",
|
|
"The objects period, i.e. the length of its orbit around the parent object given "
|
|
"in (Earth) days. In the case of Earth, this would be a sidereal year "
|
|
"(=365.242 days). If this values is specified as multiples of the period, it is "
|
|
"possible to show the effects of precession."
|
|
};
|
|
|
|
constexpr openspace::properties::Property::PropertyInfo ResolutionInfo = {
|
|
"Resolution",
|
|
"Number of samples along the orbit",
|
|
"The number of samples along the orbit. This determines the resolution of the "
|
|
"trail; the tradeoff being that a higher resolution is able to resolve more "
|
|
"detail, but will take more resources while rendering, too. The higher, the "
|
|
"smoother the trail, but also more memory will be used."
|
|
};
|
|
|
|
constexpr openspace::properties::Property::PropertyInfo RenderableTypeInfo = {
|
|
"RenderableType",
|
|
"RenderableType",
|
|
"This value specifies if the orbit should be rendered in the Background,"
|
|
"Opaque, Transparent, or Overlay rendering step. Default is Transparent."
|
|
};
|
|
|
|
struct [[codegen::Dictionary(RenderableTrailOrbit)]] Parameters {
|
|
// [[codegen::verbatim(PeriodInfo.description)]]
|
|
double period;
|
|
|
|
// [[codegen::verbatim(ResolutionInfo.description)]]
|
|
int resolution;
|
|
|
|
enum class [[codegen::map(openspace::Renderable::RenderBin)]] RenderableType {
|
|
Background,
|
|
Opaque,
|
|
PreDeferredTransparent,
|
|
PostDeferredTransparent,
|
|
Overlay
|
|
};
|
|
|
|
// [[codegen::verbatim(RenderableTypeInfo.description)]]
|
|
std::optional<RenderableType> renderableType;
|
|
};
|
|
#include "renderabletrailorbit_codegen.cpp"
|
|
} // namespace
|
|
|
|
namespace openspace {
|
|
|
|
documentation::Documentation RenderableTrailOrbit::Documentation() {
|
|
documentation::Documentation doc = codegen::doc<Parameters>(
|
|
"base_renderable_renderabletrailorbit"
|
|
);
|
|
|
|
// Insert the parents documentation entries until we have a verifier that can deal
|
|
// with class hierarchy
|
|
documentation::Documentation parentDoc = RenderableTrail::Documentation();
|
|
doc.entries.insert(
|
|
doc.entries.end(),
|
|
parentDoc.entries.begin(),
|
|
parentDoc.entries.end()
|
|
);
|
|
|
|
return doc;
|
|
}
|
|
|
|
RenderableTrailOrbit::RenderableTrailOrbit(const ghoul::Dictionary& dictionary)
|
|
: RenderableTrail(dictionary)
|
|
, _period(PeriodInfo, 0.0, 0.0, 1e9)
|
|
, _resolution(ResolutionInfo, 10000, 1, 1000000)
|
|
{
|
|
const Parameters p = codegen::bake<Parameters>(dictionary);
|
|
|
|
_translation->onParameterChange([this]() { _needsFullSweep = true; });
|
|
|
|
// Period is in days
|
|
using namespace std::chrono;
|
|
_period = p.period * duration_cast<seconds>(hours(24)).count();
|
|
_period.onChange([&] { _needsFullSweep = true; _indexBufferDirty = true; });
|
|
_period.setExponent(5.f);
|
|
addProperty(_period);
|
|
|
|
_resolution = p.resolution;
|
|
_resolution.onChange([&] { _needsFullSweep = true; _indexBufferDirty = true; });
|
|
_resolution.setExponent(3.5f);
|
|
addProperty(_resolution);
|
|
|
|
// We store the vertices with (excluding the wrapping) decending temporal order
|
|
_primaryRenderInformation.sorting = RenderInformation::VertexSorting::NewestFirst;
|
|
|
|
if (p.renderableType.has_value()) {
|
|
setRenderBin(codegen::map<Renderable::RenderBin>(*p.renderableType));
|
|
}
|
|
else {
|
|
setRenderBin(Renderable::RenderBin::Overlay);
|
|
}
|
|
}
|
|
|
|
void RenderableTrailOrbit::initializeGL() {
|
|
RenderableTrail::initializeGL();
|
|
|
|
glGenVertexArrays(1, &_primaryRenderInformation._vaoID);
|
|
glGenBuffers(1, &_primaryRenderInformation._vBufferID);
|
|
glGenBuffers(1, &_primaryRenderInformation._iBufferID);
|
|
}
|
|
|
|
void RenderableTrailOrbit::deinitializeGL() {
|
|
glDeleteVertexArrays(1, &_primaryRenderInformation._vaoID);
|
|
glDeleteBuffers(1, &_primaryRenderInformation._vBufferID);
|
|
glDeleteBuffers(1, &_primaryRenderInformation._iBufferID);
|
|
|
|
RenderableTrail::deinitializeGL();
|
|
}
|
|
|
|
void RenderableTrailOrbit::update(const UpdateData& data) {
|
|
// Overview:
|
|
// 1. Update trails
|
|
// 2. Update floating position
|
|
// 3. Determine which parts of the array to upload and upload the data
|
|
|
|
// 1
|
|
// Update the trails; the report contains whether any of the other values has been
|
|
// touched and if so, how many
|
|
const UpdateReport report = updateTrails(data);
|
|
_previousTime = data.time.j2000Seconds();
|
|
|
|
// Do not do anything if no point needs to be updated
|
|
if (!report.permanentPointsNeedUpdate && !report.floatingPointNeedsUpdate) {
|
|
return;
|
|
}
|
|
|
|
// 2
|
|
// Write the current location into the floating position
|
|
const glm::vec3 p = _translation->position({
|
|
{},
|
|
data.time,
|
|
Time(0.0)
|
|
});
|
|
_vertexArray[_primaryRenderInformation.first] = { p.x, p.y, p.z };
|
|
|
|
glBindVertexArray(_primaryRenderInformation._vaoID);
|
|
glBindBuffer(GL_ARRAY_BUFFER, _primaryRenderInformation._vBufferID);
|
|
|
|
// 3
|
|
if (!report.permanentPointsNeedUpdate) {
|
|
if (report.floatingPointNeedsUpdate) {
|
|
// If no other values have been touched, we only need to upload the
|
|
// floating value
|
|
glBufferSubData(
|
|
GL_ARRAY_BUFFER,
|
|
_primaryRenderInformation.first * sizeof(TrailVBOLayout),
|
|
sizeof(TrailVBOLayout),
|
|
_vertexArray.data() + _primaryRenderInformation.first
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
// Otherwise we need to check how many values have been changed
|
|
if (report.nUpdated == UpdateReport::All) {
|
|
// If all of the values have been invalidated, we need to upload the entire
|
|
// array
|
|
glBufferData(
|
|
GL_ARRAY_BUFFER,
|
|
_vertexArray.size() * sizeof(TrailVBOLayout),
|
|
_vertexArray.data(),
|
|
GL_STREAM_DRAW
|
|
);
|
|
|
|
if (_indexBufferDirty) {
|
|
// We only need to upload the index buffer if it has been invalidated
|
|
// by changing the number of values we want to represent
|
|
glBindBuffer(
|
|
GL_ELEMENT_ARRAY_BUFFER,
|
|
_primaryRenderInformation._iBufferID
|
|
);
|
|
glBufferData(
|
|
GL_ELEMENT_ARRAY_BUFFER,
|
|
_indexArray.size() * sizeof(unsigned int),
|
|
_indexArray.data(),
|
|
GL_STATIC_DRAW
|
|
);
|
|
_indexBufferDirty = false;
|
|
}
|
|
}
|
|
else {
|
|
// The lambda expression that will upload parts of the array starting at
|
|
// begin and containing length number of elements
|
|
auto upload = [this](int begin, int length) {
|
|
glBufferSubData(
|
|
GL_ARRAY_BUFFER,
|
|
begin * sizeof(TrailVBOLayout),
|
|
sizeof(TrailVBOLayout) * length,
|
|
_vertexArray.data() + begin
|
|
);
|
|
};
|
|
|
|
// Only update the changed ones
|
|
// Since we are using a ring buffer, the number of updated needed might be
|
|
// bigger than our current points, which means we have to split the upload
|
|
// into two calls.
|
|
if (report.nUpdated > 0) {
|
|
// deltaT is positive, so the pointer is moving backwards and update has
|
|
// to happen towards the front
|
|
|
|
// Starting index
|
|
const int i = _primaryRenderInformation.first;
|
|
// Number of values
|
|
const int n = report.nUpdated + 1; // +1 for the floating position
|
|
// Total size of the array
|
|
const int s = _primaryRenderInformation.count;
|
|
|
|
if (i + n <= s) {
|
|
// The current index is small enough to just use one upload call
|
|
upload(i, n);
|
|
}
|
|
else {
|
|
// The current index is too close to the wrap around part, so we need
|
|
// to split the upload into two parts:
|
|
// 1. from the current index to the end of the array
|
|
// 2. the rest starting from the beginning of the array
|
|
const int first = s - i;
|
|
const int second = n - first;
|
|
upload(i, first); // 1
|
|
upload(0, second); // 2
|
|
}
|
|
}
|
|
else {
|
|
// deltaT is negative, so the pointer is moving forwards
|
|
|
|
// The current index
|
|
const int i = _primaryRenderInformation.first;
|
|
// Number of values
|
|
const int n = std::abs(report.nUpdated) + 1; // +1 for the floating pos
|
|
// Total size of the array
|
|
const int s = _primaryRenderInformation.count;
|
|
|
|
if (i + 1 >= n) {
|
|
// The current index is big enough to fit everything into one call
|
|
upload(i+1 - n, n);
|
|
}
|
|
else {
|
|
// The current index is too close to the beginning of the array, so we
|
|
// need to split the upload into two parts:
|
|
// 1. from the beginning of the array to the current index
|
|
// 2. filling the back of the array with the rest
|
|
const int b = n - (i + 1);
|
|
upload(0, i + 1); // 1
|
|
upload(s-b, b); // 2
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
glEnableVertexAttribArray(0);
|
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
|
|
|
|
glBindVertexArray(0);
|
|
|
|
// Updating bounding sphere
|
|
glm::vec3 maxVertex(-std::numeric_limits<float>::max());
|
|
glm::vec3 minVertex(std::numeric_limits<float>::max());
|
|
|
|
auto setMax = [&maxVertex, &minVertex](const TrailVBOLayout& vertexData) {
|
|
maxVertex.x = std::max(maxVertex.x, vertexData.x);
|
|
maxVertex.y = std::max(maxVertex.y, vertexData.y);
|
|
maxVertex.z = std::max(maxVertex.z, vertexData.z);
|
|
|
|
minVertex.x = std::min(minVertex.x, vertexData.x);
|
|
minVertex.y = std::min(minVertex.y, vertexData.y);
|
|
minVertex.z = std::min(minVertex.z, vertexData.z);
|
|
};
|
|
|
|
std::for_each(_vertexArray.begin(), _vertexArray.end(), setMax);
|
|
|
|
setBoundingSphere(glm::distance(maxVertex, minVertex) / 2.f);
|
|
}
|
|
|
|
RenderableTrailOrbit::UpdateReport RenderableTrailOrbit::updateTrails(
|
|
const UpdateData& data)
|
|
{
|
|
if (_needsFullSweep) {
|
|
fullSweep(data.time.j2000Seconds());
|
|
return { false, true, UpdateReport::All } ;
|
|
}
|
|
|
|
|
|
constexpr const double Epsilon = 1e-7;
|
|
// When time stands still (at the iron hill), we don't need to perform any work
|
|
if (std::abs(data.time.j2000Seconds() - _previousTime) < Epsilon) {
|
|
return { false, false, 0 };
|
|
}
|
|
|
|
const double secondsPerPoint = _period / (_resolution - 1);
|
|
// How much time has passed since the last permanent point
|
|
const double delta = data.time.j2000Seconds() - _lastPointTime;
|
|
|
|
// We'd like to test for equality with 0 here, but due to rounding issues, we won't
|
|
// get there. If this check is not here, we will trigger the positive or negative
|
|
// branch below even though we don't have to
|
|
//
|
|
// This might become a bigger issue if we are starting to look at very short time
|
|
// intervals
|
|
|
|
if (std::abs(delta) < Epsilon) {
|
|
return { false, false, 0 };
|
|
}
|
|
|
|
if (delta > 0.0) {
|
|
// Check whether we need to drop a new permanent point. This is only the case if
|
|
// enough (> secondsPerPoint) time has passed since the last permanent point
|
|
if (std::abs(delta) < secondsPerPoint) {
|
|
return { true, false, 0 };
|
|
}
|
|
|
|
// See how many points we need to drop
|
|
const int nNewPoints = static_cast<int>(floor(delta / secondsPerPoint));
|
|
|
|
// If we would need to generate more new points than there are total points in the
|
|
// array, it is faster to regenerate the entire array
|
|
if (nNewPoints >= _resolution) {
|
|
fullSweep(data.time.j2000Seconds());
|
|
return { false, true, UpdateReport::All };
|
|
}
|
|
|
|
for (int i = 0; i < nNewPoints; ++i) {
|
|
_lastPointTime += secondsPerPoint;
|
|
|
|
// Get the new permanent point and write it into the (previously) floating
|
|
// location
|
|
const glm::vec3 p = _translation->position({
|
|
{},
|
|
Time(_lastPointTime),
|
|
Time(0.0)
|
|
});
|
|
_vertexArray[_primaryRenderInformation.first] = { p.x, p.y, p.z };
|
|
|
|
// Move the current pointer back one step to be used as the new floating
|
|
// location
|
|
--_primaryRenderInformation.first;
|
|
// And loop around if necessary
|
|
if (_primaryRenderInformation.first < 0) {
|
|
_primaryRenderInformation.first += _primaryRenderInformation.count;
|
|
}
|
|
}
|
|
|
|
// The previously oldest permanent point has been moved nNewPoints steps into the
|
|
// future
|
|
_firstPointTime += nNewPoints * secondsPerPoint;
|
|
|
|
return { false, true, nNewPoints };
|
|
}
|
|
else {
|
|
// See how many new points needs to be generated. Delta is negative, so we need
|
|
// to invert the ratio
|
|
const int nNewPoints = -(static_cast<int>(floor(delta / secondsPerPoint)));
|
|
|
|
// If we would need to generate more new points than there are total points in the
|
|
// array, it is faster to regenerate the entire array
|
|
if (nNewPoints >= _resolution) {
|
|
fullSweep(data.time.j2000Seconds());
|
|
return { false, true, UpdateReport::All };
|
|
}
|
|
|
|
for (int i = 0; i < nNewPoints; ++i) {
|
|
_firstPointTime -= secondsPerPoint;
|
|
|
|
// Get the new permanent point and write it into the (previously) floating
|
|
// location
|
|
const glm::vec3 p = _translation->position({
|
|
{},
|
|
Time(_firstPointTime),
|
|
Time(0.0)
|
|
});
|
|
_vertexArray[_primaryRenderInformation.first] = { p.x, p.y, p.z };
|
|
|
|
// if we are on the upper bounds of the array, we start at 0
|
|
if (_primaryRenderInformation.first == _primaryRenderInformation.count - 1) {
|
|
// If it is at the beginning, set it to the end first
|
|
_primaryRenderInformation.first = 0;
|
|
}
|
|
else {
|
|
// Move the current pointer fowards one step to be used as the new
|
|
// floating
|
|
++_primaryRenderInformation.first;
|
|
}
|
|
}
|
|
|
|
// The previously youngest point has become nNewPoints steps older
|
|
_lastPointTime -= nNewPoints * secondsPerPoint;
|
|
|
|
return { false, true, -nNewPoints };
|
|
}
|
|
}
|
|
|
|
void RenderableTrailOrbit::fullSweep(double time) {
|
|
// Reserve the space for the vertices
|
|
_vertexArray.clear();
|
|
_vertexArray.resize(_resolution);
|
|
|
|
// The index buffer stays constant until we change the size of the array
|
|
if (_indexBufferDirty) {
|
|
// Create the index buffer and fill it with two ranges for [0, _resolution)
|
|
_indexArray.clear();
|
|
_indexArray.resize(_resolution * 2);
|
|
std::iota(_indexArray.begin(), _indexArray.begin() + _resolution, 0);
|
|
std::iota(_indexArray.begin() + _resolution, _indexArray.end(), 0);
|
|
}
|
|
|
|
_lastPointTime = time;
|
|
|
|
const double secondsPerPoint = _period / (_resolution - 1);
|
|
// starting at 1 because the first position is a floating current one
|
|
for (int i = 1; i < _resolution; ++i) {
|
|
const glm::vec3 p = _translation->position({ {}, Time(time), Time(0.0) });
|
|
_vertexArray[i] = { p.x, p.y, p.z };
|
|
|
|
time -= secondsPerPoint;
|
|
}
|
|
|
|
_primaryRenderInformation.first = 0;
|
|
_primaryRenderInformation.count = _resolution;
|
|
|
|
_firstPointTime = time + secondsPerPoint;
|
|
_needsFullSweep = false;
|
|
}
|
|
|
|
} // namespace openspace
|