/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2023 * * * * 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 #include #include #include #include #include #include namespace { constexpr std::string_view KeyLazyLoading = "LazyLoading"; constexpr std::string_view _loggerCat = "RenderablePlaneTimeVaryingImage"; constexpr openspace::properties::Property::PropertyInfo SourceFolderInfo = { "SourceFolder", "Source Folder", "This value specifies the image directory that is loaded from disk and " "is used as a texture that is applied to this plane", openspace::properties::Property::Visibility::AdvancedUser }; constexpr openspace::properties::Property::PropertyInfo RenderTypeInfo = { "RenderType", "Render Type", "This value specifies if the plane should be rendered in the Background, " "Opaque, Transparent, or Overlay rendering step", // @VISIBILITY(3.67) openspace::properties::Property::Visibility::AdvancedUser }; struct [[codegen::Dictionary(RenderablePlaneTimeVaryingImage)]] Parameters { // [[codegen::verbatim(SourceFolderInfo.description)]] std::string sourceFolder; enum class [[codegen::map(openspace::Renderable::RenderBin)]] RenderType { Background, Opaque, PreDeferredTransparent [[codegen::key("PreDeferredTransparency")]], PostDeferredTransparent [[codegen::key("PostDeferredTransparency")]], Overlay }; // [[codegen::verbatim(RenderTypeInfo.description)]] std::optional renderType; }; #include "renderableplanetimevaryingimage_codegen.cpp" } // namespace namespace openspace { documentation::Documentation RenderablePlaneTimeVaryingImage::Documentation() { return codegen::doc( "base_renderable_plane_time_varying_image", RenderablePlane::Documentation() ); } RenderablePlaneTimeVaryingImage::RenderablePlaneTimeVaryingImage( const ghoul::Dictionary& dictionary) : RenderablePlane(dictionary) , _sourceFolder(SourceFolderInfo) { const Parameters p = codegen::bake(dictionary); addProperty(_blendMode); _sourceFolder = p.sourceFolder; if (!std::filesystem::is_directory(absPath(_sourceFolder))) { LERROR(fmt::format( "Time varying image, {} is not a valid directory", _sourceFolder )); } addProperty(_sourceFolder); _sourceFolder.onChange([this]() { _texture = loadTexture(); }); if (p.renderType.has_value()) { setRenderBin(codegen::map(*p.renderType)); } else { setRenderBin(Renderable::RenderBin::Opaque); } if (dictionary.hasKey(KeyLazyLoading)) { _isLoadingLazily = dictionary.value(KeyLazyLoading); if (_isLoadingLazily) { _enabled.onChange([this]() { if (_enabled) { _textureIsDirty = true; } else { BaseModule::TextureManager.release(_texture); _texture = nullptr; } }); } } } void RenderablePlaneTimeVaryingImage::initialize() { RenderablePlane::initialize(); bool success = extractMandatoryInfoFromDictionary(); if (!success) { return; } extractTriggerTimesFromFileNames(); computeSequenceEndTime(); } void RenderablePlaneTimeVaryingImage::initializeGL() { RenderablePlane::initializeGL(); _textureFiles.resize(_sourceFiles.size()); for (size_t i = 0; i < _sourceFiles.size(); ++i) { _textureFiles[i] = ghoul::io::TextureReader::ref().loadTexture( absPath(_sourceFiles[i]).string(), 2 ); _textureFiles[i]->setInternalFormat(GL_COMPRESSED_RGBA); _textureFiles[i]->uploadTexture(); _textureFiles[i]->setFilter(ghoul::opengl::Texture::FilterMode::Linear); _textureFiles[i]->purgeFromRAM(); } if (!_isLoadingLazily) { _texture = loadTexture(); } } bool RenderablePlaneTimeVaryingImage::extractMandatoryInfoFromDictionary() { // Ensure that the source folder exists and then extract // the files with the same extension as namespace fs = std::filesystem; fs::path sourceFolder = absPath(_sourceFolder); // Extract all file paths from the provided folder _sourceFiles.clear(); namespace fs = std::filesystem; for (const fs::directory_entry& e : fs::directory_iterator(sourceFolder)) { if (e.is_regular_file()) { _sourceFiles.push_back(e.path().string()); } } std::sort(_sourceFiles.begin(), _sourceFiles.end()); // Ensure that there are available and valid source files left if (_sourceFiles.empty()) { LERROR(fmt::format( "{}: Plane sequence filepath {} was empty", _identifier, _sourceFolder )); return false; } return true; } void RenderablePlaneTimeVaryingImage::deinitializeGL() { BaseModule::TextureManager.release(_texture); _textureFiles.clear(); RenderablePlane::deinitializeGL(); } void RenderablePlaneTimeVaryingImage::bindTexture() { if (_texture && !_textureIsDirty) { _texture->bind(); } } void RenderablePlaneTimeVaryingImage::update(const UpdateData& data) { ZoneScoped; RenderablePlane::update(data); if (!_enabled || _startTimes.empty()) { return; } bool needsUpdate = false; const double currentTime = data.time.j2000Seconds(); bool isInInterval = (currentTime >= _startTimes[0]) && (currentTime < _sequenceEndTime); if (isInInterval) { const size_t nextIdx = _activeTriggerTimeIndex + 1; if ( // true => we were not in an interval the previous frame but now we are _activeTriggerTimeIndex == -1 || // true => We stepped back to a time represented by another state currentTime < _startTimes[_activeTriggerTimeIndex] || // true => We stepped forward to a time represented by another state (nextIdx < _sourceFiles.size() && currentTime >= _startTimes[nextIdx])) { _activeTriggerTimeIndex = updateActiveTriggerTimeIndex(currentTime); needsUpdate = true; } // else we're still in same state as previous frame (no changes needed) } else { // not in interval => set everything to false _activeTriggerTimeIndex = -1; needsUpdate = false; } if (needsUpdate || _textureIsDirty) { _texture = loadTexture(); _textureIsDirty = false; } } void RenderablePlaneTimeVaryingImage::render(const RenderData& data, RendererTasks& t) { if (!_startTimes.empty() && data.time.j2000Seconds() < _sequenceEndTime && data.time.j2000Seconds() > _startTimes[0]) { glDisable(GL_CULL_FACE); RenderablePlane::render(data, t); } } // Requires time to be formated as such: 'YYYY-MM-DDTHH-MM-SS-XXX' void RenderablePlaneTimeVaryingImage::extractTriggerTimesFromFileNames() { for (const std::string& filePath : _sourceFiles) { // Extract the filename from the path (without extension) std::string timeString = std::filesystem::path(filePath).stem().string(); // Ensure the separators are correct timeString.replace(4, 1, "-"); timeString.replace(7, 1, "-"); timeString.replace(13, 1, ":"); timeString.replace(16, 1, ":"); timeString.replace(19, 1, "."); const double triggerTime = Time::convertTime(timeString); _startTimes.push_back(triggerTime); } } int RenderablePlaneTimeVaryingImage::updateActiveTriggerTimeIndex( double currentTime) const { int activeIndex = 0; auto iter = std::upper_bound(_startTimes.begin(), _startTimes.end(), currentTime); if (iter != _startTimes.end()) { if (iter != _startTimes.begin()) { std::ptrdiff_t idx = std::distance(_startTimes.begin(), iter); activeIndex = static_cast(idx) - 1; } else { activeIndex = 0; } } else { activeIndex = static_cast(_sourceFiles.size() - 1); } return activeIndex; } void RenderablePlaneTimeVaryingImage::computeSequenceEndTime() { if (_sourceFiles.size() > 1) { const double lastTriggerTime = _startTimes[_sourceFiles.size() - 1]; const double sequenceDuration = lastTriggerTime - _startTimes[0]; const double averageStateDuration = sequenceDuration / static_cast(_sourceFiles.size() - 1); _sequenceEndTime = lastTriggerTime + averageStateDuration; } } ghoul::opengl::Texture* RenderablePlaneTimeVaryingImage::loadTexture() const { ghoul::opengl::Texture* texture = nullptr; if (_activeTriggerTimeIndex != -1) { texture = _textureFiles[_activeTriggerTimeIndex].get(); } return texture; } } // namespace openspace