/***************************************************************************************** * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr const char* _loggerCat = "RenderableTimeVaryingVolume"; const char* KeyStepSize = "StepSize"; const char* KeyGridType = "GridType"; const float SecondsInOneDay = 60 * 60 * 24; constexpr const float VolumeMaxOpacity = 500; static const openspace::properties::Property::PropertyInfo StepSizeInfo = { "stepSize", "Step Size", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo GridTypeInfo = { "gridType", "Grid Type", "", // @TODO Missing documentation openspace::properties::Property::Visibility::Developer }; constexpr openspace::properties::Property::PropertyInfo SecondsBeforeInfo = { "secondsBefore", "Seconds before", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo SecondsAfterInfo = { "secondsAfter", "Seconds after", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo SourceDirectoryInfo = { "sourceDirectory", "Source Directory", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo TransferFunctionInfo = { "transferFunctionPath", "Transfer Function Path", "" }; constexpr openspace::properties::Property::PropertyInfo TriggerTimeJumpInfo = { "triggerTimeJump", "Jump", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo JumpToTimestepInfo = { "jumpToTimestep", "Jump to timestep", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo CurrentTimeStepInfo = { "currentTimestep", "Current timestep", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo rNormalizationInfo = { "rNormalization", "Radius normalization", "" // @TODO Missing documentation }; constexpr openspace::properties::Property::PropertyInfo rUpperBoundInfo = { "rUpperBound", "Radius upper bound", "" // @TODO Missing documentation }; struct [[codegen::Dictionary(RenderableTimeVaryingVolume)]] Parameters { // Specifies the path to load timesteps from std::string sourceDirectory; // Specifies the transfer function file path std::string transferFunction; // Specifies the number of seconds to show the the first timestep before its // actual time. The default value is 0 std::optional secondsBefore; // Specifies the number of seconds to show the the last timestep after its // actual time float secondsAfter; std::optional clipPlanes; }; #include "renderabletimevaryingvolume_codegen.cpp" } // namespace namespace openspace::volume { documentation::Documentation RenderableTimeVaryingVolume::Documentation() { return codegen::doc("volume_renderable_timevaryingvolume"); } RenderableTimeVaryingVolume::RenderableTimeVaryingVolume( const ghoul::Dictionary& dictionary) : Renderable(dictionary) , _gridType(GridTypeInfo, properties::OptionProperty::DisplayType::Dropdown) , _stepSize(StepSizeInfo, 0.02f, 0.001f, 0.1f) , _rNormalization(rNormalizationInfo, 0.f, 0.f, 2.f) , _rUpperBound(rUpperBoundInfo, 1.f, 0.f, 2.f) , _secondsBefore(SecondsBeforeInfo, 0.f, 0.01f, SecondsInOneDay) , _secondsAfter(SecondsAfterInfo, 0.f, 0.01f, SecondsInOneDay) , _sourceDirectory(SourceDirectoryInfo) , _transferFunctionPath(TransferFunctionInfo) , _triggerTimeJump(TriggerTimeJumpInfo) , _jumpToTimestep(JumpToTimestepInfo, 0, 0, 256) , _currentTimestep(CurrentTimeStepInfo, 0, 0, 256) { const Parameters p = codegen::bake(dictionary); _sourceDirectory = absPath(p.sourceDirectory).string(); _transferFunctionPath = absPath(p.transferFunction).string(); _transferFunction = std::make_shared( _transferFunctionPath, [](const openspace::TransferFunction&) {} ); _gridType.addOptions({ { static_cast(volume::VolumeGridType::Cartesian), "Cartesian grid" }, { static_cast(volume::VolumeGridType::Spherical), "Spherical grid" } }); _gridType = static_cast(volume::VolumeGridType::Cartesian); if (dictionary.hasValue(KeyStepSize)) { _stepSize = static_cast(dictionary.value(KeyStepSize)); } _secondsBefore = p.secondsBefore.value_or(_secondsBefore); _secondsAfter = p.secondsAfter; ghoul::Dictionary clipPlanesDictionary = p.clipPlanes.value_or(ghoul::Dictionary()); _clipPlanes = std::make_shared(clipPlanesDictionary); _clipPlanes->setIdentifier("clipPlanes"); _clipPlanes->setGuiName("Clip Planes"); if (dictionary.hasValue(KeyGridType)) { VolumeGridType gridType = volume::parseGridType( dictionary.value(KeyGridType) ); _gridType = static_cast>(gridType); } addProperty(_opacity); } RenderableTimeVaryingVolume::~RenderableTimeVaryingVolume() {} void RenderableTimeVaryingVolume::initializeGL() { std::filesystem::path sequenceDir = absPath(_sourceDirectory); if (!std::filesystem::is_directory(sequenceDir)) { LERROR(fmt::format("Could not load sequence directory {}", sequenceDir)); return; } namespace fs = std::filesystem; for (const fs::directory_entry& e : fs::recursive_directory_iterator(sequenceDir)) { if (e.is_regular_file() && e.path().extension() == ".dictionary") { loadTimestepMetadata(e.path().string()); } } // TODO: defer loading of data to later (separate thread or at least not when loading) for (std::pair& p : _volumeTimesteps) { Timestep& t = p.second; std::string path = fmt::format( "{}/{}.rawvolume", _sourceDirectory.value(), t.baseName ); RawVolumeReader reader(path, t.metadata.dimensions); t.rawVolume = reader.read(); float min = t.metadata.minValue; float diff = t.metadata.maxValue - t.metadata.minValue; float* data = t.rawVolume->data(); for (size_t i = 0; i < t.rawVolume->nCells(); ++i) { data[i] = glm::clamp((data[i] - min) / diff, 0.f, 1.f); } t.histogram = std::make_shared(0.f, 1.f, 100); for (size_t i = 0; i < t.rawVolume->nCells(); ++i) { t.histogram->add(data[i]); } // TODO: handle normalization properly for different timesteps + transfer function t.texture = std::make_shared( t.metadata.dimensions, ghoul::opengl::Texture::Format::Red, GL_RED, GL_FLOAT, ghoul::opengl::Texture::FilterMode::Linear, ghoul::opengl::Texture::WrappingMode::Clamp ); t.texture->setPixelData( reinterpret_cast(data), ghoul::opengl::Texture::TakeOwnership::No ); t.texture->uploadTexture(); } _clipPlanes->initialize(); _raycaster = std::make_unique( nullptr, _transferFunction, _clipPlanes ); _raycaster->initialize(); global::raycasterManager->attachRaycaster(*_raycaster.get()); onEnabledChange([&](bool enabled) { if (enabled) { global::raycasterManager->attachRaycaster(*_raycaster.get()); } else { global::raycasterManager->detachRaycaster(*_raycaster.get()); } }); _triggerTimeJump.onChange([this] () { jumpToTimestep(_jumpToTimestep); }); _jumpToTimestep.onChange([this] () { jumpToTimestep(_jumpToTimestep); }); const int lastTimestep = !_volumeTimesteps.empty() ? static_cast(_volumeTimesteps.size() - 1) : 0; _currentTimestep.setMaxValue(lastTimestep); _jumpToTimestep.setMaxValue(lastTimestep); addProperty(_stepSize); addProperty(_transferFunctionPath); addProperty(_sourceDirectory); addPropertySubOwner(_clipPlanes.get()); addProperty(_triggerTimeJump); addProperty(_jumpToTimestep); addProperty(_currentTimestep); addProperty(_rNormalization); addProperty(_rUpperBound); addProperty(_gridType); _raycaster->setGridType(static_cast(_gridType.value())); _gridType.onChange([this] { _raycaster->setGridType(static_cast(_gridType.value())); }); _transferFunctionPath.onChange([this] { _transferFunction = std::make_shared( _transferFunctionPath ); _raycaster->setTransferFunction(_transferFunction); }); } void RenderableTimeVaryingVolume::loadTimestepMetadata(const std::string& path) { RawVolumeMetadata metadata; try { ghoul::Dictionary dictionary = ghoul::lua::loadDictionaryFromFile(path); metadata = RawVolumeMetadata::createFromDictionary(dictionary); } catch (...) { return; } Timestep t; t.metadata = metadata; t.baseName = std::filesystem::path(path).stem().string(); t.inRam = false; t.onGpu = false; _volumeTimesteps[t.metadata.time] = std::move(t); } RenderableTimeVaryingVolume::Timestep* RenderableTimeVaryingVolume::currentTimestep() { if (_volumeTimesteps.empty()) { return nullptr; } double currentTime = global::timeManager->time().j2000Seconds(); // Get the first item with time > currentTime auto currentTimestepIt = _volumeTimesteps.upper_bound(currentTime); if (currentTimestepIt == _volumeTimesteps.end()) { // No such timestep was found: show last timestep if it is within the time margin. Timestep* lastTimestep = &(_volumeTimesteps.rbegin()->second); double threshold = lastTimestep->metadata.time + static_cast(_secondsAfter); return currentTime < threshold ? lastTimestep : nullptr; } if (currentTimestepIt == _volumeTimesteps.begin()) { // No such timestep was found: show first timestep if it is within the time margin Timestep* firstTimestep = &(_volumeTimesteps.begin()->second); double threshold = firstTimestep->metadata.time - static_cast(_secondsBefore); return currentTime >= threshold ? firstTimestep : nullptr; } // Get the last item with time <= currentTime currentTimestepIt--; return &(currentTimestepIt->second); } int RenderableTimeVaryingVolume::timestepIndex( const RenderableTimeVaryingVolume::Timestep* t) const { if (!t) { return -1; } int index = 0; for (const std::pair& it : _volumeTimesteps) { if (&(it.second) == t) { return index; } ++index; } return -1; } // @TODO Can this be turned into a const ref? RenderableTimeVaryingVolume::Timestep* RenderableTimeVaryingVolume::timestepFromIndex( int target) { if (target < 0) { target = 0; } int index = 0; for (std::pair& it : _volumeTimesteps) { if (index == target) { return &(it.second); } ++index; } return nullptr; } void RenderableTimeVaryingVolume::jumpToTimestep(int target) { Timestep* t = timestepFromIndex(target); if (t) { global::timeManager->setTimeNextFrame(Time(t->metadata.time)); } } void RenderableTimeVaryingVolume::update(const UpdateData&) { _transferFunction->update(); if (_raycaster) { Timestep* t = currentTimestep(); _currentTimestep = timestepIndex(t); // Set scale and translation matrices: // The original data cube is a unit cube centered in 0 // ie with lower bound from (-0.5, -0.5, -0.5) and upper bound (0.5, 0.5, 0.5) if (t && t->texture) { if (_raycaster->gridType() == volume::VolumeGridType::Cartesian) { glm::dvec3 scale = t->metadata.upperDomainBound - t->metadata.lowerDomainBound; glm::dvec3 translation = (t->metadata.lowerDomainBound + t->metadata.upperDomainBound) * 0.5f; glm::dmat4 modelTransform = glm::translate(glm::dmat4(1.0), translation); glm::dmat4 scaleMatrix = glm::scale(glm::dmat4(1.0), scale); modelTransform = modelTransform * scaleMatrix; _raycaster->setModelTransform(glm::mat4(modelTransform)); } else { // The diameter is two times the maximum radius. // No translation: the sphere is always centered in (0, 0, 0) _raycaster->setModelTransform( glm::scale( glm::dmat4(1.0), glm::dvec3(2.0 * t->metadata.upperDomainBound[0]) ) ); } _raycaster->setVolumeTexture(t->texture); } else { _raycaster->setVolumeTexture(nullptr); } _raycaster->setStepSize(_stepSize); _raycaster->setOpacity(_opacity * VolumeMaxOpacity); _raycaster->setRNormalization(_rNormalization); _raycaster->setRUpperBound(_rUpperBound); } } void RenderableTimeVaryingVolume::render(const RenderData& data, RendererTasks& tasks) { if (_raycaster && _raycaster->volumeTexture()) { tasks.raycasterTasks.push_back({ _raycaster.get(), data }); } } bool RenderableTimeVaryingVolume::isReady() const { return true; } void RenderableTimeVaryingVolume::deinitializeGL() { if (_raycaster) { global::raycasterManager->detachRaycaster(*_raycaster.get()); _raycaster = nullptr; } } } // namespace openspace::volume