mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2025-12-31 16:30:07 -06:00
* Implement new RenderableTrails as abstract base class - Implement RenderableTrailsOrbit and RenderableTrailsTrajectory as concrete instances Remove old RenderableTrails and RenderableTrailsNew classes Adapt mod files to the new structure * Addressed Pull Request comments
456 lines
19 KiB
C++
456 lines
19 KiB
C++
/*****************************************************************************************
|
|
* *
|
|
* OpenSpace *
|
|
* *
|
|
* Copyright (c) 2014-2016 *
|
|
* *
|
|
* 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/verifier.h>
|
|
#include <openspace/scene/translation.h>
|
|
|
|
#include <numeric>
|
|
|
|
// 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 {
|
|
const char* KeyPeriod = "Period";
|
|
const char* KeyResolution = "Resolution";
|
|
}
|
|
|
|
namespace openspace {
|
|
|
|
openspace::Documentation RenderableTrailOrbit::Documentation() {
|
|
using namespace documentation;
|
|
openspace::Documentation doc{
|
|
"RenderableTrailOrbit",
|
|
"base_renderable_renderabletrailorbit",
|
|
{
|
|
{
|
|
"Type",
|
|
new StringEqualVerifier("RenderableTrailOrbit"),
|
|
"",
|
|
Optional::No
|
|
},
|
|
{
|
|
KeyPeriod,
|
|
new DoubleVerifier,
|
|
"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.",
|
|
Optional::No
|
|
},
|
|
{
|
|
KeyResolution,
|
|
new IntVerifier,
|
|
"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.",
|
|
Optional::No
|
|
}
|
|
}
|
|
};
|
|
|
|
// Insert the parents documentation entries until we have a verifier that can deal
|
|
// with class hierarchy
|
|
openspace::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("period", "Period in days", 0.0, 0.0, 1e9)
|
|
, _resolution("resoluion", "Number of Samples along Orbit", 10000, 1, 1e6)
|
|
, _needsFullSweep(true)
|
|
, _indexBufferDirty(true)
|
|
{
|
|
documentation::testSpecificationAndThrow(
|
|
Documentation(),
|
|
dictionary,
|
|
"RenderableTrailOrbit"
|
|
);
|
|
|
|
// Period is in days
|
|
using namespace std::chrono;
|
|
int factor = duration_cast<seconds>(hours(24)).count();
|
|
_period = dictionary.value<double>(KeyPeriod) * factor;
|
|
_period.onChange([&] { _needsFullSweep = true; _indexBufferDirty = true; });
|
|
addProperty(_period);
|
|
|
|
_resolution = static_cast<int>(dictionary.value<double>(KeyResolution));
|
|
_resolution.onChange([&] { _needsFullSweep = true; _indexBufferDirty = true; });
|
|
addProperty(_resolution);
|
|
|
|
// We store the vertices with (excluding the wrapping) decending temporal order
|
|
_primaryRenderInformation.sorting = RenderInformation::VertexSorting::NewestFirst;
|
|
}
|
|
|
|
bool RenderableTrailOrbit::initialize() {
|
|
bool res = RenderableTrail::initialize();
|
|
|
|
glGenVertexArrays(1, &_primaryRenderInformation._vaoID);
|
|
glGenBuffers(1, &_primaryRenderInformation._vBufferID);
|
|
glGenBuffers(1, &_primaryRenderInformation._iBufferID);
|
|
|
|
return res;
|
|
}
|
|
|
|
bool RenderableTrailOrbit::deinitialize() {
|
|
glDeleteVertexArrays(1, &_primaryRenderInformation._vaoID);
|
|
glDeleteBuffers(1, &_primaryRenderInformation._vBufferID);
|
|
glDeleteBuffers(1, &_primaryRenderInformation._iBufferID);
|
|
|
|
return RenderableTrail::deinitialize();
|
|
}
|
|
|
|
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
|
|
|
|
// Early bailout when we don't move in time
|
|
if (data.timePaused || data.delta == 0.0) {
|
|
return;
|
|
}
|
|
|
|
|
|
// 1
|
|
// Update the trails; the report contains whether any of the other values has been
|
|
// touched and if so, how many
|
|
UpdateReport report = updateTrails(data);
|
|
|
|
// 2
|
|
// Write the current location into the floating position
|
|
glm::vec3 p = _translation->position(data.time);
|
|
_vertexArray[_primaryRenderInformation.first] = { p.x, p.y, p.z };
|
|
|
|
glBindVertexArray(_primaryRenderInformation._vaoID);
|
|
glBindBuffer(GL_ARRAY_BUFFER, _primaryRenderInformation._vBufferID);
|
|
|
|
// 3
|
|
if (!report.needsUpdate) {
|
|
// 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
|
|
int i = _primaryRenderInformation.first;
|
|
// Number of values
|
|
int n = report.nUpdated + 1; // +1 for the floating position
|
|
// Total size of the array
|
|
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
|
|
int first = s - i;
|
|
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
|
|
int i = _primaryRenderInformation.first;
|
|
// Number of values
|
|
int n = report.nUpdated + 1; // +1 for the floating position
|
|
// Total size of the array
|
|
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
|
|
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, 0);
|
|
|
|
glBindVertexArray(0);
|
|
}
|
|
|
|
RenderableTrailOrbit::UpdateReport RenderableTrailOrbit::updateTrails(
|
|
const UpdateData& data)
|
|
{
|
|
// If we are doing a time jump, it is in general faster to recalculate everything
|
|
// than to only update parts of the array
|
|
if (data.isTimeJump) {
|
|
_needsFullSweep = true;
|
|
}
|
|
if (_needsFullSweep) {
|
|
fullSweep(data.time);
|
|
return { true, UpdateReport::All } ;
|
|
}
|
|
|
|
// When time stands still (at the iron hill), we don't need to perform any work
|
|
if (data.delta == 0.0) {
|
|
return { false, 0 };
|
|
}
|
|
|
|
double secondsPerPoint = _period / (_resolution - 1);
|
|
// How much time has passed since the last permanent point
|
|
double delta = data.time - _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
|
|
const double Epsilon = 1e-7;
|
|
if (abs(delta) < Epsilon) {
|
|
return { 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 (abs(delta) < secondsPerPoint) {
|
|
return { false, 0 };
|
|
}
|
|
|
|
// See how many points we need to drop
|
|
int nNewPoints = 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);
|
|
return { 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
|
|
glm::vec3 p = _translation->position(_lastPointTime);
|
|
_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 { true, nNewPoints };
|
|
}
|
|
else {
|
|
// See how many new points needs to be generated. Delta is negative, so we need
|
|
// to invert the ratio
|
|
int nNewPoints = -(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);
|
|
return { 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
|
|
glm::vec3 p = _translation->position(_firstPointTime);
|
|
_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 { 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;
|
|
|
|
double secondsPerPoint = _period / (_resolution - 1);
|
|
// starting at 1 because the first position is a floating current one
|
|
for (int i = 1; i < _resolution; ++i) {
|
|
glm::vec3 p = _translation->position(time);
|
|
_vertexArray[i] = { p.x, p.y, p.z };
|
|
|
|
time -= secondsPerPoint;
|
|
}
|
|
|
|
_primaryRenderInformation.first = 0;
|
|
_primaryRenderInformation.count = _resolution;
|
|
|
|
_firstPointTime = time + secondsPerPoint;
|
|
_needsFullSweep = false;
|
|
}
|
|
|
|
} // namespace openspace
|