/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2021 * * * * 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 #include #include #include #include #include #include // 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." }; } // namespace namespace openspace { documentation::Documentation RenderableTrailOrbit::Documentation() { using namespace documentation; documentation::Documentation doc { "RenderableTrailOrbit", "base_renderable_renderabletrailorbit", { { PeriodInfo.identifier, new DoubleVerifier, Optional::No, PeriodInfo.description }, { ResolutionInfo.identifier, new IntVerifier, Optional::No, ResolutionInfo.description }, { RenderableTypeInfo.identifier, new StringVerifier, Optional::Yes, RenderableTypeInfo.description } } }; // 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) { documentation::testSpecificationAndThrow( Documentation(), dictionary, "RenderableTrailOrbit" ); _translation->onParameterChange([this]() { _needsFullSweep = true; }); // Period is in days using namespace std::chrono; const long long sph = duration_cast(hours(24)).count(); _period = dictionary.value(PeriodInfo.identifier) * sph; _period.onChange([&] { _needsFullSweep = true; _indexBufferDirty = true; }); addProperty(_period); _resolution = static_cast(dictionary.value(ResolutionInfo.identifier)); _resolution.onChange([&] { _needsFullSweep = true; _indexBufferDirty = true; }); addProperty(_resolution); // We store the vertices with (excluding the wrapping) decending temporal order _primaryRenderInformation.sorting = RenderInformation::VertexSorting::NewestFirst; if (dictionary.hasKey(RenderableTypeInfo.identifier)) { std::string renderType = dictionary.value( RenderableTypeInfo.identifier ); if (renderType == "Background") { setRenderBin(Renderable::RenderBin::Background); } else if (renderType == "Opaque") { setRenderBin(Renderable::RenderBin::Opaque); } else if (renderType == "PreDeferredTransparent") { setRenderBin(Renderable::RenderBin::PreDeferredTransparent); } else if (renderType == "PostDeferredTransparent") { setRenderBin(Renderable::RenderBin::PostDeferredTransparent); } else if (renderType == "Overlay") { setRenderBin(Renderable::RenderBin::Overlay); } } 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::max()); glm::vec3 minVertex(std::numeric_limits::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(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(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