diff --git a/CMakeLists.txt b/CMakeLists.txt index 68945f167a..a979b6c375 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,9 +27,9 @@ cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(OpenSpace) set(OPENSPACE_VERSION_MAJOR 0) -set(OPENSPACE_VERSION_MINOR 17) -set(OPENSPACE_VERSION_PATCH 1) -set(OPENSPACE_VERSION_STRING "Beta-10") +set(OPENSPACE_VERSION_MINOR 18) +set(OPENSPACE_VERSION_PATCH -1) +set(OPENSPACE_VERSION_STRING "Beta-11") set(OPENSPACE_BASE_DIR "${PROJECT_SOURCE_DIR}") set(OPENSPACE_CMAKE_EXT_DIR "${OPENSPACE_BASE_DIR}/support/cmake") diff --git a/Jenkinsfile b/Jenkinsfile index b4b1775164..21597e0e01 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -29,42 +29,6 @@ def moduleCMakeFlags() { modules = bat(returnStdout: true, script: '@dir modules /b /ad /on').trim().split('\r\n'); } - // def dirs = readDir(); - // def currentDir = new File('.') - // def dirs = [] - // currentDir.eachFile FileType.DIRECTORIES, { - // dirs << it.name - // } - // def moduleFlags = [ - // 'atmosphere', - // 'base', - // // 'cefwebgui', - // 'debugging', - // 'digitaluniverse', - // 'fieldlines', - // 'fieldlinessequence', - // 'fitsfilereader', - // 'gaia', - // 'galaxy', - // 'globebrowsing', - // 'imgui', - // 'iswa', - // 'kameleon', - // 'kameleonvolume', - // 'multiresvolume', - // 'server', - // 'space', - // 'spacecraftinstruments', - // 'space', - // 'spout', - // 'sync', - // 'touch', - // 'toyvolume', - // 'volume', - // // 'webbrowser', - // // 'webgui' - // ]; - def flags = ''; for (module in modules) { flags += "-DOPENSPACE_MODULE_${module.toUpperCase()}=ON " @@ -72,8 +36,6 @@ def moduleCMakeFlags() { return flags; } -// echo flags - // // Pipeline start // @@ -114,8 +76,10 @@ linux_gcc_make: { compileHelper.recordCompileIssues(compileHelper.Gcc()); } stage('linux-gcc-make/test') { + testHelper.runUnitTests('bin/codegentest'); + testHelper.runUnitTests('bin/SGCTTest'); + testHelper.runUnitTests('bin/GhoulTest'); testHelper.runUnitTests('bin/OpenSpaceTest'); - testHelper.runUnitTests('bin/codegentest') } cleanWs() } // node('linux') @@ -135,8 +99,10 @@ linux_gcc_ninja: { compileHelper.build(compileHelper.Ninja(), compileHelper.Gcc(), cmakeCompileOptions, '', 'build-ninja'); } stage('linux-gcc-ninja/test') { + testHelper.runUnitTests('bin/codegentest'); + testHelper.runUnitTests('bin/SGCTTest'); + testHelper.runUnitTests('bin/GhoulTest'); testHelper.runUnitTests('bin/OpenSpaceTest'); - testHelper.runUnitTests('bin/codegentest') } cleanWs() } // node('linux') @@ -157,8 +123,10 @@ linux_clang_make: { compileHelper.recordCompileIssues(compileHelper.Clang()); } stage('linux-clang-make/test') { + testHelper.runUnitTests('bin/codegentest'); + testHelper.runUnitTests('bin/SGCTTest'); + testHelper.runUnitTests('bin/GhoulTest'); testHelper.runUnitTests('bin/OpenSpaceTest'); - testHelper.runUnitTests('bin/codegentest') } cleanWs() } // node('linux') @@ -178,8 +146,10 @@ linux_clang_ninja: { compileHelper.build(compileHelper.Ninja(), compileHelper.Clang(), cmakeCompileOptions, '', 'build-ninja'); } stage('linux-clang-ninja/test') { + testHelper.runUnitTests('bin/codegentest'); + testHelper.runUnitTests('bin/SGCTTest'); + testHelper.runUnitTests('bin/GhoulTest'); testHelper.runUnitTests('bin/OpenSpaceTest'); - testHelper.runUnitTests('bin/codegentest') } cleanWs() } // node('linux') @@ -197,8 +167,10 @@ windows_msvc: { compileHelper.recordCompileIssues(compileHelper.VisualStudio()); } stage('windows-msvc/test') { - testHelper.runUnitTests('bin\\Debug\\OpenSpaceTest') - testHelper.runUnitTests('bin\\Debug\\codegentest') + testHelper.runUnitTests('bin\\Debug\\codegentest'); + testHelper.runUnitTests('bin\\Debug\\SGCTTest'); + testHelper.runUnitTests('bin\\Debug\\GhoulTest'); + testHelper.runUnitTests('bin\\Debug\\OpenSpaceTest'); } cleanWs() } // node('windows') diff --git a/apps/OpenSpace/CMakeLists.txt b/apps/OpenSpace/CMakeLists.txt index d91dd2a541..7b9a579a47 100644 --- a/apps/OpenSpace/CMakeLists.txt +++ b/apps/OpenSpace/CMakeLists.txt @@ -123,6 +123,7 @@ set_folder_location(quat "External") set_folder_location(tinyxml2static "External") set_folder_location(vrpn "External") set_folder_location(zlibstatic "External") +set_folder_location(SGCTTest "Unit Tests") if (UNIX AND (NOT APPLE)) target_link_libraries(OpenSpace PRIVATE Xcursor Xinerama X11) diff --git a/apps/OpenSpace/ext/sgct b/apps/OpenSpace/ext/sgct index 9289476ad3..3befe6c7a0 160000 --- a/apps/OpenSpace/ext/sgct +++ b/apps/OpenSpace/ext/sgct @@ -1 +1 @@ -Subproject commit 9289476ad3280f3a3e12ef3c26214b57ade2b436 +Subproject commit 3befe6c7a083bf11a71eab8e87101afce8049997 diff --git a/apps/OpenSpace/main.cpp b/apps/OpenSpace/main.cpp index a56d0640fc..2b0f28996e 100644 --- a/apps/OpenSpace/main.cpp +++ b/apps/OpenSpace/main.cpp @@ -1138,15 +1138,21 @@ int main(int argc, char* argv[]) { std::filesystem::path base = configurationFilePath.parent_path(); FileSys.registerPathToken("${BASE}", base); - // For now, we just initialize glfw since we can't execute anything meaningfully - // after SGCT initializes glfw. There is a bit of startup delay because of this, - // but it shouldn't be too bad - glfwInit(); - GLFWmonitor* m = glfwGetPrimaryMonitor(); - const GLFWvidmode* mode = glfwGetVideoMode(m); - glm::ivec2 size = glm::ivec2(mode->width, mode->height); - glfwTerminate(); - + // The previous incarnation of this was initializing GLFW to get the primary + // monitor's resolution, but that had some massive performance implications as + // there was some issue with the swap buffer handling inside of GLFW. My + // assumption is that GLFW doesn't like being initialized, destroyed, and then + // initialized again. Therefore we are using the platform specific functions now + glm::ivec2 size = glm::ivec2(1920, 1080); +#ifdef WIN32 + DEVMODEW dm = { 0 }; + dm.dmSize = sizeof(DEVMODEW); + BOOL success = EnumDisplaySettingsW(nullptr, ENUM_CURRENT_SETTINGS, &dm); + if (success) { + size.x = dm.dmPelsWidth; + size.y = dm.dmPelsHeight; + } +#endif // WIN32 // Loading configuration from disk LDEBUG("Loading configuration from disk"); diff --git a/data/assets/scene/solarsystem/planets/earth/default_layers.asset b/data/assets/scene/solarsystem/planets/earth/default_layers.asset index 9ac40e0280..4a92f8c711 100644 --- a/data/assets/scene/solarsystem/planets/earth/default_layers.asset +++ b/data/assets/scene/solarsystem/planets/earth/default_layers.asset @@ -1,4 +1,5 @@ -- Color layers +asset.require("./layers/colorlayers/blue_marble") local colorLayer = asset.require("./layers/colorlayers/esri_viirs_combo") asset.require("./layers/colorlayers/esri_world_imagery") asset.require("./layers/colorlayers/esri_imagery_world_2D") @@ -13,9 +14,11 @@ asset.require("./layers/colorlayers/ghrsst_l4_g1sst_sea_surface_temperature_temp asset.require("./layers/colorlayers/ghrsst_l4_mur_sea_surface_temperature_temporal") -- Height layers +asset.require("./layers/heightlayers/blue_marble_height") local heightLayer = asset.require("./layers/heightlayers/terrain_tileset") -- Night layers +asset.require("./layers/nightlayers/earth_night_texture") local nightLayer = asset.require("./layers/nightlayers/earth_at_night_2012") asset.require("./layers/nightlayers/earth_at_night_temporal") diff --git a/data/assets/scene/solarsystem/planets/earth/layers/colorlayers/fallbacks/blue_marble.asset b/data/assets/scene/solarsystem/planets/earth/layers/colorlayers/blue_marble.asset similarity index 69% rename from data/assets/scene/solarsystem/planets/earth/layers/colorlayers/fallbacks/blue_marble.asset rename to data/assets/scene/solarsystem/planets/earth/layers/colorlayers/blue_marble.asset index e068c4a668..1c592e753d 100644 --- a/data/assets/scene/solarsystem/planets/earth/layers/colorlayers/fallbacks/blue_marble.asset +++ b/data/assets/scene/solarsystem/planets/earth/layers/colorlayers/blue_marble.asset @@ -1,4 +1,4 @@ -local globeIdentifier = asset.require("../../../earth").Earth.Identifier +local globeIdentifier = asset.require("../../earth").Earth.Identifier local texturesPath = asset.syncedResource({ Name = "Earth Textures", @@ -14,6 +14,15 @@ local layer = { Description = [[ Earth image from Blue Marble Next Generation ]], } + +asset.onInitialize(function() + openspace.globebrowsing.addLayer(globeIdentifier, "ColorLayers", layer) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteLayer(globeIdentifier, "ColorLayers", layer) +end) + asset.export("layer", layer) diff --git a/data/assets/scene/solarsystem/planets/earth/layers/colorlayers/esri_viirs_combo.asset b/data/assets/scene/solarsystem/planets/earth/layers/colorlayers/esri_viirs_combo.asset index 2ada31b94e..b318c8b4ae 100644 --- a/data/assets/scene/solarsystem/planets/earth/layers/colorlayers/esri_viirs_combo.asset +++ b/data/assets/scene/solarsystem/planets/earth/layers/colorlayers/esri_viirs_combo.asset @@ -1,7 +1,5 @@ local globeIdentifier = asset.require("../../earth").Earth.Identifier -local fallBackLayer = asset.require("./fallbacks/blue_marble").layer - local layer = { Identifier = "ESRI_VIIRS_Combo", Name = "ESRI VIIRS Combo", @@ -41,7 +39,6 @@ local layer = { }, }, PadTiles = false, - Fallback = fallBackLayer, Description = [[Level based layer combining "VIIRS SNPP (Temporal)" and ESRI World Imagery. "VIIRS SNPP (Temporal)" is faded out at tile level 4]] } diff --git a/data/assets/scene/solarsystem/planets/earth/layers/heightlayers/fallbacks/blue_marble_height.asset b/data/assets/scene/solarsystem/planets/earth/layers/heightlayers/blue_marble_height.asset similarity index 70% rename from data/assets/scene/solarsystem/planets/earth/layers/heightlayers/fallbacks/blue_marble_height.asset rename to data/assets/scene/solarsystem/planets/earth/layers/heightlayers/blue_marble_height.asset index 48c53c1afa..5902c342b2 100644 --- a/data/assets/scene/solarsystem/planets/earth/layers/heightlayers/fallbacks/blue_marble_height.asset +++ b/data/assets/scene/solarsystem/planets/earth/layers/heightlayers/blue_marble_height.asset @@ -1,3 +1,5 @@ +local globeIdentifier = asset.require("../../earth").Earth.Identifier + local texturesPath = asset.syncedResource({ Name = "Earth Textures", Type = "HttpSynchronization", @@ -12,6 +14,14 @@ local layer = { Description = [[ Topographic layer from Blue Marble Next Generation]] } +asset.onInitialize(function() + openspace.globebrowsing.addLayer(globeIdentifier, "HeightLayers", layer) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteLayer(globeIdentifier, "HeightLayers", layer) +end) + asset.export("layer", layer) diff --git a/data/assets/scene/solarsystem/planets/earth/layers/heightlayers/terrain_tileset.asset b/data/assets/scene/solarsystem/planets/earth/layers/heightlayers/terrain_tileset.asset index 3763b8cfb5..1d8c46718b 100644 --- a/data/assets/scene/solarsystem/planets/earth/layers/heightlayers/terrain_tileset.asset +++ b/data/assets/scene/solarsystem/planets/earth/layers/heightlayers/terrain_tileset.asset @@ -1,12 +1,10 @@ local globeIdentifier = asset.require("../../earth").Earth.Identifier -local fallBackLayer = asset.require("./fallbacks/blue_marble_height").layer local layer = { Identifier = "Terrain_tileset", Name = "Terrain tileset", FilePath = asset.localResource("terrain_tileset.wms"), TilePixelSize = 64, - Fallback = fallBackLayer, Description = [[This globe layer presents elevation data at approximately 90m or 1km per pixel resolution for the world. The elevation data includes 90m Shuttle Radar Topography Mission (SRTM) elevation data from NASA and National diff --git a/data/assets/scene/solarsystem/planets/earth/layers/nightlayers/earth_at_night_2012.asset b/data/assets/scene/solarsystem/planets/earth/layers/nightlayers/earth_at_night_2012.asset index 6ec0c09712..36829712b3 100644 --- a/data/assets/scene/solarsystem/planets/earth/layers/nightlayers/earth_at_night_2012.asset +++ b/data/assets/scene/solarsystem/planets/earth/layers/nightlayers/earth_at_night_2012.asset @@ -1,12 +1,9 @@ local globeIdentifier = asset.require("../../earth").Earth.Identifier -local fallBackLayer = asset.require("./fallbacks/earth_night_texture").layer - local layer = { Identifier = "Earth_at_Night_2012", Name = "Earth at Night 2012", FilePath = asset.localResource("earth_at_night_2012.wms"), - Fallback = fallBackLayer, Description = [[ The lights of cities and villages trace the outlines of civilization in this global view. ]], } diff --git a/data/assets/scene/solarsystem/planets/earth/layers/nightlayers/fallbacks/earth_night_texture.asset b/data/assets/scene/solarsystem/planets/earth/layers/nightlayers/earth_night_texture.asset similarity index 69% rename from data/assets/scene/solarsystem/planets/earth/layers/nightlayers/fallbacks/earth_night_texture.asset rename to data/assets/scene/solarsystem/planets/earth/layers/nightlayers/earth_night_texture.asset index e4e20aceb4..898b34473f 100644 --- a/data/assets/scene/solarsystem/planets/earth/layers/nightlayers/fallbacks/earth_night_texture.asset +++ b/data/assets/scene/solarsystem/planets/earth/layers/nightlayers/earth_night_texture.asset @@ -1,3 +1,5 @@ +local globeIdentifier = asset.require("../../earth").Earth.Identifier + local texturesPath = asset.syncedResource({ Name = "Earth Textures", Type = "HttpSynchronization", @@ -12,6 +14,14 @@ local layer = { Description = [[ Earth's city lights are clearly visible from space ]] } +asset.onInitialize(function() + openspace.globebrowsing.addLayer(globeIdentifier, "NightLayers", layer) +end) + +asset.onDeinitialize(function() + openspace.globebrowsing.deleteLayer(globeIdentifier, "NightLayers", layer) +end) + asset.export("layer", layer) diff --git a/data/assets/scene/solarsystem/planets/earth/satellites/misc/iss.asset b/data/assets/scene/solarsystem/planets/earth/satellites/misc/iss.asset index 3840852c8b..647d348b00 100644 --- a/data/assets/scene/solarsystem/planets/earth/satellites/misc/iss.asset +++ b/data/assets/scene/solarsystem/planets/earth/satellites/misc/iss.asset @@ -23,6 +23,7 @@ local initializeAndAddNodes = function() local iss = { Identifier = "ISS", Parent = transforms.EarthInertial.Identifier, + BoundingSphere = 54.5, -- half the width InteractionSphere = 30, Transform = { Translation = { diff --git a/data/assets/scene/solarsystem/planets/mars/default_layers.asset b/data/assets/scene/solarsystem/planets/mars/default_layers.asset index 198fa783b5..3440e29b42 100644 --- a/data/assets/scene/solarsystem/planets/mars/default_layers.asset +++ b/data/assets/scene/solarsystem/planets/mars/default_layers.asset @@ -1,6 +1,7 @@ -- Color layer -local colorLayer = asset.require("./layers/colorlayers/moc_wa_color_utah") -- TODO: fallback -asset.require("./layers/colorlayers/moc_wa_color_sweden") -- TODO: fallback +asset.require("./layers/colorlayers/mars_texture") +local colorLayer = asset.require("./layers/colorlayers/moc_wa_color_utah") +asset.require("./layers/colorlayers/moc_wa_color_sweden") asset.require("./layers/colorlayers/viking_mdim_utah") asset.require("./layers/colorlayers/viking_mdim_sweden") asset.require("./layers/colorlayers/mola_pseudo_color_utah") diff --git a/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/fallbacks/mars_texture.asset b/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/fallbacks/mars_texture.asset deleted file mode 100644 index 5f5e11db94..0000000000 --- a/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/fallbacks/mars_texture.asset +++ /dev/null @@ -1,24 +0,0 @@ -local texturesPath = asset.syncedResource({ - Name = "Mars Textures", - Type = "HttpSynchronization", - Identifier = "mars_textures", - Version = 1 -}) - -local layer = { - Identifier = "Mars_Texture", - Name = "Mars Texture", - FilePath = texturesPath .. "mars.jpg", -} - -asset.export("layer", layer) - - -asset.meta = { - Name = "Mars Texutre", - Version = "1.0", - Description = [[ Default jpg texture for Mars]], - Author = "OpenSpace Team", - URL = "http://openspaceproject.com", - License = "MIT license" -} diff --git a/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/moc_wa_color_sweden.asset b/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/moc_wa_color_sweden.asset index 853531dbec..986d526984 100644 --- a/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/moc_wa_color_sweden.asset +++ b/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/moc_wa_color_sweden.asset @@ -1,7 +1,5 @@ local globeIdentifier = asset.require("../../mars").Mars.Identifier -local fallbackLayer = asset.require("./fallbacks/mars_texture").layer - local layer = { Identifier = "MOC_WA_Color_LiU", Name = "MOC WA Color [Sweden]", @@ -10,7 +8,6 @@ local layer = { Gamma = 1.6, Multiplier = 1.07 }, - Fallback = fallbackLayer, Description = [[This map is an AMNH version of the global mossaic produced by the Mars Global Surveyor Wide Angle Camera. This version has color added and the shadows subdued based on the MOLA DTM. Data Reference: diff --git a/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/moc_wa_color_utah.asset b/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/moc_wa_color_utah.asset index 94e23f2241..69cc100827 100644 --- a/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/moc_wa_color_utah.asset +++ b/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/moc_wa_color_utah.asset @@ -1,7 +1,5 @@ local globeIdentifier = asset.require("../../mars").Mars.Identifier -local fallbackLayer = asset.require("./fallbacks/mars_texture").layer - local layer = { Identifier = "MOC_WA_Color_Utah", Name = "MOC WA Color [Utah]", @@ -10,7 +8,6 @@ local layer = { Gamma = 1.6, Multiplier = 1.07 }, - Fallback = fallbackLayer, Description = [[This map is an AMNH version of the global mossaic produced by the Mars Global Surveyor Wide Angle Camera. This version has color added and the shadows subdued based on the MOLA DTM. Data Reference: diff --git a/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/themis_ir_night_utah.asset b/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/themis_ir_night_utah.asset index 12ce8f479b..3ec6fd56b7 100644 --- a/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/themis_ir_night_utah.asset +++ b/data/assets/scene/solarsystem/planets/mars/layers/colorlayers/themis_ir_night_utah.asset @@ -13,7 +13,7 @@ local layer = { latitude.

Reference: * Edwards, C.S., K. J. Nowicki, P. R. Christensen, J. Hill, N. Gorelick, and K. Murray (2011), Mosaicking of global planetary image datasets: 1. Techniques and data processing for Thermal Emission Imaging System - (THEMIS) multi‐spectral data, J. Geophys. Res., 116, E10008, + (THEMIS) multi-spectral data, J. Geophys. Res., 116, E10008, doi:10.1029/2010JE003755. http://dx.doi.org/10.1029/2010JE003755 (Description from URL).]], } diff --git a/data/assets/scene/solarsystem/planets/mars/layers/heightlayers/MDEM200M.asset b/data/assets/scene/solarsystem/planets/mars/layers/heightlayers/MDEM200M.asset index 07249e7d89..72809965f1 100644 --- a/data/assets/scene/solarsystem/planets/mars/layers/heightlayers/MDEM200M.asset +++ b/data/assets/scene/solarsystem/planets/mars/layers/heightlayers/MDEM200M.asset @@ -5,13 +5,12 @@ local layer = { Name = "HRSC MOLA Blended DEM Global 200m v2", FilePath = asset.localResource("mdem200m.wms"), Description = [[ Blend of data derived from the Mars Orbiter Laser Altimeter - (MOLA, an instrument aboard NASA’s - Mars Global Surveyor spacecraft), and data derived from the High-Resolution - Stereo Camera (HRSC, an instrument aboard the European Space Agency’s Mars - Express spacecraft). The average accuracy is ~100 meters in horizontal - position and the elevation uncertainty is at least ±3 m. This - tiled elevation layer, hosted by Esri, is made available using - lossless LERC compression. (Description from URL).]], + (MOLA, an instrument aboard NASA's Mars Global Surveyor spacecraft), and data derived + from the High-Resolution Stereo Camera (HRSC, an instrument aboard the European Space + Agency's Mars Express spacecraft). The average accuracy is ~100 meters in horizontal + position and the elevation uncertainty is at least ±3 m. This tiled elevation layer, + hosted by ESRI, is made available using lossless LERC compression. + (Description from URL).]], } asset.onInitialize(function() diff --git a/data/assets/scene/solarsystem/planets/mars/layers/heightlayers/hirisels.asset b/data/assets/scene/solarsystem/planets/mars/layers/heightlayers/hirisels.asset index 98dba0edb9..c771a9e8de 100644 --- a/data/assets/scene/solarsystem/planets/mars/layers/heightlayers/hirisels.asset +++ b/data/assets/scene/solarsystem/planets/mars/layers/heightlayers/hirisels.asset @@ -7,11 +7,11 @@ local layer = { Description = [[ HiRISE (High Resolution Imaging Science Experiment) is the most powerful camera ever sent to another planet, one of six instruments onboard the Mars Reconnaissance Orbiter. We launched in 2005, arrived at Mars in 2006 and have - been imaging ever since. Our camera’s high resolution capability (imaging up to 30 + been imaging ever since. Our camera's high resolution capability (imaging up to 30 centimeters per pixel) remains unprecedented for any existing orbiter in the study of the Red Planet, as well as being an indispensable instrument for helping to select landing sites for robotic and future human exploration. In the past decade, - we’ve also imaged avalanches in progress, and discovered dark flows that may or + we've also imaged avalanches in progress, and discovered dark flows that may or may not be briny seeps. Hundreds of science papers have been published with our data. (Description from URL). This map contains a subet set of the HiRISE DEM where available as of 2018.]], diff --git a/include/openspace/engine/openspaceengine.h b/include/openspace/engine/openspaceengine.h index 8596dc133c..505eececc6 100644 --- a/include/openspace/engine/openspaceengine.h +++ b/include/openspace/engine/openspaceengine.h @@ -107,6 +107,12 @@ public: bool setMode(Mode newMode); void resetMode(); + using CallbackHandle = int; + using ModeChangeCallback = std::function; + + CallbackHandle addModeChangeCallback(ModeChangeCallback cb); + void removeModeChangeCallback(CallbackHandle handle); + // Guaranteed to return a valid pointer AssetManager& assetManager(); LoadingScreen* loadingScreen(); @@ -151,6 +157,10 @@ private: bool _isRenderingFirstFrame = true; Mode _currentMode = Mode::UserControl; + Mode _modeLastFrame = Mode::UserControl; + + int _nextCallbackHandle = 0; + std::vector> _modeChangeCallbacks; }; /** diff --git a/include/openspace/interaction/sessionrecording.h b/include/openspace/interaction/sessionrecording.h index 706377aa99..a24b13f1e0 100644 --- a/include/openspace/interaction/sessionrecording.h +++ b/include/openspace/interaction/sessionrecording.h @@ -671,7 +671,6 @@ protected: bool isPropertyAllowedForBaseline(const std::string& propString); unsigned int findIndexOfLastCameraKeyframeInTimeline(); bool doesTimelineEntryContainCamera(unsigned int index) const; - std::vector> _stateChangeCallbacks; bool doesStartWithSubstring(const std::string& s, const std::string& matchSubstr); void trimCommandsFromScriptIfFound(std::string& script); void replaceCommandsFromScriptIfFound(std::string& script); @@ -800,6 +799,7 @@ protected: double _cameraFirstInTimeline_timestamp = 0; int _nextCallbackHandle = 0; + std::vector> _stateChangeCallbacks; DataMode _conversionDataMode = DataMode::Binary; int _conversionLineNum = 1; diff --git a/include/openspace/navigation/orbitalnavigator.h b/include/openspace/navigation/orbitalnavigator.h index 3177a6273f..01150b1d4c 100644 --- a/include/openspace/navigation/orbitalnavigator.h +++ b/include/openspace/navigation/orbitalnavigator.h @@ -155,16 +155,6 @@ private: properties::FloatProperty _followAnchorNodeRotationDistance; properties::FloatProperty _minimumAllowedDistance; - struct LinearFlight : public properties::PropertyOwner { - LinearFlight(); - - properties::BoolProperty apply; - properties::FloatProperty destinationDistance; - properties::DoubleProperty destinationFactor; - properties::FloatProperty velocitySensitivity; - }; - LinearFlight _linearFlight; - properties::FloatProperty _mouseSensitivity; properties::FloatProperty _joystickSensitivity; properties::FloatProperty _websocketSensitivity; @@ -291,20 +281,6 @@ private: const glm::dvec3& objectPosition, const glm::dquat& globalCameraRotation, const SurfacePositionHandle& positionHandle) const; - /** - * Moves the camera along a vector, camPosToCenterPosDiff, until it reaches the - * focusLimit. The velocity of the zooming depend on distFromCameraToFocus and the - * final frame where the camera stops moving depends on the distance set in the - * variable focusLimit. The bool determines whether to move/fly towards the focus node - * or away from it. - * - * \return a new position of the camera, closer to the focusLimit than the previous - * position - */ - glm::dvec3 moveCameraAlongVector(const glm::dvec3& camPos, - double distFromCameraToFocus, const glm::dvec3& camPosToCenterPosDiff, - double destination, double deltaTime) const; - /* * Adds rotation to the camera position so that it follows the rotation of the anchor * node defined by the differential anchorNodeRotationDiff. diff --git a/include/openspace/navigation/path.h b/include/openspace/navigation/path.h index 364afcc43c..17aa20c690 100644 --- a/include/openspace/navigation/path.h +++ b/include/openspace/navigation/path.h @@ -92,24 +92,50 @@ public: */ CameraPose interpolatedPose(double distance) const; + /** + * Reset variables used to play back path + */ + void resetPlaybackVariables(); + private: /** * Interpolate between the paths start and end rotation using the approach that * corresponds to the path's curve type. The interpolation parameter \p t is the - * same as for the position interpolation, i.e. the relative traveled in distance + * same as for the position interpolation, i.e. the relative traveled distance * along the path, in [0, 1] + * + * \param t The interpolation parameter, given as the relative traveled distance + along the path, in [0, 1] */ glm::dquat interpolateRotation(double t) const; /** * Compute the interpolated rotation quaternion using an eased SLERP approach + * + * \param t The interpolation variable for the rotatation interpolation. + * Should be the relative traveled distance, in [0, 1] */ glm::dquat easedSlerpRotation(double t) const; + /** + * Compute the interpolated rotation quaternion using a method that is customized + * for linear paths. The camera will first interpoalte to look at the targetted + * node, and keep doing so for most of the path. At the end, when within a certain + * distance from the target, the rotation is interpolated so that the camera ends up + * in the target pose at the end of the path. + * + * \param t The interpolation variable for the rotatation interpolation. + * Should be the relative traveled distance, in [0, 1] + */ + glm::dquat linearPathRotation(double t) const; + /** * Compute the interpolated rotation quaternion using an approach that first * interpolates to look at the start node, and then the end node, before * interpolating to the end rotation + * + * \param t The interpolation variable for the rotatation interpolation. + * Should be the relative traveled distance, in [0, 1] */ glm::dquat lookAtTargetsRotation(double t) const; @@ -117,6 +143,8 @@ private: * Evaluate the current traversal speed along the path, based on the currently * traveled distance. The final speed will be scaled to match the desired duration * for the path (which might have been specified by the user) + * + * \param traveledDistance The current distance traveled along the path, in meters */ double speedAlongPath(double traveledDistance) const; @@ -129,15 +157,23 @@ private: double _speedFactorFromDuration = 1.0; // Playback variables - double _traveledDistance = 0.0; - double _progressedTime = 0.0; // Time since playback started + double _traveledDistance = 0.0; // Meters + double _progressedTime = 0.0; // Time since playback started (seconds) + bool _shouldQuit = false; + CameraPose _prevPose; }; - -// Create a path of the given type based on an instruction given as a dictionary. -// See top of cpp file for documentation on keys and values for the dictionary. -// Returns the created path. -Path createPathFromDictionary(const ghoul::Dictionary& dictionary, Path::Type type); +/** + * Create a path based on an instruction given as a dictionary. (See top of cpp file + * for documentation on keys and values for the dictionary.) + * If /p forceType is specified, that type will primarily be used as the type for the + * created path. Secondly, the type will be read from the dictionary, and lastly it will + * use the default from PathNavigator. + * + * \return the created path + */ +Path createPathFromDictionary(const ghoul::Dictionary& dictionary, + std::optional forceType = std::nullopt); } // namespace openspace::interaction diff --git a/include/openspace/navigation/pathcurve.h b/include/openspace/navigation/pathcurve.h index c85835069e..9f9e99b42c 100644 --- a/include/openspace/navigation/pathcurve.h +++ b/include/openspace/navigation/pathcurve.h @@ -26,6 +26,7 @@ #define __OPENSPACE_CORE___PATHCURVE___H__ #include +#include #include namespace openspace::interaction { @@ -34,21 +35,38 @@ class Waypoint; class PathCurve { public: + struct InsufficientPrecisionError : public ghoul::RuntimeError { + explicit InsufficientPrecisionError(std::string msg); + }; + + struct TooShortPathError : public ghoul::RuntimeError { + explicit TooShortPathError(std::string msg); + }; + virtual ~PathCurve() = 0; + /** + * Return the length of the curve, in meters + */ double length() const; /** * Compute and return the position along the path at the specified relative * distance. The input parameter should be in range [0, 1], where 1 correspond to - * the full length of the path + * the full length of the path. + * + * Can be overridden by subclasses that want more control over the position + * interpolation */ - glm::dvec3 positionAt(double relativeDistance) const; + virtual glm::dvec3 positionAt(double relativeDistance) const; /** * Get the intorlatied position along the spline, based on the given curve parameter * u in range [0, 1]. A curve parameter of 0 returns the start position and 1 the end * position. Note that u does not correspond to the relatively traveled distance. + * + * Can be overridden by subclasses that want more control over the position + * interpolation */ virtual glm::dvec3 interpolate(double u) const; @@ -59,16 +77,16 @@ public: protected: /** - * Precompute information related to the spline parameters, that are + * Precompute information related to the spline parameters that are * needed for arc length reparameterization. Must be called after - * control point creation + * control point creation. */ void initializeParameterData(); /** * Compute curve parameter u that matches the input arc length s. - * Input s is a length value, in the range [0, _totalLength]. The returned curve - * parameter u is in range [0, 1] + * Input s is a length value in meters, in the range [0, _totalLength]. + * The returned curve parameter u is in range [0, 1]. */ double curveParameter(double s) const; @@ -81,7 +99,7 @@ protected: std::vector _curveParameterSteps; // per segment std::vector _lengthSums; // per segment - double _totalLength = 0.0; + double _totalLength = 0.0; // meters struct ParameterPair { double u; // curve parameter @@ -94,6 +112,9 @@ protected: class LinearCurve : public PathCurve { public: LinearCurve(const Waypoint& start, const Waypoint& end); + + glm::dvec3 positionAt(double relativeDistance) const override; + glm::dvec3 interpolate(double u) const override; }; } // namespace openspace::interaction diff --git a/include/openspace/navigation/pathnavigator.h b/include/openspace/navigation/pathnavigator.h index b993d086bc..8e4801ae3f 100644 --- a/include/openspace/navigation/pathnavigator.h +++ b/include/openspace/navigation/pathnavigator.h @@ -58,6 +58,7 @@ public: const SceneGraphNode* anchor() const; const Path* currentPath() const; double speedScale() const; + double arrivalDistanceFactor() const; bool hasCurrentPath() const; bool hasFinished() const; @@ -71,7 +72,10 @@ public: void pausePath(); void continuePath(); + Path::Type defaultPathType() const; double minValidBoundingSphere() const; + double findValidBoundingSphere(const SceneGraphNode* node) const; + const std::vector& relevantNodes(); /** @@ -98,6 +102,7 @@ private: properties::BoolProperty _includeRoll; properties::FloatProperty _speedScale; properties::BoolProperty _applyIdleBehaviorOnFinish; + properties::DoubleProperty _arrivalDistanceFactor; properties::DoubleProperty _minValidBoundingSphere; properties::StringListProperty _relevantNodeTags; diff --git a/include/openspace/navigation/waypoint.h b/include/openspace/navigation/waypoint.h index 2257242a31..39624bfd74 100644 --- a/include/openspace/navigation/waypoint.h +++ b/include/openspace/navigation/waypoint.h @@ -41,8 +41,6 @@ public: Waypoint(const glm::dvec3& pos, const glm::dquat& rot, const std::string& ref); explicit Waypoint(const NavigationState& ns); - static double findValidBoundingSphere(const SceneGraphNode* node); - CameraPose pose() const; glm::dvec3 position() const; glm::dquat rotation() const; diff --git a/include/openspace/util/threadpool.h b/include/openspace/util/threadpool.h index 73a65e5302..6c2287f258 100644 --- a/include/openspace/util/threadpool.h +++ b/include/openspace/util/threadpool.h @@ -56,6 +56,8 @@ public: void enqueue(std::function f); void clearTasks(); + bool hasOutstandingTasks() const; + private: friend class Worker; diff --git a/modules/base/dashboard/dashboarditemdistance.cpp b/modules/base/dashboard/dashboarditemdistance.cpp index 353577cdb5..8f738e1b24 100644 --- a/modules/base/dashboard/dashboarditemdistance.cpp +++ b/modules/base/dashboard/dashboarditemdistance.cpp @@ -147,20 +147,10 @@ namespace { namespace openspace { documentation::Documentation DashboardItemDistance::Documentation() { - documentation::Documentation doc = - codegen::doc("base_dashboarditem_distance"); - - // @TODO cleanup - // Insert the parent's documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = DashboardTextItem::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() + return codegen::doc( + "base_dashboarditem_distance", + DashboardTextItem::Documentation() ); - - return doc; } DashboardItemDistance::DashboardItemDistance(const ghoul::Dictionary& dictionary) diff --git a/modules/base/dashboard/dashboarditemframerate.cpp b/modules/base/dashboard/dashboarditemframerate.cpp index 5a08e4556d..62df3ba45f 100644 --- a/modules/base/dashboard/dashboarditemframerate.cpp +++ b/modules/base/dashboard/dashboarditemframerate.cpp @@ -157,20 +157,10 @@ namespace { namespace openspace { documentation::Documentation DashboardItemFramerate::Documentation() { - documentation::Documentation doc = - codegen::doc("base_dashboarditem_framerate"); - - // @TODO cleanup - // Insert the parent's documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = DashboardTextItem::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() + return codegen::doc( + "base_dashboarditem_framerate", + DashboardTextItem::Documentation() ); - - return doc; } DashboardItemFramerate::DashboardItemFramerate(const ghoul::Dictionary& dictionary) diff --git a/modules/base/dashboard/dashboarditempropertyvalue.cpp b/modules/base/dashboard/dashboarditempropertyvalue.cpp index c630692d96..ac28a1b5c9 100644 --- a/modules/base/dashboard/dashboarditempropertyvalue.cpp +++ b/modules/base/dashboard/dashboarditempropertyvalue.cpp @@ -62,20 +62,10 @@ namespace { namespace openspace { documentation::Documentation DashboardItemPropertyValue::Documentation() { - documentation::Documentation doc = - codegen::doc("base_dashboarditem_propertyvalue"); - - // @TODO cleanup - // Insert the parent's documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = DashboardTextItem::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() + return codegen::doc( + "base_dashboarditem_propertyvalue", + DashboardTextItem::Documentation() ); - - return doc; } DashboardItemPropertyValue::DashboardItemPropertyValue( diff --git a/modules/base/dashboard/dashboarditemsimulationincrement.cpp b/modules/base/dashboard/dashboarditemsimulationincrement.cpp index f55d41d02d..0b6ffcb951 100644 --- a/modules/base/dashboard/dashboarditemsimulationincrement.cpp +++ b/modules/base/dashboard/dashboarditemsimulationincrement.cpp @@ -101,20 +101,10 @@ namespace { namespace openspace { documentation::Documentation DashboardItemSimulationIncrement::Documentation() { - documentation::Documentation doc = - codegen::doc("base_dashboarditem_simulationincrement"); - - // @TODO cleanup - // Insert the parent's documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = DashboardTextItem::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() + return codegen::doc( + "base_dashboarditem_simulationincrement", + DashboardTextItem::Documentation() ); - - return doc; } DashboardItemSimulationIncrement::DashboardItemSimulationIncrement( diff --git a/modules/base/dashboard/dashboarditemtext.cpp b/modules/base/dashboard/dashboarditemtext.cpp index 8bc450aa87..6a8ba5f22f 100644 --- a/modules/base/dashboard/dashboarditemtext.cpp +++ b/modules/base/dashboard/dashboarditemtext.cpp @@ -50,20 +50,10 @@ namespace { namespace openspace { documentation::Documentation DashboardItemText::Documentation() { - documentation::Documentation doc = - codegen::doc("base_dashboarditem_text"); - - // @TODO cleanup - // Insert the parent's documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = DashboardTextItem::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() + return codegen::doc( + "base_dashboarditem_text", + DashboardTextItem::Documentation() ); - - return doc; } DashboardItemText::DashboardItemText(const ghoul::Dictionary& dictionary) diff --git a/modules/base/dashboard/dashboarditemvelocity.cpp b/modules/base/dashboard/dashboarditemvelocity.cpp index ed0f1bcd11..dcea43fdd8 100644 --- a/modules/base/dashboard/dashboarditemvelocity.cpp +++ b/modules/base/dashboard/dashboarditemvelocity.cpp @@ -82,20 +82,10 @@ namespace { namespace openspace { documentation::Documentation DashboardItemVelocity::Documentation() { - documentation::Documentation doc = - codegen::doc("base_dashboarditem_velocity"); - - // @TODO cleanup - // Insert the parent's documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = DashboardTextItem::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() + return codegen::doc( + "base_dashboarditem_velocity", + DashboardTextItem::Documentation() ); - - return doc; } DashboardItemVelocity::DashboardItemVelocity(const ghoul::Dictionary& dictionary) diff --git a/modules/base/rendering/renderableplaneimagelocal.cpp b/modules/base/rendering/renderableplaneimagelocal.cpp index 1cc3d4c937..5269ba77a3 100644 --- a/modules/base/rendering/renderableplaneimagelocal.cpp +++ b/modules/base/rendering/renderableplaneimagelocal.cpp @@ -60,20 +60,10 @@ namespace { namespace openspace { documentation::Documentation RenderablePlaneImageLocal::Documentation() { - documentation::Documentation doc = codegen::doc( - "base_renderable_plane_image_local" + return codegen::doc( + "base_renderable_plane_image_local", + RenderablePlane::Documentation() ); - - // @TODO cleanup - // Insert the parents documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = RenderablePlane::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() - ); - return doc; } RenderablePlaneImageLocal::RenderablePlaneImageLocal(const ghoul::Dictionary& dictionary) diff --git a/modules/base/rendering/renderableplaneimageonline.cpp b/modules/base/rendering/renderableplaneimageonline.cpp index c07753bdb1..bbbe18d4b1 100644 --- a/modules/base/rendering/renderableplaneimageonline.cpp +++ b/modules/base/rendering/renderableplaneimageonline.cpp @@ -52,20 +52,10 @@ namespace { namespace openspace { documentation::Documentation RenderablePlaneImageOnline::Documentation() { - documentation::Documentation doc = codegen::doc( - "base_renderable_plane_image_online" + return codegen::doc( + "base_renderable_plane_image_online", + RenderablePlane::Documentation() ); - - // @TODO cleanup - // Insert the parents documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = RenderablePlane::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() - ); - return doc; } RenderablePlaneImageOnline::RenderablePlaneImageOnline( diff --git a/modules/base/rendering/renderableplanetimevaryingimage.cpp b/modules/base/rendering/renderableplanetimevaryingimage.cpp index c3e3a50993..d957708bce 100644 --- a/modules/base/rendering/renderableplanetimevaryingimage.cpp +++ b/modules/base/rendering/renderableplanetimevaryingimage.cpp @@ -76,20 +76,10 @@ namespace { namespace openspace { documentation::Documentation RenderablePlaneTimeVaryingImage::Documentation() { - documentation::Documentation doc = codegen::doc( - "base_renderable_plane_time_varying_image" + return codegen::doc( + "base_renderable_plane_time_varying_image", + RenderablePlane::Documentation() ); - - // Insert the parents documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = RenderablePlane::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() - ); - - return doc; } RenderablePlaneTimeVaryingImage::RenderablePlaneTimeVaryingImage( diff --git a/modules/base/rendering/renderabletrailorbit.cpp b/modules/base/rendering/renderabletrailorbit.cpp index 1f3a0e6005..129a14041a 100644 --- a/modules/base/rendering/renderabletrailorbit.cpp +++ b/modules/base/rendering/renderabletrailorbit.cpp @@ -136,20 +136,10 @@ namespace { namespace openspace { documentation::Documentation RenderableTrailOrbit::Documentation() { - documentation::Documentation doc = codegen::doc( - "base_renderable_renderabletrailorbit" + return codegen::doc( + "base_renderable_renderabletrailorbit", + RenderableTrail::Documentation() ); - - // 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) diff --git a/modules/base/rendering/renderabletrailtrajectory.cpp b/modules/base/rendering/renderabletrailtrajectory.cpp index 14e4be317a..1f38c38d7f 100644 --- a/modules/base/rendering/renderabletrailtrajectory.cpp +++ b/modules/base/rendering/renderabletrailtrajectory.cpp @@ -105,21 +105,10 @@ namespace { namespace openspace { documentation::Documentation RenderableTrailTrajectory::Documentation() { - documentation::Documentation doc = codegen::doc( - "base_renderable_renderabletrailtrajectory" + return codegen::doc( + "base_renderable_renderabletrailtrajectory", + RenderableTrail::Documentation() ); - - // @TODO cleanup - // 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; } RenderableTrailTrajectory::RenderableTrailTrajectory(const ghoul::Dictionary& dictionary) diff --git a/modules/digitaluniverse/rendering/renderablepoints.cpp b/modules/digitaluniverse/rendering/renderablepoints.cpp index 0ddc693ddf..d94ad66e88 100644 --- a/modules/digitaluniverse/rendering/renderablepoints.cpp +++ b/modules/digitaluniverse/rendering/renderablepoints.cpp @@ -77,7 +77,7 @@ namespace { constexpr openspace::properties::Property::PropertyInfo ColorMapInfo = { "ColorMap", "Color Map File", - "The path to the color map file of the astronomical onject." + "The path to the color map file of the astronomical object." }; struct [[codegen::Dictionary(RenderablePoints)]] Parameters { diff --git a/modules/galaxy/rendering/renderablegalaxy.cpp b/modules/galaxy/rendering/renderablegalaxy.cpp index ead0306bcb..891270155b 100644 --- a/modules/galaxy/rendering/renderablegalaxy.cpp +++ b/modules/galaxy/rendering/renderablegalaxy.cpp @@ -494,6 +494,11 @@ void RenderableGalaxy::deinitializeGL() { _raycaster = nullptr; } + global::renderEngine->removeRenderProgram(_pointsProgram.get()); + _pointsProgram = nullptr; + global::renderEngine->removeRenderProgram(_billboardsProgram.get()); + _billboardsProgram = nullptr; + glDeleteVertexArrays(1, &_pointsVao); glDeleteBuffers(1, &_positionVbo); glDeleteBuffers(1, &_colorVbo); diff --git a/modules/globebrowsing/globebrowsingmodule_lua.inl b/modules/globebrowsing/globebrowsingmodule_lua.inl index 699f448122..5818a34b3a 100644 --- a/modules/globebrowsing/globebrowsingmodule_lua.inl +++ b/modules/globebrowsing/globebrowsingmodule_lua.inl @@ -322,10 +322,21 @@ int flyToGeo(lua_State* L) { altitude ); + const glm::dvec3 currentPosW = global::navigationHandler->camera()->positionVec3(); + const glm::dvec3 currentPosModelCoords = + glm::inverse(globe->modelTransform()) * glm::dvec4(currentPosW, 1.0); + + constexpr const double LengthEpsilon = 10.0; // meters + if (glm::distance(currentPosModelCoords, positionModelCoords) < LengthEpsilon) { + LINFOC("GlobeBrowsing", "flyToGeo: Already at the requested position"); + return 0; + } + ghoul::Dictionary instruction; instruction.setValue("TargetType", std::string("Node")); instruction.setValue("Target", n->identifier()); instruction.setValue("Position", positionModelCoords); + instruction.setValue("PathType", std::string("ZoomOutOverview")); // Handle the two optional arguments: duration and use target's up direction argument // The user can either provide both, or one of them diff --git a/modules/globebrowsing/src/layergroup.cpp b/modules/globebrowsing/src/layergroup.cpp index d788adf9c7..9a3965190d 100644 --- a/modules/globebrowsing/src/layergroup.cpp +++ b/modules/globebrowsing/src/layergroup.cpp @@ -34,7 +34,6 @@ namespace { constexpr const char* _loggerCat = "LayerGroup"; - constexpr const char* KeyFallback = "Fallback"; constexpr openspace::properties::Property::PropertyInfo BlendTileInfo = { "BlendTileLevels", @@ -68,18 +67,6 @@ void LayerGroup::setLayersFromDict(const ghoul::Dictionary& dict) { } catch (const ghoul::RuntimeError& e) { LERRORC(e.component, e.message); - - if (layerDict.hasValue(KeyFallback)) { - LWARNING("Unable to create layer. Initializing fallback layer."); - ghoul::Dictionary fallbackLayerDict = - layerDict.value(KeyFallback); - try { - addLayer(fallbackLayerDict); - } - catch (const ghoul::RuntimeError& except) { - LERRORC(except.component, except.message); - } - } } } } diff --git a/modules/server/CMakeLists.txt b/modules/server/CMakeLists.txt index ba1d6ff74a..ad88275bb3 100644 --- a/modules/server/CMakeLists.txt +++ b/modules/server/CMakeLists.txt @@ -34,6 +34,7 @@ set(HEADER_FILES include/topics/authorizationtopic.h include/topics/bouncetopic.h include/topics/documentationtopic.h + include/topics/enginemodetopic.h include/topics/flightcontrollertopic.h include/topics/getpropertytopic.h include/topics/luascripttopic.h @@ -57,6 +58,7 @@ set(SOURCE_FILES src/topics/authorizationtopic.cpp src/topics/bouncetopic.cpp src/topics/documentationtopic.cpp + src/topics/enginemodetopic.cpp src/topics/flightcontrollertopic.cpp src/topics/getpropertytopic.cpp src/topics/luascripttopic.cpp diff --git a/modules/server/include/topics/enginemodetopic.h b/modules/server/include/topics/enginemodetopic.h new file mode 100644 index 0000000000..fbf4546c63 --- /dev/null +++ b/modules/server/include/topics/enginemodetopic.h @@ -0,0 +1,55 @@ +/***************************************************************************************** + * * + * 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. * + ****************************************************************************************/ + +#ifndef __OPENSPACE_MODULE_SERVER___ENGINE_MODE_TOPIC___H__ +#define __OPENSPACE_MODULE_SERVER___ENGINE_MODE_TOPIC___H__ + +#include + +#include + +namespace openspace { + +class EngineModeTopic : public Topic { +public: + EngineModeTopic(); + virtual ~EngineModeTopic(); + + void handleJson(const nlohmann::json& json) override; + bool isDone() const override; + +private: + static const int UnsetOnChangeHandle = -1; + + // Provides the mode int value in json message + void sendJsonData(); + + OpenSpaceEngine::Mode _lastMode; + int _modeCallbackHandle = UnsetOnChangeHandle; + bool _isDone = false; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_SERVER___ENGINE_MODE_TOPIC___H__ diff --git a/modules/server/src/connection.cpp b/modules/server/src/connection.cpp index ea8b887b5c..90ccac3a0a 100644 --- a/modules/server/src/connection.cpp +++ b/modules/server/src/connection.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ namespace { constexpr const char* DocumentationTopicKey = "documentation"; constexpr const char* GetPropertyTopicKey = "get"; constexpr const char* LuaScriptTopicKey = "luascript"; + constexpr const char* EngineModeTopicKey = "engineMode"; constexpr const char* SessionRecordingTopicKey = "sessionRecording"; constexpr const char* SetPropertyTopicKey = "set"; constexpr const char* ShortcutTopicKey = "shortcuts"; @@ -97,6 +99,7 @@ Connection::Connection(std::unique_ptr s, _topicFactory.registerClass(DocumentationTopicKey); _topicFactory.registerClass(GetPropertyTopicKey); _topicFactory.registerClass(LuaScriptTopicKey); + _topicFactory.registerClass(EngineModeTopicKey); _topicFactory.registerClass(SessionRecordingTopicKey); _topicFactory.registerClass(SetPropertyTopicKey); _topicFactory.registerClass(ShortcutTopicKey); diff --git a/modules/server/src/topics/enginemodetopic.cpp b/modules/server/src/topics/enginemodetopic.cpp new file mode 100644 index 0000000000..436f06c1af --- /dev/null +++ b/modules/server/src/topics/enginemodetopic.cpp @@ -0,0 +1,118 @@ +/***************************************************************************************** + * * + * 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/server/include/topics/enginemodetopic.h" + +#include +#include +#include +#include + +namespace { + constexpr const char _loggerCat[] = "EngineModeTopic"; + + constexpr const char EventKey[] = "event"; + constexpr const char SubscribeEvent[] = "start_subscription"; + constexpr const char UnsubscribeEvent[] = "stop_subscription"; + constexpr const char RefreshEvent[] = "refresh"; + + constexpr const char ModeKey[] = "mode"; + +} // namespace + +using nlohmann::json; + +namespace openspace { + +EngineModeTopic::EngineModeTopic() { + LDEBUG("Starting new EngineMode subscription"); +} + +EngineModeTopic::~EngineModeTopic() { + if (_modeCallbackHandle != UnsetOnChangeHandle) { + global::openSpaceEngine->removeModeChangeCallback(_modeCallbackHandle); + } +} + +bool EngineModeTopic::isDone() const { + return _isDone; +} + +void EngineModeTopic::handleJson(const nlohmann::json& json) { + const std::string event = json.at(EventKey).get(); + if (event != SubscribeEvent && event != UnsubscribeEvent && + event != RefreshEvent) + { + LERROR("Unsupported event"); + _isDone = true; + return; + } + + if (event == UnsubscribeEvent) { + _isDone = true; + return; + } + + sendJsonData(); + + if (event == SubscribeEvent) { + _modeCallbackHandle = global::openSpaceEngine->addModeChangeCallback( + [this]() { + OpenSpaceEngine::Mode currentMode = + global::openSpaceEngine->currentMode(); + if (currentMode != _lastMode) { + sendJsonData(); + _lastMode = currentMode; + } + } + ); + } +} + +void EngineModeTopic::sendJsonData() { + json stateJson; + + OpenSpaceEngine::Mode mode = global::openSpaceEngine->currentMode(); + std::string modeString; + switch (mode) { + case OpenSpaceEngine::Mode::UserControl: + modeString = "user_control"; + break; + case OpenSpaceEngine::Mode::SessionRecordingPlayback: + modeString = "session_recording_playback"; + break; + case OpenSpaceEngine::Mode::CameraPath: + modeString = "camera_path"; + break; + default: + throw ghoul::MissingCaseException(); + } + stateJson[ModeKey] = modeString; + + if (!stateJson.empty()) { + _connection->sendJson(wrappedPayload(stateJson)); + } +} + +} // namespace openspace diff --git a/modules/space/rendering/renderablehabitablezone.cpp b/modules/space/rendering/renderablehabitablezone.cpp index e17643090b..51b3e6fd7f 100644 --- a/modules/space/rendering/renderablehabitablezone.cpp +++ b/modules/space/rendering/renderablehabitablezone.cpp @@ -94,21 +94,10 @@ namespace { namespace openspace { documentation::Documentation RenderableHabitableZone::Documentation() { - documentation::Documentation doc = codegen::doc( - "space_renderablehabitablezone" + return codegen::doc( + "space_renderablehabitablezone", + RenderableDisc::Documentation() ); - - // @TODO cleanup - // Insert the parents documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = RenderableDisc::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() - ); - - return doc; } RenderableHabitableZone::RenderableHabitableZone(const ghoul::Dictionary& dictionary) diff --git a/modules/space/rendering/renderablesatellites.cpp b/modules/space/rendering/renderablesatellites.cpp index d7b14a048b..8428e875c6 100644 --- a/modules/space/rendering/renderablesatellites.cpp +++ b/modules/space/rendering/renderablesatellites.cpp @@ -64,20 +64,10 @@ namespace { namespace openspace { documentation::Documentation RenderableSatellites::Documentation() { - documentation::Documentation doc = codegen::doc( - "space_renderablesatellites" + return codegen::doc( + "space_renderablesatellites", + RenderableOrbitalKepler::Documentation() ); - - // Insert the parents documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = RenderableOrbitalKepler::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() - ); - - return doc; } RenderableSatellites::RenderableSatellites(const ghoul::Dictionary& dictionary) diff --git a/modules/space/rendering/renderablesmallbody.cpp b/modules/space/rendering/renderablesmallbody.cpp index fb3531a9d0..61e0626a6b 100644 --- a/modules/space/rendering/renderablesmallbody.cpp +++ b/modules/space/rendering/renderablesmallbody.cpp @@ -98,19 +98,10 @@ namespace { namespace openspace { documentation::Documentation RenderableSmallBody::Documentation() { - documentation::Documentation doc = codegen::doc( - "space_renderablesmallbody" + return codegen::doc( + "space_renderablesmallbody", + RenderableOrbitalKepler::Documentation() ); - - // Insert the parents documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = RenderableOrbitalKepler::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() - ); - return doc; } RenderableSmallBody::RenderableSmallBody(const ghoul::Dictionary& dictionary) diff --git a/modules/spacecraftinstruments/dashboard/dashboarditeminstruments.cpp b/modules/spacecraftinstruments/dashboard/dashboarditeminstruments.cpp index 5d420fd47b..87f549dd7b 100644 --- a/modules/spacecraftinstruments/dashboard/dashboarditeminstruments.cpp +++ b/modules/spacecraftinstruments/dashboard/dashboarditeminstruments.cpp @@ -85,20 +85,10 @@ namespace { namespace openspace { documentation::Documentation DashboardItemInstruments::Documentation() { - documentation::Documentation doc = - codegen::doc("spacecraftinstruments_dashboarditem_instuments"); - - // @TODO cleanup - // Insert the parent's documentation entries until we have a verifier that can deal - // with class hierarchy - documentation::Documentation parentDoc = DashboardTextItem::Documentation(); - doc.entries.insert( - doc.entries.end(), - parentDoc.entries.begin(), - parentDoc.entries.end() + return codegen::doc( + "spacecraftinstruments_dashboarditem_instuments", + DashboardTextItem::Documentation() ); - - return doc; } DashboardItemInstruments::DashboardItemInstruments(const ghoul::Dictionary& dictionary) diff --git a/scripts/configuration_helper.lua b/scripts/configuration_helper.lua index 6f524c213a..b18e7f158b 100644 --- a/scripts/configuration_helper.lua +++ b/scripts/configuration_helper.lua @@ -53,7 +53,7 @@ function sgct.config.single(arg) end -- shared: Determines whether the contents of the window should be shared using the SPOUT library [example: shared=true] {default: false} -- Expert settings: --- vsync: Whether the rendering speed is locked to the refreshrate [example: vsync=true] {default: false} +-- vsync: Whether the rendering speed is locked to the refreshrate [example: vsync=true] {default: true} -- refreshRate: If vsync is enabled, this is the target framerate [example: refreshRate=30] {default: infinity} -- stereo: Select the stereo rendering mode as supported by SGCT [example: stereo='anaglyph_red_cyan'] {default: 'none'} -- msaa: The multisampling anti-aliasing factor [example: msaa=8] {default: 4} @@ -279,11 +279,9 @@ function generateCluster(arg, viewport) table.insert(result, [[ "debug": ]] .. tostring(arg["sgctdebug"]) .. [[,]]) end - if (arg["settings"]) then - table.insert(result, [[ "settings": {]]) - generateSettings(result, arg["refreshRate"], arg["vsync"]) - table.insert(result, [[ },]]) - end + table.insert(result, [[ "settings": {]]) + generateSettings(result, arg["refreshRate"], arg["vsync"]) + table.insert(result, [[ },]]) if arg["scene"] then table.insert(result, [[ "scene": {]]) diff --git a/src/engine/openspaceengine.cpp b/src/engine/openspaceengine.cpp index 6147cac6d0..6a136ec221 100644 --- a/src/engine/openspaceengine.cpp +++ b/src/engine/openspaceengine.cpp @@ -1180,6 +1180,17 @@ void OpenSpaceEngine::preSynchronization() { setCameraFromProfile(*global::profile); setAdditionalScriptsFromProfile(*global::profile); } + + // Handle callback(s) for change in engine mode + if (_modeLastFrame != _currentMode) { + using K = CallbackHandle; + using V = ModeChangeCallback; + for (const std::pair& it : _modeChangeCallbacks) { + it.second(); + } + } + _modeLastFrame = _currentMode; + LTRACE("OpenSpaceEngine::preSynchronization(end)"); } @@ -1617,6 +1628,31 @@ void OpenSpaceEngine::resetMode() { LDEBUG(fmt::format("Reset engine mode to {}", stringify(_currentMode))); } +OpenSpaceEngine::CallbackHandle OpenSpaceEngine::addModeChangeCallback( + ModeChangeCallback cb) +{ + CallbackHandle handle = _nextCallbackHandle++; + _modeChangeCallbacks.emplace_back(handle, std::move(cb)); + return handle; +} + +void OpenSpaceEngine::removeModeChangeCallback(CallbackHandle handle) { + const auto it = std::find_if( + _modeChangeCallbacks.begin(), + _modeChangeCallbacks.end(), + [handle](const std::pair& cb) { + return cb.first == handle; + } + ); + + ghoul_assert( + it != _modeChangeCallbacks.end(), + "handle must be a valid callback handle" + ); + + _modeChangeCallbacks.erase(it); +} + void setCameraFromProfile(const Profile& p) { if (!p.camera.has_value()) { throw ghoul::RuntimeError("No 'camera' entry exists in the startup profile"); diff --git a/src/navigation/orbitalnavigator.cpp b/src/navigation/orbitalnavigator.cpp index 29f36c947b..16a1f06bda 100644 --- a/src/navigation/orbitalnavigator.cpp +++ b/src/navigation/orbitalnavigator.cpp @@ -158,34 +158,6 @@ namespace { "meters above the surface." }; - constexpr openspace::properties::Property::PropertyInfo VelocityZoomControlInfo = { - "VelocityZoomControl", - "Velocity Zoom Control", - "Controls the velocity of the camera motion when zooming in to the focus node " - "on a linear flight. The higher the value the faster the camera will move " - "towards the focus." - }; - - constexpr openspace::properties::Property::PropertyInfo ApplyLinearFlightInfo = { - "ApplyLinearFlight", - "Apply Linear Flight", - "This property makes the camera move to the specified distance " - "'DestinationDistance' while facing the anchor" - }; - - constexpr openspace::properties::Property::PropertyInfo FlightDestinationDistInfo = { - "FlightDestinationDistance", - "Flight Destination Distance", - "The final distance we want to fly to, with regards to the anchor node." - }; - - constexpr openspace::properties::Property::PropertyInfo FlightDestinationFactorInfo = - { - "FlightDestinationFactor", - "Flight Destination Factor", - "The minimal distance factor that we need to reach to end linear flight." - }; - constexpr openspace::properties::Property::PropertyInfo StereoInterpolationTimeInfo = { "StereoInterpolationTime", @@ -299,20 +271,6 @@ OrbitalNavigator::Friction::Friction() addProperty(friction); } -OrbitalNavigator::LinearFlight::LinearFlight() - : properties::PropertyOwner({ "LinearFlight" }) - , apply(ApplyLinearFlightInfo, false) - , destinationDistance(FlightDestinationDistInfo, 2e8f, 10.f, 1e10f) - , destinationFactor(FlightDestinationFactorInfo, 1E-4, 1E-6, 0.5, 1E-3) - , velocitySensitivity(VelocityZoomControlInfo, 3.5f, 0.001f, 20.f) -{ - addProperty(apply); - addProperty(velocitySensitivity); - destinationDistance.setExponent(5.f); - addProperty(destinationDistance); - addProperty(destinationFactor); -} - OrbitalNavigator::IdleBehavior::IdleBehavior() : properties::PropertyOwner({ "IdleBehavior" }) , apply(ApplyIdleBehaviorInfo, false) @@ -472,7 +430,6 @@ OrbitalNavigator::OrbitalNavigator() }); addPropertySubOwner(_friction); - addPropertySubOwner(_linearFlight); addPropertySubOwner(_idleBehavior); _idleBehaviorDampenInterpolator.setTransferFunction( @@ -593,37 +550,6 @@ void OrbitalNavigator::updateCameraStateFromStates(double deltaTime) { _camera->rotationQuaternion() }; - if (_linearFlight.apply) { - // Calculate a position handle based on the camera position in world space - glm::dvec3 camPosToAnchorPosDiff = prevCameraPosition - anchorPos; - // Use the interaction sphere to get an approximate distance to the node surface - double nodeRadius = _anchorNode->interactionSphere(); - double distFromCameraToFocus = - glm::distance(prevCameraPosition, anchorPos) - nodeRadius; - - // Make the approximation delta size depending on the flight distance - double arrivalThreshold = - _linearFlight.destinationDistance * _linearFlight.destinationFactor; - - const double distToDestination = - std::fabs(distFromCameraToFocus - _linearFlight.destinationDistance); - - // Fly towards the flight destination distance. When getting closer than - // arrivalThreshold terminate the flight - if (distToDestination > arrivalThreshold) { - pose.position = moveCameraAlongVector( - pose.position, - distFromCameraToFocus, - camPosToAnchorPosDiff, - _linearFlight.destinationDistance, - deltaTime - ); - } - else { - _linearFlight.apply = false; - } - } - const bool hasPreviousPositions = _previousAnchorNodePosition.has_value() && _previousAimNodePosition.has_value(); @@ -1416,28 +1342,6 @@ glm::dvec3 OrbitalNavigator::translateHorizontally(double deltaTime, return cameraPosition + rotationDiffVec3; } -glm::dvec3 OrbitalNavigator::moveCameraAlongVector(const glm::dvec3& camPos, - double distFromCameraToFocus, - const glm::dvec3& camPosToAnchorPosDiff, - double destination, - double deltaTime) const -{ - // This factor adapts the velocity so it slows down when getting closer - // to our final destination - double velocity = 0.0; - - if (destination < distFromCameraToFocus) { // When flying towards anchor - velocity = 1.0 - destination / distFromCameraToFocus; - } - else { // When flying away from anchor - velocity = distFromCameraToFocus / destination - 1.0; - } - velocity *= _linearFlight.velocitySensitivity * deltaTime; - - // Return the updated camera position - return camPos - velocity * camPosToAnchorPosDiff; -} - glm::dvec3 OrbitalNavigator::followAnchorNodeRotation(const glm::dvec3& cameraPosition, const glm::dvec3& objectPosition, const glm::dquat& focusNodeRotationDiff) const diff --git a/src/navigation/path.cpp b/src/navigation/path.cpp index b5fcb02891..1bd4eb3b30 100644 --- a/src/navigation/path.cpp +++ b/src/navigation/path.cpp @@ -42,7 +42,7 @@ namespace { constexpr const char _loggerCat[] = "Path"; - constexpr const float Epsilon = 1e-5f; + constexpr const float LengthEpsilon = 1e-5f; constexpr const char SunIdentifier[] = "Sun"; @@ -82,6 +82,15 @@ namespace { // A navigation state that determines the start state for the camera path std::optional startState [[codegen::reference("core_navigation_state")]]; + + enum class [[codegen::map(openspace::interaction::Path::Type)]] PathType { + AvoidCollision, + ZoomOutOverview, + Linear, + AvoidCollisionWithLookAt + }; + // The type of the created path. Affects the shape of the resulting path + std::optional pathType; }; #include "path_codegen.cpp" } // namespace @@ -108,6 +117,8 @@ Path::Path(Waypoint start, Waypoint end, Type type, throw ghoul::MissingCaseException(); } + _prevPose = _start.pose(); + // Compute speed factor to match any given duration, by traversing the path and // computing how much faster/slower it should be _speedFactorFromDuration = 1.0; @@ -119,11 +130,9 @@ Path::Path(Waypoint start, Waypoint end, Type type, // We now know how long it took to traverse the path. Use that _speedFactorFromDuration = _progressedTime / *duration; - } - // Reset playback variables - _traveledDistance = 0.0; - _progressedTime = 0.0; + resetPlaybackVariables(); + } } Waypoint Path::startPoint() const { return _start; } @@ -139,12 +148,49 @@ std::vector Path::controlPoints() const { CameraPose Path::traversePath(double dt, float speedScale) { double speed = speedAlongPath(_traveledDistance); speed *= static_cast(speedScale); - const double displacement = dt * speed; + double displacement = dt * speed; + + const double prevDistance = _traveledDistance; _progressedTime += dt; _traveledDistance += displacement; - return interpolatedPose(_traveledDistance); + CameraPose newPose; + + if (_type == Type::Linear) { + // Special handling of linear paths, so that it can be used when we are + // traversing very large distances without introducing precision problems + const glm::dvec3 prevPosToEnd = _prevPose.position - _end.position(); + const double remainingDistance = glm::length(prevPosToEnd); + + // Actual displacement may not be bigger than remaining distance + if (displacement > remainingDistance) { + displacement = remainingDistance; + _traveledDistance = pathLength(); + _shouldQuit = true; + return _end.pose(); + } + + // Just move along the line from the current position to the target + newPose.position = _prevPose.position - + displacement * glm::normalize(prevPosToEnd); + + const double relativeDistance = _traveledDistance / pathLength(); + newPose.rotation = interpolateRotation(relativeDistance); + } + else { + if (std::abs(prevDistance - _traveledDistance) < LengthEpsilon) { + // The distaces are too large, so we are not making progress because of + // insufficient precision + _shouldQuit = true; + LWARNING("Quit camera path prematurely due to insufficient precision"); + } + + newPose = interpolatedPose(_traveledDistance); + } + + _prevPose = newPose; + return newPose; } std::string Path::currentAnchor() const { @@ -153,9 +199,20 @@ std::string Path::currentAnchor() const { } bool Path::hasReachedEnd() const { + if (_shouldQuit) { + return true; + } + return (_traveledDistance / pathLength()) >= 1.0; } +void Path::resetPlaybackVariables() { + _prevPose = _start.pose(); + _traveledDistance = 0.0; + _progressedTime = 0.0; + _shouldQuit = false; +} + CameraPose Path::interpolatedPose(double distance) const { const double relativeDistance = distance / pathLength(); CameraPose cs; @@ -167,8 +224,9 @@ CameraPose Path::interpolatedPose(double distance) const { glm::dquat Path::interpolateRotation(double t) const { switch (_type) { case Type::AvoidCollision: - case Type::Linear: return easedSlerpRotation(t); + case Type::Linear: + return linearPathRotation(t); case Type::ZoomOutOverview: case Type::AvoidCollisionWithLookAt: return lookAtTargetsRotation(t); @@ -183,6 +241,48 @@ glm::dquat Path::easedSlerpRotation(double t) const { return glm::slerp(_start.rotation(), _end.rotation(), tScaled); } +glm::dquat Path::linearPathRotation(double t) const { + const double tHalf = 0.5; + + const glm::dvec3 endNodePos = _end.node()->worldPosition(); + const glm::dvec3 endUp = _end.rotation() * glm::dvec3(0.0, 1.0, 0.0); + + if (t < tHalf) { + // Interpolate to look at target + const glm::dvec3 halfWayPosition = _curve->positionAt(tHalf); + const glm::dquat q = ghoul::lookAtQuaternion(halfWayPosition, endNodePos, endUp); + + const double tScaled = ghoul::sineEaseInOut(t / tHalf); + return glm::slerp(_start.rotation(), q, tScaled); + } + + // This distance is guaranteed to be strictly decreasing for linear paths + const double distanceToEnd = glm::distance(_prevPose.position, _end.position()); + + // Determine the distance at which to start interpolating to the target rotation. + // The magic numbers here are just randomly picked constants, set to make the + // resulting rotation look ok-ish + double closingUpDistance = 10.0 * _end.validBoundingSphere(); + if (pathLength() < 2.0 * closingUpDistance) { + closingUpDistance = 0.2 * pathLength(); + } + + if (distanceToEnd < closingUpDistance) { + // Interpolate to target rotation + const double tScaled = ghoul::sineEaseInOut(1.0 - distanceToEnd / closingUpDistance); + + // Compute a position in front of the camera at the end orientation + const double inFrontDistance = glm::distance(_end.position(), endNodePos); + const glm::dvec3 viewDir = ghoul::viewDirection(_end.rotation()); + const glm::dvec3 inFrontOfEnd = _end.position() + inFrontDistance * viewDir; + const glm::dvec3 lookAtPos = ghoul::interpolateLinear(tScaled, endNodePos, inFrontOfEnd); + return ghoul::lookAtQuaternion(_prevPose.position, lookAtPos, endUp); + } + + // Keep looking at the end node + return ghoul::lookAtQuaternion(_prevPose.position, endNodePos, endUp); +} + glm::dquat Path::lookAtTargetsRotation(double t) const { const double t1 = 0.2; const double t2 = 0.8; @@ -234,51 +334,55 @@ double Path::speedAlongPath(double traveledDistance) const { const glm::dvec3 endNodePos = _end.node()->worldPosition(); const glm::dvec3 startNodePos = _start.node()->worldPosition(); - const CameraPose prevPose = interpolatedPose(traveledDistance); - const double distanceToEndNode = glm::distance(prevPose.position, endNodePos); - const double distanceToStartNode = glm::distance(prevPose.position, startNodePos); + // Set speed based on distance to closest node + const double distanceToEndNode = glm::distance(_prevPose.position, endNodePos); + const double distanceToStartNode = glm::distance(_prevPose.position, startNodePos); + bool isCloserToEnd = distanceToEndNode < distanceToStartNode; - // Decide which is the closest node - SceneGraphNode* closestNode = _start.node(); - glm::dvec3 closestPos = startNodePos; + const glm::dvec3 closestPos = isCloserToEnd ? endNodePos : startNodePos; + const double distanceToClosestNode = glm::distance(closestPos, _prevPose.position); - if (distanceToEndNode < distanceToStartNode) { - closestPos = endNodePos; - closestNode = _end.node(); - } - - const double distanceToClosestNode = glm::distance(closestPos, prevPose.position); - double speed = distanceToClosestNode; - - // Dampen speed in beginning of path - double startUpDistance = 2.0 * _start.node()->boundingSphere(); - if (startUpDistance < Epsilon) { // zero bounding sphere - startUpDistance = glm::distance(_start.position(), startNodePos); + const double speed = distanceToClosestNode; + + // Dampen at the start and end + constexpr const double DampenDistanceFactor = 3.0; + double startUpDistance = DampenDistanceFactor * _start.validBoundingSphere(); + double closeUpDistance = DampenDistanceFactor * _end.validBoundingSphere(); + + if (pathLength() < startUpDistance + closeUpDistance) { + startUpDistance = 0.49 * pathLength(); // a little less than half + closeUpDistance = startUpDistance; } + double dampeningFactor = 1.0; if (traveledDistance < startUpDistance) { - speed *= traveledDistance / startUpDistance + 0.01; + dampeningFactor = traveledDistance / startUpDistance; } - - // Dampen speed in end of path - // Note: this leads to problems when the full length of the path is really big - double closeUpDistance = 2.0 * _end.node()->boundingSphere(); - if (closeUpDistance < Epsilon) { // zero bounding sphere - closeUpDistance = glm::distance(_end.position(), endNodePos); + else if (_type == Type::Linear) { + // Dampen at end of linear path is handled separately, as we can use the + // current position to scompute the remaining distance rather than the + // path length minus travels distance. This is more suitable for long paths + const double remainingDistance = glm::distance( + _prevPose.position, + _end.position() + ); + if (remainingDistance < closeUpDistance) { + dampeningFactor = remainingDistance / closeUpDistance; + } } - - if (traveledDistance > (pathLength() - closeUpDistance)) { + else if (traveledDistance > (pathLength() - closeUpDistance)) { const double remainingDistance = pathLength() - traveledDistance; - speed *= remainingDistance / closeUpDistance + 0.01; + dampeningFactor = remainingDistance / closeUpDistance; } + dampeningFactor = ghoul::sineEaseOut(dampeningFactor); + + // Prevent multiplying with 0 (and hence a speed of 0.0 => no movement) + dampeningFactor += 0.01; // TODO: also dampen speed based on curvature, or make sure the curve has a rounder // shape - // TODO: check for when path is shorter than the starUpDistance or closeUpDistance - // variables - - return _speedFactorFromDuration * speed; + return _speedFactorFromDuration * speed * dampeningFactor; } Waypoint waypointFromCamera() { @@ -297,7 +401,7 @@ SceneGraphNode* findNodeNearTarget(const SceneGraphNode* node) { bool isSame = (n->identifier() == node->identifier()); // If the nodes are in the very same position, they are probably representing // the same object - isSame |= glm::distance(n->worldPosition(), node->worldPosition()) < Epsilon; + isSame |= glm::distance(n->worldPosition(), node->worldPosition()) < LengthEpsilon; if (isSame) { continue; @@ -376,7 +480,9 @@ struct NodeInfo { bool useTargetUpDirection; }; -Waypoint computeWaypointFromNodeInfo(const NodeInfo& info, const Waypoint& startPoint) { +Waypoint computeWaypointFromNodeInfo(const NodeInfo& info, const Waypoint& startPoint, + Path::Type type) +{ const SceneGraphNode* targetNode = sceneGraphNode(info.identifier); if (!targetNode) { LERROR(fmt::format("Could not find target node '{}'", info.identifier)); @@ -392,12 +498,23 @@ Waypoint computeWaypointFromNodeInfo(const NodeInfo& info, const Waypoint& start ); } else { - const double radius = Waypoint::findValidBoundingSphere(targetNode); - const double defaultHeight = 2.0 * radius; + const PathNavigator& navigator = global::navigationHandler->pathNavigator(); + + const double radius = navigator.findValidBoundingSphere(targetNode); + const double defaultHeight = radius * navigator.arrivalDistanceFactor(); const double height = info.height.value_or(defaultHeight); const double distanceFromNodeCenter = radius + height; - const glm::dvec3 stepDir = computeGoodStepDirection(targetNode, startPoint); + glm::dvec3 stepDir = glm::dvec3(0.0); + if (type == Path::Type::Linear) { + // If linear path, compute position along line form start to end point + glm::dvec3 endNodePos = targetNode->worldPosition(); + stepDir = glm::normalize(startPoint.position() - endNodePos); + } + else { + stepDir = computeGoodStepDirection(targetNode, startPoint); + } + targetPos = targetNode->worldPosition() + stepDir * distanceFromNodeCenter; } @@ -416,18 +533,29 @@ Waypoint computeWaypointFromNodeInfo(const NodeInfo& info, const Waypoint& start return Waypoint(targetPos, targetRot, info.identifier); } -Path createPathFromDictionary(const ghoul::Dictionary& dictionary, Path::Type type) { +Path createPathFromDictionary(const ghoul::Dictionary& dictionary, + std::optional forceType) +{ const Parameters p = codegen::bake(dictionary); const std::optional duration = p.duration; + Path::Type type; + if (forceType.has_value()) { + type = *forceType; + } + else if (p.pathType.has_value()) { + type = codegen::map(*p.pathType); + } + else { + type = global::navigationHandler->pathNavigator().defaultPathType(); + } + bool hasStart = p.startState.has_value(); const Waypoint startPoint = hasStart ? Waypoint(NavigationState(p.startState.value())) : waypointFromCamera(); - // TODO: also handle curve type here - std::vector waypoints; switch (p.targetType) { case Parameters::TargetType::NavigationState: { @@ -462,7 +590,7 @@ Path createPathFromDictionary(const ghoul::Dictionary& dictionary, Path::Type ty p.useTargetUpDirection.value_or(false) }; - waypoints = { computeWaypointFromNodeInfo(info, startPoint) }; + waypoints = { computeWaypointFromNodeInfo(info, startPoint, type) }; break; } default: { @@ -473,7 +601,36 @@ Path createPathFromDictionary(const ghoul::Dictionary& dictionary, Path::Type ty // @TODO (emmbr) Allow for an instruction to represent a list of multiple waypoints const Waypoint waypointToAdd = waypoints[0]; - return Path(startPoint, waypointToAdd, type, duration); + if (glm::distance(startPoint.position(), waypointToAdd.position()) < LengthEpsilon) { + LINFO("Already at the requested target"); + throw PathCurve::TooShortPathError("Path too short!"); + } + + try { + return Path(startPoint, waypointToAdd, type, duration); + } + catch (const PathCurve::TooShortPathError& e) { + LINFO("Already at the requested target"); + // Rethrow e, so the pathnavigator can handle it as well + throw e; + } + catch (const PathCurve::InsufficientPrecisionError&) { + // There wasn't enough precision to represent the full curve in world + // coordinates. For now, use a linear path instead. It uses another + // method of interpolation that isn't as sensitive to these kinds of problems + + LINFO( + "Switching to a linear path, to avoid problems with precision due to " + "immense path length." + ); + + return createPathFromDictionary(dictionary, Path::Type::Linear); + + // @TODO (emmbr26, 2022-02-15): later on we want to improve this case, so that + // interpolation is not a problem. One suggestion to solve this would be using + // two identical paths and switch the direction of interpolation halfway through. + // That should give us sufficient precision at both ends of the path + } } } // namespace openspace::interaction diff --git a/src/navigation/pathcurve.cpp b/src/navigation/pathcurve.cpp index e8f8eb0368..72aab1dc54 100644 --- a/src/navigation/pathcurve.cpp +++ b/src/navigation/pathcurve.cpp @@ -35,11 +35,23 @@ #include namespace { - constexpr const int NrSamplesPerSegment = 100; + constexpr const char* _loggerCat = "PathCurve"; + + constexpr const double LengthEpsilon = + 100.0 * std::numeric_limits::epsilon(); + } // namespace namespace openspace::interaction { +PathCurve::InsufficientPrecisionError::InsufficientPrecisionError(std::string error) + : ghoul::RuntimeError(std::move(error), "PathCurve") +{} + +PathCurve::TooShortPathError::TooShortPathError(std::string error) + : ghoul::RuntimeError(std::move(error), "PathCurve") +{} + PathCurve::~PathCurve() {} double PathCurve::length() const { @@ -57,21 +69,19 @@ std::vector PathCurve::points() const { void PathCurve::initializeParameterData() { _nSegments = static_cast(_points.size() - 3); - ghoul_assert(_nSegments > 0, "Cannot have a curve with zero segments!"); _curveParameterSteps.clear(); _lengthSums.clear(); _parameterSamples.clear(); + const double max = static_cast(_nSegments); + // Evenly space out parameter intervals _curveParameterSteps.reserve(_nSegments + 1); - const double dt = 1.0 / _nSegments; - _curveParameterSteps.push_back(0.0); - for (unsigned int i = 1; i < _nSegments; i++) { - _curveParameterSteps.push_back(dt * i); + for (int i = 0; i <= _nSegments; i++) { + _curveParameterSteps.push_back(static_cast(i)); } - _curveParameterSteps.push_back(1.0); // Arc lengths _lengthSums.reserve(_nSegments + 1); @@ -84,49 +94,82 @@ void PathCurve::initializeParameterData() { } _totalLength = _lengthSums.back(); + if (_totalLength < LengthEpsilon) { + throw TooShortPathError("Path too short!"); + } + // Compute a map of arc lengths s and curve parameters u, for reparameterization - _parameterSamples.reserve(NrSamplesPerSegment * _nSegments + 1); - const double uStep = 1.0 / (_nSegments * NrSamplesPerSegment); + constexpr const int Steps = 100; + const double uStep = 1.0 / static_cast(Steps); + _parameterSamples.reserve(Steps * _nSegments + 1); + for (unsigned int i = 0; i < _nSegments; i++) { double uStart = _curveParameterSteps[i]; double sStart = _lengthSums[i]; - for (int j = 0; j < NrSamplesPerSegment; ++j) { + _parameterSamples.push_back({ uStart, sStart }); + // Intermediate sampels + for (int j = 1; j < Steps; ++j) { double u = uStart + j * uStep; double s = sStart + arcLength(uStart, u); + // Identify samples that are indistinguishable due to precision limitations + if (std::abs(s - _parameterSamples.back().s) < LengthEpsilon) { + throw InsufficientPrecisionError("Insufficient precision due to path length"); + } _parameterSamples.push_back({ u, s }); } } - _parameterSamples.push_back({ 1.0, _totalLength }); + + // Remove the very last sample if indistinguishable from the final one + const double diff = std::abs(_totalLength - _parameterSamples.back().s); + if (diff < LengthEpsilon) { + _parameterSamples.pop_back(); + } + + _parameterSamples.push_back({ max, _totalLength }); + _parameterSamples.shrink_to_fit(); } // Compute the curve parameter from an arc length value, using a combination of // Newton's method and bisection. Source: // https://www.geometrictools.com/Documentation/MovingAlongCurveSpecifiedSpeed.pdf // Input s is a length value, in the range [0, _totalLength] -// Returns curve parameter in range [0, 1] +// Returns curve parameter in range [0, _nSegments] double PathCurve::curveParameter(double s) const { if (s <= 0.0) return 0.0; - if (s >= _totalLength) return 1.0; + if (s >= (_totalLength - LengthEpsilon)) return _curveParameterSteps.back(); unsigned int segmentIndex = 1; while (s > _lengthSums[segmentIndex]) { segmentIndex++; } - const int startIndex = (segmentIndex - 1) * NrSamplesPerSegment; - const int endIndex = segmentIndex * NrSamplesPerSegment + 1; - const double segmentS = s - _lengthSums[segmentIndex - 1]; const double uMin = _curveParameterSteps[segmentIndex - 1]; const double uMax = _curveParameterSteps[segmentIndex]; + // Find sample indices corresponding to the selected sample + auto findIndexOfFirstEqualOrLarger_u = [&samples = _parameterSamples](double value) { + auto it = std::lower_bound( + samples.begin(), + samples.end(), + ParameterPair{ value, 0.0 }, // 0.0 is a dummy value for s + [](const ParameterPair& lhs, const ParameterPair& rhs) { + return lhs.u < rhs.u; + } + ); + return std::distance(samples.begin(), it); + }; + + size_t startIndex = findIndexOfFirstEqualOrLarger_u(uMin); + size_t endIndex = findIndexOfFirstEqualOrLarger_u(uMax); + // Use samples to find an initial guess for Newton's method // Find first sample with s larger than input s auto sampleIterator = std::upper_bound( _parameterSamples.begin() + startIndex, _parameterSamples.begin() + endIndex, ParameterPair{ 0.0 , s }, // 0.0 is a dummy value for u - [](const ParameterPair& lhs,const ParameterPair& rhs) { + [](const ParameterPair& lhs, const ParameterPair& rhs) { return lhs.s < rhs.s; } ); @@ -148,7 +191,7 @@ double PathCurve::curveParameter(double s) const { double F = arcLength(uMin, u) - segmentS; // The error we tolerate, in meters. Note that distances are very large - constexpr const double tolerance = 0.005; + constexpr const double tolerance = 0.5; if (std::abs(F) <= tolerance) { return u; } @@ -174,11 +217,12 @@ double PathCurve::curveParameter(double s) const { } double PathCurve::approximatedDerivative(double u, double h) const { + const double max = _curveParameterSteps.back(); if (u <= h) { return (1.0 / h) * glm::length(interpolate(0.0 + h) - interpolate(0.0)); } - if (u >= 1.0 - h) { - return (1.0 / h) * glm::length(interpolate(1.0) - interpolate(1.0 - h)); + if (u >= max - h) { + return (1.0 / h) * glm::length(interpolate(max) - interpolate(max - h)); } return (0.5 / h) * glm::length(interpolate(u + h) - interpolate(u - h)); } @@ -196,12 +240,13 @@ double PathCurve::arcLength(double lowerLimit, double upperLimit) const { } glm::dvec3 PathCurve::interpolate(double u) const { - ghoul_assert(u >= 0 && u <= 1.0, "Interpolation variable must be in range [0,1]"); + const double max = _curveParameterSteps.back(); + ghoul_assert(u >= 0 && u <= max, "Interpolation variable must be in range [0,_nSegments]"); - if (u < 0.0) { + if (u <= 0.0) { return _points[1]; } - if (u > 1.0) { + if (u >= max) { return *(_points.end() - 2); } @@ -226,11 +271,26 @@ glm::dvec3 PathCurve::interpolate(double u) const { } LinearCurve::LinearCurve(const Waypoint& start, const Waypoint& end) { - _points.push_back(start.position()); _points.push_back(start.position()); _points.push_back(end.position()); - _points.push_back(end.position()); - initializeParameterData(); + + _totalLength = glm::distance(start.position(), end.position()); + _nSegments = 1; +} + +glm::dvec3 LinearCurve::positionAt(double relativeDistance) const { + return interpolate(relativeDistance); +} + +glm::dvec3 LinearCurve::interpolate(double u) const { + if (u <= 0.0) { + return _points.front(); + } + if (u >= 1.0) { + return _points.back(); + } + + return ghoul::interpolateLinear(u, _points.front(), _points.back()); } } // namespace openspace::interaction diff --git a/src/navigation/pathcurves/avoidcollisioncurve.cpp b/src/navigation/pathcurves/avoidcollisioncurve.cpp index 1f123d289e..d5365e299f 100644 --- a/src/navigation/pathcurves/avoidcollisioncurve.cpp +++ b/src/navigation/pathcurves/avoidcollisioncurve.cpp @@ -70,10 +70,18 @@ AvoidCollisionCurve::AvoidCollisionCurve(const Waypoint& start, const Waypoint& // Add an extra point to first go backwards if starting close to planet glm::dvec3 nodeToStart = start.position() - startNodeCenter; - double distanceToStartNode = glm::length(nodeToStart); + const double distanceToStartNode = glm::length(nodeToStart); - if (distanceToStartNode < CloseToNodeThresholdRadiusMultiplier * startNodeRadius) { - double distance = startNodeRadius; + // Note that the factor 2.0 is arbitrarily chosen to look ok. + // @TODO: (2022-02-27, emmbr) Should be unified to a "getting close to object sphere" + // that can be used in multiple cases when creating paths more cleverly later on + const double closeToNodeThresholdFactor = glm::max( + CloseToNodeThresholdRadiusMultiplier, + 2.0 * global::navigationHandler->pathNavigator().arrivalDistanceFactor() + ); + + if (distanceToStartNode < closeToNodeThresholdFactor * startNodeRadius) { + const double distance = startNodeRadius; glm::dvec3 newPos = start.position() + distance * glm::normalize(nodeToStart); _points.push_back(newPos); } @@ -104,8 +112,8 @@ AvoidCollisionCurve::AvoidCollisionCurve(const Waypoint& start, const Waypoint& const glm::dvec3 nodeToEnd = end.position() - endNodeCenter; const double distanceToEndNode = glm::length(nodeToEnd); - if (distanceToEndNode < CloseToNodeThresholdRadiusMultiplier * endNodeRadius) { - double distance = endNodeRadius; + if (distanceToEndNode < closeToNodeThresholdFactor * endNodeRadius) { + const double distance = endNodeRadius; glm::dvec3 newPos = end.position() + distance * glm::normalize(nodeToEnd); _points.push_back(newPos); } diff --git a/src/navigation/pathnavigator.cpp b/src/navigation/pathnavigator.cpp index e9e5922014..fb161cdedf 100644 --- a/src/navigation/pathnavigator.cpp +++ b/src/navigation/pathnavigator.cpp @@ -45,10 +45,9 @@ namespace { constexpr openspace::properties::Property::PropertyInfo DefaultCurveOptionInfo = { "DefaultPathType", "Default Path Type", - "The defualt path type chosen when generating a path. See wiki for alternatives." - " The shape of the generated path will be different depending on the path type." - // TODO (2021-08-15, emmbr) right now there is no way to specify a type for a - // single path instance, only for any created paths + "The default path type chosen when generating a path or flying to a target. " + "See wiki for alternatives. The shape of the generated path will be different " + "depending on the path type." }; constexpr openspace::properties::Property::PropertyInfo IncludeRollInfo = { @@ -72,6 +71,15 @@ namespace { "triggered once the path has reached its target." }; + constexpr openspace::properties::Property::PropertyInfo ArrivalDistanceFactorInfo = { + "ArrivalDistanceFactor", + "Arrival Distance Factor", + "A factor used to compute the default distance from a target scene graph node " + "when creating a camera path. The factor will be multipled with the node's " + "bounding sphere to compute the target height from the bounding sphere of the " + "object." + }; + constexpr const openspace::properties::Property::PropertyInfo MinBoundingSphereInfo = { "MinimalValidBoundingSphere", @@ -102,13 +110,14 @@ PathNavigator::PathNavigator() , _includeRoll(IncludeRollInfo, false) , _speedScale(SpeedScaleInfo, 1.f, 0.01f, 2.f) , _applyIdleBehaviorOnFinish(IdleBehaviorOnFinishInfo, false) + , _arrivalDistanceFactor(ArrivalDistanceFactorInfo, 2.0, 0.1, 20.0) , _minValidBoundingSphere(MinBoundingSphereInfo, 10.0, 1.0, 3e10) , _relevantNodeTags(RelevantNodeTagsInfo) { _defaultPathType.addOptions({ { Path::Type::AvoidCollision, "AvoidCollision" }, - { Path::Type::Linear, "Linear" }, { Path::Type::ZoomOutOverview, "ZoomOutOverview"}, + { Path::Type::Linear, "Linear" }, { Path::Type::AvoidCollisionWithLookAt, "AvoidCollisionWithLookAt"} }); addProperty(_defaultPathType); @@ -116,6 +125,7 @@ PathNavigator::PathNavigator() addProperty(_includeRoll); addProperty(_speedScale); addProperty(_applyIdleBehaviorOnFinish); + addProperty(_arrivalDistanceFactor); addProperty(_minValidBoundingSphere); _relevantNodeTags = std::vector{ @@ -144,6 +154,10 @@ double PathNavigator::speedScale() const { return _speedScale; } +double PathNavigator::arrivalDistanceFactor() const { + return _arrivalDistanceFactor; +} + bool PathNavigator::hasCurrentPath() const { return _currentPath != nullptr; } @@ -191,7 +205,7 @@ void PathNavigator::updateCamera(double deltaTime) { camera()->setPose(newPose); if (_currentPath->hasReachedEnd()) { - LINFO("Reached end of path"); + LINFO("Reached target"); handlePathEnd(); if (_applyIdleBehaviorOnFinish) { @@ -211,10 +225,6 @@ void PathNavigator::updateCamera(double deltaTime) { } void PathNavigator::createPath(const ghoul::Dictionary& dictionary) { - // @TODO (2021-08.16, emmbr): Improve how curve types are handled. - // We want the user to be able to choose easily - const int pathType = _defaultPathType; - OpenSpaceEngine::Mode m = global::openSpaceEngine->currentMode(); if (m == OpenSpaceEngine::Mode::SessionRecordingPlayback) { // Silently ignore any paths that are being created during a session recording @@ -225,9 +235,7 @@ void PathNavigator::createPath(const ghoul::Dictionary& dictionary) { clearPath(); try { - _currentPath = std::make_unique( - createPathFromDictionary(dictionary, Path::Type(pathType)) - ); + _currentPath = std::make_unique(createPathFromDictionary(dictionary)); } catch (const documentation::SpecificationError& e) { LERROR("Could not create camera path"); @@ -238,16 +246,21 @@ void PathNavigator::createPath(const ghoul::Dictionary& dictionary) { LWARNINGC(w.offender, ghoul::to_string(w.reason)); } } + catch (const PathCurve::TooShortPathError&) { + // Do nothing + } catch (const ghoul::RuntimeError& e) { LERROR(fmt::format("Could not create path. Reason: ", e.message)); return; } - LINFO("Successfully generated camera path"); + LDEBUG("Successfully generated camera path"); } void PathNavigator::clearPath() { - LDEBUG("Clearing path"); + if (_currentPath) { + LDEBUG("Clearing path"); + } _currentPath = nullptr; } @@ -272,16 +285,14 @@ void PathNavigator::startPath() { ); _startSimulationTimeOnFinish = true; - LINFO( - "Pausing time simulation during path traversal. " - "Will unpause once the camera path is finished" - ); + LINFO("Pausing time simulation during path traversal"); } LINFO("Starting path"); _isPlaying = true; global::navigationHandler->orbitalNavigator().updateOnCameraInteraction(); + global::navigationHandler->orbitalNavigator().resetVelocities(); } void PathNavigator::abortPath() { @@ -323,10 +334,58 @@ void PathNavigator::continuePath() { _isPlaying = true; } +Path::Type PathNavigator::defaultPathType() const { + const int pathType = _defaultPathType; + return Path::Type(pathType); +} + double PathNavigator::minValidBoundingSphere() const { return _minValidBoundingSphere; } +double PathNavigator::findValidBoundingSphere(const SceneGraphNode* node) const { + ghoul_assert(node != nullptr, "Node must not be nulltpr"); + auto sphere = [](const SceneGraphNode* n) { + // Use the biggest of the bounding sphere and interaction sphere, + // so we don't accidentally choose a bounding sphere that is much smaller + // than the interaction sphere of the node + double bs = n->boundingSphere(); + double is = n->interactionSphere(); + return is > bs ? is : bs; + }; + + double result = sphere(node); + + if (result < _minValidBoundingSphere) { + // If the bs of the target is too small, try to find a good value in a child node. + // Only check the closest children, to avoid deep traversal in the scene graph. + // Alsp. the chance to find a bounding sphere that represents the visual size of + // the target well is higher for these nodes + for (SceneGraphNode* child : node->children()) { + result = sphere(child); + if (result > _minValidBoundingSphere) { + LDEBUG(fmt::format( + "The scene graph node '{}' has no, or a very small, bounding sphere. " + "Using bounding sphere of child node '{}' in computations.", + node->identifier(), + child->identifier() + )); + return result; + } + } + + LDEBUG(fmt::format( + "The scene graph node '{}' has no, or a very small, bounding sphere. Using " + "minimal value. This might lead to unexpected results.", + node->identifier()) + ); + + result = _minValidBoundingSphere; + } + + return result; +} + const std::vector& PathNavigator::relevantNodes() { if (!_hasInitializedRelevantNodes) { findRelevantNodes(); @@ -346,7 +405,6 @@ void PathNavigator::handlePathEnd() { ); } _startSimulationTimeOnFinish = false; - clearPath(); global::openSpaceEngine->resetMode(); } @@ -438,9 +496,9 @@ scripting::LuaLibrary PathNavigator::luaLibrary() { &luascriptfunctions::flyTo, "string [, bool, double]", "Move the camera to the node with the specified identifier. The optional " - "double specifies the duration of the motion. If the optional bool is " - "set to true the target up vector for camera is set based on the target " - "node. Either of the optional parameters can be left out." + "double specifies the duration of the motion, in seconds. If the optional " + "bool is set to true the target up vector for camera is set based on the " + "target node. Either of the optional parameters can be left out." }, { "flyToHeight", @@ -449,18 +507,48 @@ scripting::LuaLibrary PathNavigator::luaLibrary() { "Move the camera to the node with the specified identifier. The second " "argument is the desired target height above the target node's bounding " "sphere, in meters. The optional double specifies the duration of the " - "motion. If the optional bool is set to true, the target up vector for " - "camera is set based on the target node. Either of the optional " - "parameters can be left out." + "motion, in seconds. If the optional bool is set to true, the target " + "up vector for camera is set based on the target node. Either of the " + "optional parameters can be left out." }, - { + { "flyToNavigationState", &luascriptfunctions::flyToNavigationState, "table, [double]", "Create a path to the navigation state described by the input table. " - "The optional double specifies the target duration of the motion. Note " - "that roll must be included for the target up direction to be taken " - "into account." + "The optional double specifies the target duration of the motion, " + "in seconds. Note that roll must be included for the target up " + "direction to be taken into account." + }, + { + "zoomToFocus", + &luascriptfunctions::zoomToFocus, + "[duration]", + "Zoom linearly to the current focus node, using the default distance." + "The optional input parameter specifies the duration for the motion, " + "in seconds." + }, + { + "zoomToDistance", + &luascriptfunctions::zoomToDistance, + "distance, [duration]", + "Fly linearly to a specific distance in relation to the focus node. " + "The distance is given in meters above the bounding sphere of the " + "current focus node." + "The optional input parameter specifies the duration for the motion, " + "in seconds." + }, + { + "zoomToDistanceRelative", + &luascriptfunctions::zoomToDistanceRelative, + "distance, [duration]", + "Fly linearly to a specific distance in relation to the focus node. " + "The distance is given as a multiple of the bounding sphere of the " + "current focus node. That is, a value of 1 will result in a position " + "at a distance of one times the size of the bounding sphere away from " + "the object." + "The optional input parameter specifies the duration for the motion, " + "in seconds." }, { "createPath", diff --git a/src/navigation/pathnavigator_lua.inl b/src/navigation/pathnavigator_lua.inl index de306c1d4c..2e29b240e2 100644 --- a/src/navigation/pathnavigator_lua.inl +++ b/src/navigation/pathnavigator_lua.inl @@ -37,10 +37,6 @@ #include #include -namespace { - constexpr const double Epsilon = 1e-5; -} // namespace - namespace openspace::luascriptfunctions { int isFlying(lua_State* L) { @@ -81,7 +77,6 @@ int flyTo(lua_State* L) { return ghoul::lua::luaError(L, "Duration cannot be specified twice"); } - if (!sceneGraphNode(nodeIdentifier)) { return ghoul::lua::luaError(L, "Unknown node name: " + nodeIdentifier); } @@ -98,7 +93,7 @@ int flyTo(lua_State* L) { } else { double d = std::get(*useUpFromTargetOrDuration); - if (d <= Epsilon) { + if (d <= 0.0) { return ghoul::lua::luaError(L, "Duration must be larger than zero"); } insDict.setValue("Duration", d); @@ -106,7 +101,7 @@ int flyTo(lua_State* L) { } if (duration.has_value()) { double d = *duration; - if (d <= Epsilon) { + if (d <= 0.0) { return ghoul::lua::luaError(L, "Duration must be larger than zero"); } insDict.setValue("Duration", d); @@ -146,7 +141,7 @@ int flyToHeight(lua_State* L) { } else { double d = std::get(*useUpFromTargetOrDuration); - if (d <= Epsilon) { + if (d <= 0.0) { return ghoul::lua::luaError(L, "Duration must be larger than zero"); } insDict.setValue("Duration", d); @@ -154,7 +149,7 @@ int flyToHeight(lua_State* L) { } if (duration.has_value()) { double d = *duration; - if (d <= Epsilon) { + if (d <= 0.0) { return ghoul::lua::luaError(L, "Duration must be larger than zero"); } insDict.setValue("Duration", d); @@ -194,7 +189,7 @@ int flyToNavigationState(lua_State* L) { if (duration.has_value()) { double d = *duration; - if (d <= Epsilon) { + if (d <= 0.0) { return ghoul::lua::luaError(L, "Duration must be larger than zero"); } instruction.setValue("Duration", d); @@ -208,6 +203,113 @@ int flyToNavigationState(lua_State* L) { return 0; } +int zoomToFocus(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, { 0, 1 }, "lua::zoomToFocus"); + std::optional duration = ghoul::lua::value>(L); + + const SceneGraphNode* node = global::navigationHandler->anchorNode(); + if (!node) { + return ghoul::lua::luaError(L, "Could not determine current focus node"); + } + + ghoul::Dictionary insDict; + insDict.setValue("TargetType", std::string("Node")); + insDict.setValue("Target", node->identifier()); + insDict.setValue("PathType", std::string("Linear")); + + if (duration.has_value()) { + double d = *duration; + if (d <= 0.0) { + return ghoul::lua::luaError(L, "Duration must be larger than zero"); + } + insDict.setValue("Duration", d); + } + + global::navigationHandler->pathNavigator().createPath(insDict); + + if (global::navigationHandler->pathNavigator().hasCurrentPath()) { + global::navigationHandler->pathNavigator().startPath(); + } + + return 0; +} + +int zoomToDistance(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, { 1, 2 }, "lua::zoomToDistance"); + auto [distance, duration] = + ghoul::lua::values>(L); + + if (distance <= 0.0) { + return ghoul::lua::luaError(L, "The distance must be larger than zero"); + } + + const SceneGraphNode* node = global::navigationHandler->anchorNode(); + if (!node) { + return ghoul::lua::luaError(L, "Could not determine current focus node"); + } + + ghoul::Dictionary insDict; + insDict.setValue("TargetType", std::string("Node")); + insDict.setValue("Target", node->identifier()); + insDict.setValue("Height", distance); + insDict.setValue("PathType", std::string("Linear")); + + if (duration.has_value()) { + double d = *duration; + if (d <= 0.0) { + return ghoul::lua::luaError(L, "Duration must be larger than zero"); + } + insDict.setValue("Duration", d); + } + + global::navigationHandler->pathNavigator().createPath(insDict); + + if (global::navigationHandler->pathNavigator().hasCurrentPath()) { + global::navigationHandler->pathNavigator().startPath(); + } + + return 0; +} + +int zoomToDistanceRelative(lua_State* L) { + ghoul::lua::checkArgumentsAndThrow(L, { 1, 2 }, "lua::zoomToDistanceRelative"); + auto [distance, duration] = + ghoul::lua::values>(L); + + if (distance <= 0.0) { + return ghoul::lua::luaError(L, "The distance must be larger than zero"); + } + + const SceneGraphNode* node = global::navigationHandler->anchorNode(); + if (!node) { + return ghoul::lua::luaError(L, "Could not determine current focus node"); + } + + distance *= node->boundingSphere(); + + ghoul::Dictionary insDict; + insDict.setValue("TargetType", std::string("Node")); + insDict.setValue("Target", node->identifier()); + insDict.setValue("Height", distance); + insDict.setValue("PathType", std::string("Linear")); + + if (duration.has_value()) { + double d = *duration; + if (d <= 0.0) { + return ghoul::lua::luaError(L, "Duration must be larger than zero"); + } + insDict.setValue("Duration", d); + } + + global::navigationHandler->pathNavigator().createPath(insDict); + + if (global::navigationHandler->pathNavigator().hasCurrentPath()) { + global::navigationHandler->pathNavigator().startPath(); + } + + return 0; +} + int createPath(lua_State* L) { ghoul::lua::checkArgumentsAndThrow(L, 1, "lua::createPath"); ghoul::Dictionary dictionary = ghoul::lua::value(L); diff --git a/src/navigation/waypoint.cpp b/src/navigation/waypoint.cpp index 4c29170702..7db02e9014 100644 --- a/src/navigation/waypoint.cpp +++ b/src/navigation/waypoint.cpp @@ -49,7 +49,9 @@ Waypoint::Waypoint(const glm::dvec3& pos, const glm::dquat& rot, const std::stri LERROR(fmt::format("Could not find node '{}'", _nodeIdentifier)); return; } - _validBoundingSphere = findValidBoundingSphere(node); + + const PathNavigator& navigator = global::navigationHandler->pathNavigator(); + _validBoundingSphere = navigator.findValidBoundingSphere(node); } Waypoint::Waypoint(const NavigationState& ns) { @@ -61,7 +63,8 @@ Waypoint::Waypoint(const NavigationState& ns) { } _nodeIdentifier = ns.anchor; - _validBoundingSphere = findValidBoundingSphere(anchorNode); + const PathNavigator& navigator = global::navigationHandler->pathNavigator(); + _validBoundingSphere = navigator.findValidBoundingSphere(anchorNode); _pose = ns.cameraPose(); } @@ -89,41 +92,4 @@ double Waypoint::validBoundingSphere() const { return _validBoundingSphere; } -double Waypoint::findValidBoundingSphere(const SceneGraphNode* node) { - double bs = node->boundingSphere(); - - const double minValidBoundingSphere = - global::navigationHandler->pathNavigator().minValidBoundingSphere(); - - if (bs < minValidBoundingSphere) { - // If the bs of the target is too small, try to find a good value in a child node. - // Only check the closest children, to avoid deep traversal in the scene graph. - // Alsp. the chance to find a bounding sphere that represents the visual size of - // the target well is higher for these nodes - for (SceneGraphNode* child : node->children()) { - bs = child->boundingSphere(); - if (bs > minValidBoundingSphere) { - LWARNING(fmt::format( - "The scene graph node '{}' has no, or a very small, bounding sphere. " - "Using bounding sphere of child node '{}' in computations.", - node->identifier(), - child->identifier() - )); - - return bs; - } - } - - LWARNING(fmt::format( - "The scene graph node '{}' has no, or a very small, bounding sphere. Using " - "minimal value. This might lead to unexpected results.", - node->identifier()) - ); - - bs = minValidBoundingSphere; - } - - return bs; -} - } // namespace openspace::interaction diff --git a/src/scene/sceneinitializer.cpp b/src/scene/sceneinitializer.cpp index 2f121cf403..b1068e27a9 100644 --- a/src/scene/sceneinitializer.cpp +++ b/src/scene/sceneinitializer.cpp @@ -100,6 +100,13 @@ void MultiThreadedSceneInitializer::initializeNode(SceneGraphNode* node) { } std::vector MultiThreadedSceneInitializer::takeInitializedNodes() { + // Some of the scene graph nodes might still be in the initialization queue and we + // should wait for those to finish or we end up in some half-initialized state since + // other parts of the application already know about their existence + while (_threadPool.hasOutstandingTasks()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::lock_guard g(_mutex); std::vector nodes = std::move(_initializedNodes); return nodes; diff --git a/src/util/threadpool.cpp b/src/util/threadpool.cpp index b07c2b44a2..ab77e0939c 100644 --- a/src/util/threadpool.cpp +++ b/src/util/threadpool.cpp @@ -99,4 +99,8 @@ void ThreadPool::clearTasks() { } // release lock } +bool ThreadPool::hasOutstandingTasks() const { + return !tasks.empty(); +} + } // namespace openspace diff --git a/support/coding/codegen b/support/coding/codegen index ad3aa1e5e1..f511a93fdd 160000 --- a/support/coding/codegen +++ b/support/coding/codegen @@ -1 +1 @@ -Subproject commit ad3aa1e5e1a3656316f2fc94bf60f31090872e4a +Subproject commit f511a93fddee9dda92d87408d2c7587bb63746b9