From 0d2935c43bd6127a0dd6444e9cdcd4388822623f Mon Sep 17 00:00:00 2001 From: Emil Axelsson Date: Wed, 21 Nov 2018 00:19:08 +0100 Subject: [PATCH] Feature/gaia mission merge (#766) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Adam AslegÄrd's module that renders Gaia mission stars * Add module for Gaia mission * Add CCfits and cfitsio submodules and the implemented fitsfilereader module from the OpenSpace-sun-earth-event branch * Add a TaskRunner to read from a fits file --- .gitmodules | 6 + data/assets/gaia.scene | 39 + .../gaia/gaia_dr2_download_stars.asset | 19 + .../scene/milkyway/gaia/gaiastars.asset | 60 + .../solarsystem/missions/gaia/gaia.asset | 67 + .../solarsystem/missions/gaia/trail.asset | 64 + .../missions/gaia/transforms.asset | 27 + data/tasks/gaia/gaia_download.task | 6 + data/tasks/gaia/gaia_octree.task | 45 + data/tasks/gaia/gaia_read.task | 16 + include/openspace/properties/propertyowner.h | 6 + include/openspace/util/distanceconversion.h | 2 + modules/base/CMakeLists.txt | 2 + modules/base/basemodule.cpp | 3 + modules/base/rendering/renderableboxgrid.cpp | 320 +++ modules/base/rendering/renderableboxgrid.h | 83 + modules/base/rotation/fixedrotation.cpp | 47 +- modules/fitsfilereader/CMakeLists.txt | 56 + modules/fitsfilereader/ext/CCfits | 1 + modules/fitsfilereader/ext/cfitsio | 1 + .../fitsfilereader/fitsfilereadermodule.cpp | 31 + modules/fitsfilereader/fitsfilereadermodule.h | 41 + modules/fitsfilereader/include.cmake | 0 .../fitsfilereader/include/fitsfilereader.h | 116 + modules/fitsfilereader/src/fitsfilereader.cpp | 668 +++++ modules/gaia/CMakeLists.txt | 71 + modules/gaia/gaiamodule.cpp | 72 + modules/gaia/gaiamodule.h | 50 + modules/gaia/include.cmake | 4 + modules/gaia/rendering/gaiaoptions.h | 54 + modules/gaia/rendering/octreeculler.cpp | 86 + modules/gaia/rendering/octreeculler.h | 76 + modules/gaia/rendering/octreemanager.cpp | 1415 +++++++++ modules/gaia/rendering/octreemanager.h | 391 +++ .../gaia/rendering/renderablegaiastars.cpp | 2529 +++++++++++++++++ modules/gaia/rendering/renderablegaiastars.h | 216 ++ modules/gaia/scripts/filtering.lua | 102 + modules/gaia/shaders/gaia_billboard_fs.glsl | 114 + modules/gaia/shaders/gaia_billboard_ge.glsl | 132 + .../gaia/shaders/gaia_billboard_nofbo_fs.glsl | 123 + modules/gaia/shaders/gaia_point_fs.glsl | 98 + modules/gaia/shaders/gaia_point_ge.glsl | 76 + modules/gaia/shaders/gaia_ssbo_vs.glsl | 186 ++ .../gaia_tonemapping_billboard_fs.glsl | 53 + .../shaders/gaia_tonemapping_point_fs.glsl | 178 ++ modules/gaia/shaders/gaia_tonemapping_vs.glsl | 34 + modules/gaia/shaders/gaia_vbo_vs.glsl | 119 + modules/gaia/tasks/constructoctreetask.cpp | 817 ++++++ modules/gaia/tasks/constructoctreetask.h | 143 + modules/gaia/tasks/readfilejob.cpp | 265 ++ modules/gaia/tasks/readfilejob.h | 71 + modules/gaia/tasks/readfitstask.cpp | 392 +++ modules/gaia/tasks/readfitstask.h | 82 + modules/gaia/tasks/readspecktask.cpp | 122 + modules/gaia/tasks/readspecktask.h | 50 + modules/server/src/jsonconverters.cpp | 6 - modules/space/CMakeLists.txt | 2 + modules/space/shaders/star_fs.glsl | 2 +- modules/space/shaders/star_ge.glsl | 2 +- modules/space/shaders/star_vs.glsl | 2 +- modules/space/spacemodule.cpp | 3 + .../space/translation/horizonstranslation.cpp | 189 ++ .../space/translation/horizonstranslation.h | 72 + modules/sync/tasks/syncassettask.cpp | 6 + modules/webbrowser/src/webrenderhandler.cpp | 2 +- openspace.cfg | 3 + src/properties/optionproperty.cpp | 3 + src/properties/propertyowner.cpp | 10 + src/util/distanceconversion.cpp | 12 + src/util/transformationmanager.cpp | 4 + tests/test_optionproperty.inl | 4 +- 71 files changed, 10150 insertions(+), 19 deletions(-) create mode 100644 data/assets/gaia.scene create mode 100644 data/assets/scene/milkyway/gaia/gaia_dr2_download_stars.asset create mode 100644 data/assets/scene/milkyway/gaia/gaiastars.asset create mode 100644 data/assets/scene/solarsystem/missions/gaia/gaia.asset create mode 100644 data/assets/scene/solarsystem/missions/gaia/trail.asset create mode 100644 data/assets/scene/solarsystem/missions/gaia/transforms.asset create mode 100644 data/tasks/gaia/gaia_download.task create mode 100644 data/tasks/gaia/gaia_octree.task create mode 100644 data/tasks/gaia/gaia_read.task create mode 100644 modules/base/rendering/renderableboxgrid.cpp create mode 100644 modules/base/rendering/renderableboxgrid.h create mode 100644 modules/fitsfilereader/CMakeLists.txt create mode 160000 modules/fitsfilereader/ext/CCfits create mode 160000 modules/fitsfilereader/ext/cfitsio create mode 100644 modules/fitsfilereader/fitsfilereadermodule.cpp create mode 100644 modules/fitsfilereader/fitsfilereadermodule.h create mode 100644 modules/fitsfilereader/include.cmake create mode 100644 modules/fitsfilereader/include/fitsfilereader.h create mode 100644 modules/fitsfilereader/src/fitsfilereader.cpp create mode 100644 modules/gaia/CMakeLists.txt create mode 100644 modules/gaia/gaiamodule.cpp create mode 100644 modules/gaia/gaiamodule.h create mode 100644 modules/gaia/include.cmake create mode 100644 modules/gaia/rendering/gaiaoptions.h create mode 100644 modules/gaia/rendering/octreeculler.cpp create mode 100644 modules/gaia/rendering/octreeculler.h create mode 100644 modules/gaia/rendering/octreemanager.cpp create mode 100644 modules/gaia/rendering/octreemanager.h create mode 100644 modules/gaia/rendering/renderablegaiastars.cpp create mode 100644 modules/gaia/rendering/renderablegaiastars.h create mode 100644 modules/gaia/scripts/filtering.lua create mode 100644 modules/gaia/shaders/gaia_billboard_fs.glsl create mode 100644 modules/gaia/shaders/gaia_billboard_ge.glsl create mode 100644 modules/gaia/shaders/gaia_billboard_nofbo_fs.glsl create mode 100644 modules/gaia/shaders/gaia_point_fs.glsl create mode 100644 modules/gaia/shaders/gaia_point_ge.glsl create mode 100644 modules/gaia/shaders/gaia_ssbo_vs.glsl create mode 100644 modules/gaia/shaders/gaia_tonemapping_billboard_fs.glsl create mode 100644 modules/gaia/shaders/gaia_tonemapping_point_fs.glsl create mode 100644 modules/gaia/shaders/gaia_tonemapping_vs.glsl create mode 100644 modules/gaia/shaders/gaia_vbo_vs.glsl create mode 100644 modules/gaia/tasks/constructoctreetask.cpp create mode 100644 modules/gaia/tasks/constructoctreetask.h create mode 100644 modules/gaia/tasks/readfilejob.cpp create mode 100644 modules/gaia/tasks/readfilejob.h create mode 100644 modules/gaia/tasks/readfitstask.cpp create mode 100644 modules/gaia/tasks/readfitstask.h create mode 100644 modules/gaia/tasks/readspecktask.cpp create mode 100644 modules/gaia/tasks/readspecktask.h create mode 100644 modules/space/translation/horizonstranslation.cpp create mode 100644 modules/space/translation/horizonstranslation.h diff --git a/.gitmodules b/.gitmodules index 1c979bcb37..4311e7851a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -20,3 +20,9 @@ [submodule "apps/OpenSpace/ext/sgct"] path = apps/OpenSpace/ext/sgct url = https://github.com/opensgct/sgct +[submodule "modules/fitsfilereader/ext/CCfits"] + path = modules/fitsfilereader/ext/CCfits + url = https://github.com/OpenSpace/CCfits.git +[submodule "modules/fitsfilereader/ext/cfitsio"] + path = modules/fitsfilereader/ext/cfitsio + url = https://github.com/OpenSpace/cfitsio.git diff --git a/data/assets/gaia.scene b/data/assets/gaia.scene new file mode 100644 index 0000000000..9e11b65718 --- /dev/null +++ b/data/assets/gaia.scene @@ -0,0 +1,39 @@ +local assetHelper = asset.require('util/asset_helper') + +asset.require('default') + +-- Augment default scene with gaia data, 3D model and trail +asset.require('scene/milkyway/gaia/gaiastars') +asset.require('scene/milkyway/gaia/apogee') +asset.require('scene/milkyway/gaia/galah') +asset.require('scene/solarsystem/missions/gaia/gaia') +asset.require('scene/solarsystem/missions/gaia/trail') + +assetHelper.registerDashboardItems(asset, { + { + Type = "DashboardItemDistance", + Identifier = "GaiaEarthDistance", + GuiName = "Gaia Earth Distance", + SourceType = "Node", + SourceNodeName = "Gaia", + DestinationType = "Node Surface", + DestinationNodeName = "Earth" + } +}) + +asset.onInitialize(function () + openspace.setPropertyValueSingle('Scene.Stars.Renderable.Enabled', false); + + openspace.markInterestingNodes({ "Gaia" }) + + openspace.navigation.setCameraState({ + Focus = "Gaia", + Position = { 1000000000000.0, 1000000000000.0, 1000000000000.0 }, + Rotation = { 0.683224, -0.765934, -0.601234, -0.418073 }, + }) +end) + +-- Remove interesting nodes and added virtual properties +asset.onDeinitialize(function () + openspace.removeInterestingNodes({ "Gaia" }) +end) diff --git a/data/assets/scene/milkyway/gaia/gaia_dr2_download_stars.asset b/data/assets/scene/milkyway/gaia/gaia_dr2_download_stars.asset new file mode 100644 index 0000000000..4dac2821f8 --- /dev/null +++ b/data/assets/scene/milkyway/gaia/gaia_dr2_download_stars.asset @@ -0,0 +1,19 @@ +-- Download a dataset of 618 million stars (28 GB), already preprocessed and stored in a binary octree. +-- The octree was generated from the full DR2 by filtering away all stars with a parallax error higher than 0.5 +-- Max Star Per Node = 50,000 and max distance = 500kpc +asset.syncedResource({ + Name = "Gaia DR2 618M Octree", + Type = "HttpSynchronization", + Identifier = "gaia_stars_618M_octree", + Version = 1 +}) + +-- Download the full DR2 dataset with 24 values per star (preprocessed with theReadFitsTask (gaia_read.task) into 8 binary files). +-- From these files new subsets can be created with the ConstructOctreeTask (gaia_octree.task). +-- Total size of download is 151 GB. +asset.syncedResource({ + Name = "Gaia DR2 Full Raw", + Type = "HttpSynchronization", + Identifier = "gaia_stars_dr2_raw", + Version = 1 +}) diff --git a/data/assets/scene/milkyway/gaia/gaiastars.asset b/data/assets/scene/milkyway/gaia/gaiastars.asset new file mode 100644 index 0000000000..2dd3a0083f --- /dev/null +++ b/data/assets/scene/milkyway/gaia/gaiastars.asset @@ -0,0 +1,60 @@ +local assetHelper = asset.require("util/asset_helper") + +local textures = asset.syncedResource({ + Name = "Stars Textures", + Type = "HttpSynchronization", + Identifier = "stars_textures", + Version = 1 +}) + +local colorLUT = asset.syncedResource({ + Name = "Stars Color Table", + Type = "HttpSynchronization", + Identifier = "stars_colormap", + Version = 1 +}) + +-- Download a preprocessed binary octree of Radial Velocity subset values per star (preprocessed into 8 binary files). +local starsFolder = asset.syncedResource({ + Name = "Gaia Stars RV", + Type = "HttpSynchronization", + Identifier = "gaia_stars_rv_octree", + Version = 1 +}) + +local GaiaStars = { + Identifier = "GaiaStars", + Renderable = { + Type = "RenderableGaiaStars", + File = starsFolder .. "/", + FileReaderOption = "StreamOctree", + RenderOption = "Motion", + ShaderOption = "Point_SSBO", + Texture = textures .. "/halo.png", + ColorMap = colorLUT .. "/colorbv.cmap", + LuminosityMultiplier = 35, + MagnitudeBoost = 25, + CutOffThreshold = 38, + BillboardSize = 1, + CloseUpBoostDist = 250, + Sharpness = 1.45, + LodPixelThreshold = 0, + MaxGpuMemoryPercent = 0.24, + MaxCpuMemoryPercent = 0.4, + FilterSize = 5, + Sigma = 0.5, + AdditionalNodes = {3.0, 2.0}, + FilterPosX = {0.0, 0.0}, + FilterPosY = {0.0, 0.0}, + FilterPosZ = {0.0, 0.0}, + FilterGMag = {20.0, 20.0}, + FilterBpRp = {0.0, 0.0}, + FilterDist = {9.0, 9.0}, + }, + GUI = { + Name = "Gaia Stars", + Path = "/Milky Way" + } +} + +assetHelper.registerSceneGraphNodesAndExport(asset, { GaiaStars }) diff --git a/data/assets/scene/solarsystem/missions/gaia/gaia.asset b/data/assets/scene/solarsystem/missions/gaia/gaia.asset new file mode 100644 index 0000000000..b5c6d6a18d --- /dev/null +++ b/data/assets/scene/solarsystem/missions/gaia/gaia.asset @@ -0,0 +1,67 @@ +local assetHelper = asset.require('util/asset_helper') +local transforms = asset.require('./transforms') +local sunTransforms = asset.require('scene/solarsystem/sun/transforms') + + +local textures = asset.syncedResource({ + Name = "Gaia Textures", + Type = "HttpSynchronization", + Identifier = "gaia_textures", + Version = 1 +}) + +local model = asset.syncedResource({ + Name = "Gaia Model", + Type = "HttpSynchronization", + Identifier = "gaia_model", + Version = 1 +}) + + +local Gaia = { + Identifier = "Gaia", + Parent = transforms.GaiaPosition.Identifier, + Transform = { + Rotation = { + Type = "FixedRotation", + Attached = "Gaia", + XAxis = { 1.0, 0.0, 0.0 }, + XAxisOrthogonal = true, + YAxis = "Sun", + YAxisInverted = true + }, + Scale = { + Type = "StaticScale", + Scale = 10.0 + } + }, + -- X Orthogonal + Renderable = { + Type = "RenderableModel", + Body = "GAIA", + Geometry = { + Type = "MultiModelGeometry", + GeometryFile = model .. "/gaia.obj" + }, + ColorTexture = textures .. "/gaia-baked.png", + LightSources = { + { + Type = "SceneGraphLightSource", + Identifier = "Sun", + Node = sunTransforms.SolarSystemBarycenter.Identifier, + Intensity = 0.3 + }, + { + Identifier = "Camera", + Type = "CameraLightSource", + Intensity = 0.4 + } + } + }, + GUI = { + Name = "Gaia", + Path = "/Solar System/Missions/Gaia" + } +} + +assetHelper.registerSceneGraphNodesAndExport(asset, { Gaia }) diff --git a/data/assets/scene/solarsystem/missions/gaia/trail.asset b/data/assets/scene/solarsystem/missions/gaia/trail.asset new file mode 100644 index 0000000000..0ca2041067 --- /dev/null +++ b/data/assets/scene/solarsystem/missions/gaia/trail.asset @@ -0,0 +1,64 @@ +local assetHelper = asset.require('util/asset_helper') +local earthTransforms = asset.require('scene/solarsystem/planets/earth/transforms') +local sunTransforms = asset.require('scene/solarsystem/sun/transforms') + +local trail = asset.syncedResource({ + Name = "Gaia Trail", + Type = "HttpSynchronization", + Identifier = "gaia_trail", + Version = 2 +}) + +local GaiaTrail = { + Identifier = "GaiaTrail", + Parent = earthTransforms.EarthBarycenter.Identifier, + Renderable = { + Type = "RenderableTrailTrajectory", + Enabled = false, + Translation = { + Type = "HorizonsTranslation", + HorizonsTextFile = trail .. "/gaia_orbit_horizons.dat" + }, + Color = { 0.0, 0.8, 0.7 }, + ShowFullTrail = false, + StartTime = "2013 DEC 19 09:55:10", + EndTime = "2019 JUN 20 05:55:10", + PointSize = 5, + SampleInterval = 12000, + TimeStampSubsampleFactor = 1, + EnableFade = false, + Rendering = "Lines" + }, + GUI = { + Name = "Gaia Trail", + Path = "/Solar System/Missions/Gaia" + } +} + +local GaiaTrailEclip = { + Identifier = "GaiaTrail_Eclip", + Parent = sunTransforms.SolarSystemBarycenter.Identifier, + Renderable = { + Type = "RenderableTrailTrajectory", + Enabled = false, + Translation = { + Type = "HorizonsTranslation", + HorizonsTextFile = trail .. "/gaia_orbit_horizons_sun.dat" + }, + Color = { 1.0, 0.0, 0.0 }, + ShowFullTrail = false, + StartTime = "2013 DEC 19 09:55:10", + EndTime = "2019 JUN 20 05:55:10", + PointSize = 5, + SampleInterval = 6000, + TimeStampSubsampleFactor = 1, + EnableFade = false, + Rendering = "Lines" + }, + GUI = { + Name = "Gaia Ecliptic Trail", + Path = "/Solar System/Missions/Gaia" + } +} + +assetHelper.registerSceneGraphNodesAndExport(asset, { GaiaTrail, GaiaTrailEclip } ) diff --git a/data/assets/scene/solarsystem/missions/gaia/transforms.asset b/data/assets/scene/solarsystem/missions/gaia/transforms.asset new file mode 100644 index 0000000000..a06ed09a95 --- /dev/null +++ b/data/assets/scene/solarsystem/missions/gaia/transforms.asset @@ -0,0 +1,27 @@ +local assetHelper = asset.require('util/asset_helper') +local earthTransforms = asset.require('scene/solarsystem/planets/earth/transforms') + + +local trail = asset.syncedResource({ + Name = "Gaia Trail", + Type = "HttpSynchronization", + Identifier = "gaia_trail", + Version = 1 +}) + +local GaiaPosition = { + Identifier = "GaiaPosition", + Parent = earthTransforms.EarthBarycenter.Identifier, + Transform = { + Translation = { + Type = "HorizonsTranslation", + HorizonsTextFile = trail .. "/gaia_orbit_horizons.dat" + }, + }, + GUI = { + Name = "Position", + Path = "/Solar System/Missions/Gaia" + } +} + +assetHelper.registerSceneGraphNodesAndExport(asset, { GaiaPosition }) diff --git a/data/tasks/gaia/gaia_download.task b/data/tasks/gaia/gaia_download.task new file mode 100644 index 0000000000..020a47ced1 --- /dev/null +++ b/data/tasks/gaia/gaia_download.task @@ -0,0 +1,6 @@ +return { + { + Type = "SyncAssetTask", + Asset = "scene/milkyway/gaia/gaia_dr2_download_stars" + } +} diff --git a/data/tasks/gaia/gaia_octree.task b/data/tasks/gaia/gaia_octree.task new file mode 100644 index 0000000000..a1262fef25 --- /dev/null +++ b/data/tasks/gaia/gaia_octree.task @@ -0,0 +1,45 @@ +local dataFolder = "E:/gaia_sync_data" +return { + { + Type = "ConstructOctreeTask", + InFileOrFolderPath = dataFolder .. "/Gaia_DR2_full_24columns/", + OutFileOrFolderPath = dataFolder .. "/DR2_full_Octree_test_50,50/", + MaxDist = 500, + MaxStarsPerNode = 50000, + SingleFileInput = false, + -- Specify filter thresholds + --FilterPosX = {0.0, 0.0}, + --FilterPosY = {0.0, 0.0}, + --FilterPosZ = {0.0, 0.0}, + FilterGMag = {20.0, 20.0}, + FilterBpRp = {0.0, 0.0}, + --FilterVelX = {0.0, 0.0}, + --FilterVelY = {0.0, 0.0}, + --FilterVelZ = {0.0, 0.0}, + --FilterBpMag = {20.0, 20.0}, + --FilterRpMag = {20.0, 20.0}, + --FilterBpG = {0.0, 0.0}, + --FilterGRp = {0.0, 0.0}, + --FilterRa = {0.0, 0.0}, + --FilterRaError = {0.0, 0.0}, + --FilterDec = {0.0, 0.0}, + --FilterDecError = {0.0, 0.0}, + FilterParallax = {0.01, 0.0}, + FilterParallaxError = {0.00001, 0.5}, + --FilterPmra = {0.0, 0.0}, + --FilterPmraError = {0.0, 0.0}, + --FilterPmdec = {0.0, 0.0}, + --FilterPmdecError = {0.0, 0.0}, + --FilterRv = {0.0, 0.0}, + --FilterRvError = {0.0, 0.0}, + }, + + -- { + -- Type = "ConstructOctreeTask", + -- InFileOrFolderPath = dataFolder .. "/AMNH/Binary/GaiaUMS.bin", + -- OutFileOrFolderPath = dataFolder .. "/AMNH/Octree/GaiaUMS_Octree.bin", + -- MaxDist = 10, + -- MaxStarsPerNode = 20000, + -- SingleFileInput = true, + -- }, +} diff --git a/data/tasks/gaia/gaia_read.task b/data/tasks/gaia/gaia_read.task new file mode 100644 index 0000000000..f1d89e5c0a --- /dev/null +++ b/data/tasks/gaia/gaia_read.task @@ -0,0 +1,16 @@ +local dataFolder = "E:/gaia_sync_data" +return { + { + Type = "ReadFitsTask", + InFileOrFolderPath = "L:/Gaia_DR2/gaia_source/fits/", + OutFileOrFolderPath = dataFolder .. "/Gaia_DR2_full_24columns/", + SingleFileProcess = false, + ThreadsToUse = 8, + }, + + --{ + -- Type = "ReadSpeckTask", + -- InFilePath = dataFolder .. "/AMNH/GaiaUMS/GaiaUMS.speck", + -- OutFilePath = dataFolder .. "/AMNH/Binary/GaiaUMS.bin", + --}, +} diff --git a/include/openspace/properties/propertyowner.h b/include/openspace/properties/propertyowner.h index b0e5b0a3b1..8a5a8f7d6e 100644 --- a/include/openspace/properties/propertyowner.h +++ b/include/openspace/properties/propertyowner.h @@ -160,6 +160,12 @@ public: */ bool hasProperty(const std::string& uri) const; + /** + * This method checks if a Property exists in this PropertyOwner. + * \return true if the Property existed, false otherwise. + */ + bool hasProperty(const Property* prop) const; + void setPropertyOwner(PropertyOwner* owner) { _owner = owner; } PropertyOwner* owner() const { return _owner; } diff --git a/include/openspace/util/distanceconversion.h b/include/openspace/util/distanceconversion.h index eff927ac92..16a528c3d3 100644 --- a/include/openspace/util/distanceconversion.h +++ b/include/openspace/util/distanceconversion.h @@ -289,6 +289,8 @@ constexpr double convertDistance(double meters, DistanceUnit requestedUnit) { } } +float convertMasPerYearToMeterPerSecond(float masPerYear, float parallax); + } // namespace openspace #endif // __OPENSPACE_CORE___DISTANCECONVERSION___H__ diff --git a/modules/base/CMakeLists.txt b/modules/base/CMakeLists.txt index 117b53a7f7..98829cd19b 100644 --- a/modules/base/CMakeLists.txt +++ b/modules/base/CMakeLists.txt @@ -39,6 +39,7 @@ set(HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/lightsource/scenegraphlightsource.h ${CMAKE_CURRENT_SOURCE_DIR}/rendering/modelgeometry.h ${CMAKE_CURRENT_SOURCE_DIR}/rendering/multimodelgeometry.h + ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderableboxgrid.h ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderablecartesianaxes.h ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderablemodel.h ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderableplane.h @@ -82,6 +83,7 @@ set(SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/lightsource/scenegraphlightsource.cpp ${CMAKE_CURRENT_SOURCE_DIR}/rendering/modelgeometry.cpp ${CMAKE_CURRENT_SOURCE_DIR}/rendering/multimodelgeometry.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderableboxgrid.cpp ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderablecartesianaxes.cpp ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderablemodel.cpp ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderableplane.cpp diff --git a/modules/base/basemodule.cpp b/modules/base/basemodule.cpp index be13023c2c..186149a215 100644 --- a/modules/base/basemodule.cpp +++ b/modules/base/basemodule.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -117,6 +118,7 @@ void BaseModule::internalInitialize(const ghoul::Dictionary&) { auto fRenderable = FactoryManager::ref().factory(); ghoul_assert(fRenderable, "Renderable factory was not created"); + fRenderable->registerClass("RenderableBoxGrid"); fRenderable->registerClass("RenderableCartesianAxes"); fRenderable->registerClass("RenderableModel"); fRenderable->registerClass("RenderablePlaneImageLocal"); @@ -181,6 +183,7 @@ std::vector BaseModule::documentations() const { DashboardItemSpacing::Documentation(), DashboardItemVelocity::Documentation(), + RenderableBoxGrid::Documentation(), RenderableModel::Documentation(), RenderablePlane::Documentation(), RenderableSphere::Documentation(), diff --git a/modules/base/rendering/renderableboxgrid.cpp b/modules/base/rendering/renderableboxgrid.cpp new file mode 100644 index 0000000000..0253237d22 --- /dev/null +++ b/modules/base/rendering/renderableboxgrid.cpp @@ -0,0 +1,320 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr const char* ProgramName = "GridProgram"; + + constexpr openspace::properties::Property::PropertyInfo GridColorInfo = { + "GridColor", + "Grid Color", + "This value determines the color of the grid lines that are rendered." + }; + + constexpr openspace::properties::Property::PropertyInfo GridMatrixInfo = { + "GridMatrix", + "Grid Matrix", + "This value specifies the local transformation matrix that defines the " + "orientation of this grid relative to the parent's rotation." + }; + + constexpr openspace::properties::Property::PropertyInfo SegmentsInfo = { + "Segments", + "Number of Segments", + "This value specifies the number of segments that are used to render the " + "surrounding sphere." + }; + + constexpr openspace::properties::Property::PropertyInfo LineWidthInfo = { + "LineWidth", + "Line Width", + "This value specifies the line width of the spherical grid." + }; + + constexpr openspace::properties::Property::PropertyInfo SizeInfo = { + "Size", + "Grid Size", + "This value species the size of each dimensions of the box" + }; +} // namespace + +namespace openspace { + +documentation::Documentation RenderableBoxGrid::Documentation() { + using namespace documentation; + return { + "RenderableSphericalGrid", + "base_renderable_sphericalgrid", + { + { + GridMatrixInfo.identifier, + new DoubleMatrix4x4Verifier, + Optional::Yes, + GridMatrixInfo.description + }, + { + GridColorInfo.identifier, + new DoubleVector4Verifier, + Optional::Yes, + GridColorInfo.description + }, + { + SegmentsInfo.identifier, + new IntVerifier, + Optional::Yes, + SegmentsInfo.description + }, + { + LineWidthInfo.identifier, + new DoubleVerifier, + Optional::Yes, + LineWidthInfo.description + }, + { + SizeInfo.identifier, + new DoubleVector3Verifier, + Optional::Yes, + SizeInfo.description + } + } + }; +} + + +RenderableBoxGrid::RenderableBoxGrid(const ghoul::Dictionary& dictionary) + : Renderable(dictionary) + , _gridMatrix(GridMatrixInfo, glm::mat4(1.f)) + , _gridColor( + GridColorInfo, + glm::vec4(0.5f, 0.5, 0.5f, 1.f), + glm::vec4(0.f), + glm::vec4(1.f) + ) + , _segments(SegmentsInfo, 36, 4, 200) + , _lineWidth(LineWidthInfo, 0.5f, 0.f, 20.f) + , _size(SizeInfo, glm::vec3(1e20f), glm::vec3(1.f), glm::vec3(1e35f)) +{ + documentation::testSpecificationAndThrow( + Documentation(), + dictionary, + "RenderableBoxGrid" + ); + + addProperty(_opacity); + registerUpdateRenderBinFromOpacity(); + + if (dictionary.hasKey(GridMatrixInfo.identifier)) { + _gridMatrix = dictionary.value(GridMatrixInfo.identifier); + } + addProperty(_gridMatrix); + + if (dictionary.hasKey(GridColorInfo.identifier)) { + _gridColor = dictionary.value(GridColorInfo.identifier); + } + _gridColor.setViewOption(properties::Property::ViewOptions::Color); + addProperty(_gridColor); + + if (dictionary.hasKey(SegmentsInfo.identifier)) { + _segments = static_cast(dictionary.value(SegmentsInfo.identifier)); + } + _segments.onChange([&]() { _gridIsDirty = true; }); + addProperty(_segments); + + if (dictionary.hasKey(LineWidthInfo.identifier)) { + _lineWidth = static_cast( + dictionary.value(LineWidthInfo.identifier) + ); + } + addProperty(_lineWidth); + + if (dictionary.hasKey(SizeInfo.identifier)) { + _size = dictionary.value(SizeInfo.identifier); + } + _size.onChange([&]() { _gridIsDirty = true; }); + addProperty(_size); +} + +bool RenderableBoxGrid::isReady() const { + return _gridProgram != nullptr; +} + +void RenderableBoxGrid::initializeGL() { + _gridProgram = BaseModule::ProgramObjectManager.request( + ProgramName, + []() -> std::unique_ptr { + return global::renderEngine.buildRenderProgram( + ProgramName, + absPath("${MODULE_BASE}/shaders/grid_vs.glsl"), + absPath("${MODULE_BASE}/shaders/grid_fs.glsl") + ); + } + ); + + glGenVertexArrays(1, &_vaoID); + glGenBuffers(1, &_vBufferID); + + glBindVertexArray(_vaoID); + glBindBuffer(GL_ARRAY_BUFFER, _vBufferID); + glEnableVertexAttribArray(0); + glBindVertexArray(0); +} + +void RenderableBoxGrid::deinitializeGL() { + glDeleteVertexArrays(1, &_vaoID); + _vaoID = 0; + + glDeleteBuffers(1, &_vBufferID); + _vBufferID = 0; + + BaseModule::ProgramObjectManager.release( + ProgramName, + [](ghoul::opengl::ProgramObject* p) { + global::renderEngine.removeRenderProgram(p); + } + ); + _gridProgram = nullptr; +} + +void RenderableBoxGrid::render(const RenderData& data, RendererTasks&){ + _gridProgram->activate(); + + _gridProgram->setUniform("opacity", _opacity); + + glm::dmat4 modelTransform = + glm::translate(glm::dmat4(1.0), data.modelTransform.translation) * // Translation + glm::dmat4(data.modelTransform.rotation) * // Spice rotation + glm::scale(glm::dmat4(1.0), glm::dvec3(data.modelTransform.scale)); + + glm::dmat4 modelViewTransform = data.camera.combinedViewMatrix() * modelTransform; + + _gridProgram->setUniform("modelViewTransform", glm::mat4(modelViewTransform)); + _gridProgram->setUniform("projectionTransform", data.camera.projectionMatrix()); + + _gridProgram->setUniform("gridColor", _gridColor); + + glLineWidth(_lineWidth); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + glEnable(GL_LINE_SMOOTH); + + glBindVertexArray(_vaoID); + glDrawArrays(_mode, 0, _varray.size()); + glBindVertexArray(0); + + _gridProgram->deactivate(); +} + +void RenderableBoxGrid::update(const UpdateData&) { + if (_gridIsDirty) { + //_vsize = (_segments + 1) * (_segments + 1); + //_varray.resize(_vsize); + + const glm::vec3 llf = -_size.value() / 2.f; + const glm::vec3 urb = _size.value() / 2.f; + + // 7 + // -------------------- 6 + // / / + // /| /| + // 4 / | / | + // x-------------------x | + // | | |5 | + // | | | | + // | | | | + // | 3/----------------|--/ 2 + // | / | / + // |/ |/ + // x-------------------x + // 0 1 + // + // + // For Line strip: + // 0 -> 1 -> 2 -> 3 -> 0 -> 4 -> 5 -> 6 -> 7 -> 4 -> 5(d) -> 1 -> 2(d) -> 6 + // -> 7(d) -> 3 + + const glm::vec3 v0 = { llf.x, llf.y, llf.z }; + const glm::vec3 v1 = { urb.x, llf.y, llf.z }; + const glm::vec3 v2 = { urb.x, urb.y, llf.z }; + const glm::vec3 v3 = { llf.x, urb.y, llf.z }; + const glm::vec3 v4 = { llf.x, llf.y, urb.z }; + const glm::vec3 v5 = { urb.x, llf.y, urb.z }; + const glm::vec3 v6 = { urb.x, urb.y, urb.z }; + const glm::vec3 v7 = { llf.x, urb.y, urb.z }; + + // First add the bounds + _varray.push_back({ v0.x, v0.y, v0.z }); + _varray.push_back({ v1.x, v1.y, v1.z }); + _varray.push_back({ v2.x, v2.y, v2.z }); + _varray.push_back({ v3.x, v3.y, v3.z }); + _varray.push_back({ v0.x, v0.y, v0.z }); + _varray.push_back({ v4.x, v4.y, v4.z }); + _varray.push_back({ v5.x, v5.y, v5.z }); + _varray.push_back({ v6.x, v6.y, v6.z }); + _varray.push_back({ v7.x, v7.y, v7.z }); + _varray.push_back({ v4.x, v4.y, v4.z }); + _varray.push_back({ v5.x, v5.y, v5.z }); + _varray.push_back({ v1.x, v1.y, v1.z }); + _varray.push_back({ v2.x, v2.y, v2.z }); + _varray.push_back({ v6.x, v6.y, v6.z }); + _varray.push_back({ v7.x, v7.y, v7.z }); + _varray.push_back({ v3.x, v3.y, v3.z }); + + + glBindVertexArray(_vaoID); + glBindBuffer(GL_ARRAY_BUFFER, _vBufferID); + glBufferData( + GL_ARRAY_BUFFER, + _varray.size() * sizeof(Vertex), + _varray.data(), + GL_STATIC_DRAW + ); + + glVertexAttribPointer( + 0, + 3, + GL_FLOAT, + GL_FALSE, + sizeof(Vertex), + nullptr // = reinterpret_cast(offsetof(Vertex, location)) + ); + + glBindVertexArray(0); + + _gridIsDirty = false; + } +} + +} // namespace openspace diff --git a/modules/base/rendering/renderableboxgrid.h b/modules/base/rendering/renderableboxgrid.h new file mode 100644 index 0000000000..1deb095f79 --- /dev/null +++ b/modules/base/rendering/renderableboxgrid.h @@ -0,0 +1,83 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_BASE___RENDERABLEBOXGRID___H__ +#define __OPENSPACE_MODULE_BASE___RENDERABLEBOXGRID___H__ + +#include + +#include +#include +#include +#include +#include +#include + +namespace ghoul::opengl { + class ProgramObject; +} // namespace ghoul::opengl + +namespace openspace::documentation { struct Documentation; } + +namespace openspace { + +class RenderableBoxGrid : public Renderable { +public: + RenderableBoxGrid(const ghoul::Dictionary& dictionary); + + void initializeGL() override; + void deinitializeGL() override; + + bool isReady() const override; + + void render(const RenderData& data, RendererTasks& rendererTask) override; + void update(const UpdateData& data) override; + + static documentation::Documentation Documentation(); + +protected: + struct Vertex { + float location[3]; + }; + + ghoul::opengl::ProgramObject* _gridProgram = nullptr; + + properties::DMat4Property _gridMatrix; + properties::Vec4Property _gridColor; + properties::IntProperty _segments; + properties::FloatProperty _lineWidth; + properties::Vec3Property _size; + + bool _gridIsDirty = true; + + GLuint _vaoID = 0; + GLuint _vBufferID = 0; + + GLenum _mode = GL_LINE_STRIP; + std::vector _varray; +}; + +}// namespace openspace + +#endif // __OPENSPACE_MODULE_BASE___RENDERABLEBOXGRID___H__ diff --git a/modules/base/rotation/fixedrotation.cpp b/modules/base/rotation/fixedrotation.cpp index b01ee117c0..f7ec1bb3fb 100644 --- a/modules/base/rotation/fixedrotation.cpp +++ b/modules/base/rotation/fixedrotation.cpp @@ -217,6 +217,12 @@ documentation::Documentation FixedRotation::Documentation() { Optional::Yes, XAxisOrthogonalVectorInfo.description }, + { + XAxisInvertObjectInfo.identifier, + new BoolVerifier, + Optional::Yes, + XAxisInvertObjectInfo.description + }, { KeyYAxis, new OrVerifier({ new StringVerifier, new DoubleVector3Verifier, }), @@ -234,6 +240,12 @@ documentation::Documentation FixedRotation::Documentation() { Optional::Yes, YAxisOrthogonalVectorInfo.description }, + { + YAxisInvertObjectInfo.identifier, + new BoolVerifier, + Optional::Yes, + YAxisInvertObjectInfo.description + }, { KeyZAxis, new OrVerifier({ new StringVerifier, new DoubleVector3Verifier, }), @@ -251,6 +263,12 @@ documentation::Documentation FixedRotation::Documentation() { Optional::Yes, ZAxisOrthogonalVectorInfo.description }, + { + ZAxisInvertObjectInfo.identifier, + new BoolVerifier, + Optional::Yes, + ZAxisInvertObjectInfo.description + }, { AttachedInfo.identifier, new StringVerifier, @@ -454,6 +472,11 @@ bool FixedRotation::initialize() { if (_constructorDictionary.hasKey(KeyXAxisOrthogonal)) { _xAxis.isOrthogonal = _constructorDictionary.value(KeyXAxisOrthogonal); } + if (_constructorDictionary.hasKey(XAxisInvertObjectInfo.identifier)) { + _xAxis.invertObject = _constructorDictionary.value( + XAxisInvertObjectInfo.identifier + ); + } if (_xAxis.isOrthogonal) { _xAxis.type = Axis::Type::OrthogonalVector; } @@ -474,6 +497,11 @@ bool FixedRotation::initialize() { if (_constructorDictionary.hasKey(KeyYAxisOrthogonal)) { _yAxis.isOrthogonal = _constructorDictionary.value(KeyYAxisOrthogonal); } + if (_constructorDictionary.hasKey(YAxisInvertObjectInfo.identifier)) { + _yAxis.invertObject = _constructorDictionary.value( + YAxisInvertObjectInfo.identifier + ); + } if (_yAxis.isOrthogonal) { _yAxis.type = Axis::Type::OrthogonalVector; } @@ -494,12 +522,16 @@ bool FixedRotation::initialize() { if (_constructorDictionary.hasKey(KeyZAxisOrthogonal)) { _zAxis.isOrthogonal = _constructorDictionary.value(KeyZAxisOrthogonal); } + if (_constructorDictionary.hasKey(ZAxisInvertObjectInfo.identifier)) { + _yAxis.invertObject = _constructorDictionary.value( + ZAxisInvertObjectInfo.identifier + ); + } if (_zAxis.isOrthogonal) { _zAxis.type = Axis::Type::OrthogonalVector; } - if (!hasXAxis && hasYAxis && hasZAxis) { _xAxis.type = Axis::Type::CoordinateSystemCompletion; } @@ -554,11 +586,14 @@ glm::vec3 FixedRotation::xAxis() const { return glm::vec3(1.f, 0.f, 0.f); case Axis::Type::Object: if (_xAxis.node && _attachedNode) { - glm::vec3 dir = glm::vec3(glm::normalize( - _xAxis.node->worldPosition() - - _attachedNode->worldPosition() - )); - return _xAxis.invertObject ? -dir : dir; + glm::dvec3 dir = _xAxis.node->worldPosition() - + _attachedNode->worldPosition(); + + if (dir == glm::dvec3(0.0)) { + dir = glm::dvec3(1.0, 0.0, 0.0); + } + glm::vec3 dirNorm = glm::vec3(glm::normalize(dir)); + return _xAxis.invertObject ? -dirNorm : dirNorm; } else { if (_xAxis.node) { diff --git a/modules/fitsfilereader/CMakeLists.txt b/modules/fitsfilereader/CMakeLists.txt new file mode 100644 index 0000000000..566d7ab6cb --- /dev/null +++ b/modules/fitsfilereader/CMakeLists.txt @@ -0,0 +1,56 @@ +########################################################################################## +# # +# OpenSpace # +# # +# Copyright (c) 2014-2018 # +# # +# 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(${OPENSPACE_CMAKE_EXT_DIR}/module_definition.cmake) + +set(HEADER_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/fitsfilereadermodule.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/fitsfilereader.h +) +source_group("Header Files" FILES ${HEADER_FILES}) + +set(SOURCE_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/fitsfilereadermodule.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/fitsfilereader.cpp +) +source_group("Source Files" FILES ${SOURCE_FILES}) + +create_new_module( + "FitsFileReader" + fitsfilereader + ${HEADER_FILES} + ${SOURCE_FILES}) + +# Set root directories for external libraries. +set(CFITSIO_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/cfitsio/") +set(CCFITS_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/CCfits/") +set(INCLUDES_FOR_TARGET ${CCFITS_ROOT_DIR} "${CCFITS_ROOT_DIR}/../" ${CFITSIO_ROOT_DIR}) +set(MODULE_NAME openspace-module-fitsfilereader) + +# CCfits is dependent on cfitsio, let it handle the internal linking +add_subdirectory(${CFITSIO_ROOT_DIR}) +add_subdirectory(${CCFITS_ROOT_DIR}) + +TARGET_INCLUDE_DIRECTORIES(${MODULE_NAME} SYSTEM PUBLIC ${INCLUDES_FOR_TARGET}) +TARGET_LINK_LIBRARIES(${MODULE_NAME} CCfits) diff --git a/modules/fitsfilereader/ext/CCfits b/modules/fitsfilereader/ext/CCfits new file mode 160000 index 0000000000..48400ae262 --- /dev/null +++ b/modules/fitsfilereader/ext/CCfits @@ -0,0 +1 @@ +Subproject commit 48400ae26288af3f8d1854929aea6c38749cb483 diff --git a/modules/fitsfilereader/ext/cfitsio b/modules/fitsfilereader/ext/cfitsio new file mode 160000 index 0000000000..b267f17603 --- /dev/null +++ b/modules/fitsfilereader/ext/cfitsio @@ -0,0 +1 @@ +Subproject commit b267f17603ae62fe8c59aaef6ec9709063ff01d5 diff --git a/modules/fitsfilereader/fitsfilereadermodule.cpp b/modules/fitsfilereader/fitsfilereadermodule.cpp new file mode 100644 index 0000000000..1e3e27a904 --- /dev/null +++ b/modules/fitsfilereader/fitsfilereadermodule.cpp @@ -0,0 +1,31 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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 + +namespace openspace { + +FitsFileReaderModule::FitsFileReaderModule() : OpenSpaceModule(Name) {} + +} // namespace openspace diff --git a/modules/fitsfilereader/fitsfilereadermodule.h b/modules/fitsfilereader/fitsfilereadermodule.h new file mode 100644 index 0000000000..e4698ba54d --- /dev/null +++ b/modules/fitsfilereader/fitsfilereadermodule.h @@ -0,0 +1,41 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_FITSFILEREADER___FITSFILEREADERMODULE___H__ +#define __OPENSPACE_MODULE_FITSFILEREADER___FITSFILEREADERMODULE___H__ + +#include + +namespace openspace { + +class FitsFileReaderModule : public OpenSpaceModule { +public: + constexpr static const char* Name = "FitsFileReader"; + + FitsFileReaderModule(); +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_FITSFILEREADER___FITSFILEREADERMODULE___H__ diff --git a/modules/fitsfilereader/include.cmake b/modules/fitsfilereader/include.cmake new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/fitsfilereader/include/fitsfilereader.h b/modules/fitsfilereader/include/fitsfilereader.h new file mode 100644 index 0000000000..bba4e9f7a1 --- /dev/null +++ b/modules/fitsfilereader/include/fitsfilereader.h @@ -0,0 +1,116 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_FITSFILEREADER___FITSFILEREADER___H__ +#define __OPENSPACE_MODULE_FITSFILEREADER___FITSFILEREADER___H__ + +#include +#include +#include +#include +#include +#include + +namespace CCfits { + class FITS; + class PHDU; + class ExtHDU; +} // namespace CCfits + +namespace ghoul::opengl { class Texture; } + +namespace openspace { + +template +struct ImageData { + std::valarray contents; + long int width; + long int height; +}; + +template +struct TableData { + std::unordered_map> contents; + int readRows; + long int optimalRowsize; + std::string name; +}; + +class FitsFileReader { +public: + FitsFileReader(bool verboseMode); + ~FitsFileReader(); + + template + std::shared_ptr> readImage(const std::string& path); + + template + std::shared_ptr> readHeader( + std::vector& keywords); + template + std::shared_ptr readHeaderValue(const std::string key); + + /** + * Read specified table columns from fits file. + * If readAll is set to true the entire table will be read before the + * selected columns, which makes the function take a lot longer if it's a big file. + * If no HDU index is given the current Extension HDU will be read from. + */ + template + std::shared_ptr> readTable(std::string& path, + const std::vector& columnNames, int startRow = 1, int endRow = 10, + int hduIdx = 1, bool readAll = false); + + /** + * Reads a single FITS file with pre-defined columns (defined for Viennas TGAS-file). + * Returns a vector with all read stars with nValuesPerStar. + * If additional columns are given by filterColumnNames, they will be + * read but it will slow doen the reading tremendously. + */ + std::vector readFitsFile(std::string filePath, int& nValuesPerStar, + int firstRow, int lastRow, std::vector filterColumnNames, + int multiplier = 1); + + /** + * Reads a single SPECK file and returns a vector with nRenderValues + * per star. Reads data in pre-defined order based on AMNH's star data files. + */ + std::vector readSpeckFile(std::string filePath, int& nRenderValues); + +private: + std::unique_ptr _infile; + bool _verboseMode; + + bool isPrimaryHDU(); + template + const std::shared_ptr> readImageInternal(CCfits::PHDU& image); + template + const std::shared_ptr> readImageInternal(CCfits::ExtHDU& image); + + mutable std::mutex _mutex; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_FITSFILEREADER___FITSFILEREADER___H__ diff --git a/modules/fitsfilereader/src/fitsfilereader.cpp b/modules/fitsfilereader/src/fitsfilereader.cpp new file mode 100644 index 0000000000..e0cff969dd --- /dev/null +++ b/modules/fitsfilereader/src/fitsfilereader.cpp @@ -0,0 +1,668 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include + +using namespace CCfits; + +namespace { + constexpr const char* _loggerCat = "FitsFileReader"; +} // namespace + +namespace openspace { + +FitsFileReader::FitsFileReader(bool verboseMode) { + _verboseMode = verboseMode; + FITS::setVerboseMode(_verboseMode); +} + +FitsFileReader::~FitsFileReader() { + if (_infile) { + _infile->destroy(); + _infile = nullptr; + } +} + +bool FitsFileReader::isPrimaryHDU() { + return _infile->extension().size() == 0; +} + +template +std::shared_ptr> FitsFileReader::readImage(const std::string& path) { + try { + _infile = std::make_unique(path, Read, true); + // Primary HDU Object + if (isPrimaryHDU()) { + return readImageInternal(_infile->pHDU()); + } + // Extension HDU Object + return readImageInternal(_infile->currentExtension()); + } catch (const FitsException& e){ + LERROR("Could not read FITS image from table. " + e.message() ); + } + + return nullptr; +} + +template +std::shared_ptr> FitsFileReader::readHeader( + std::vector& keywords) +{ + try { + HDU& image = isPrimaryHDU() ? + static_cast(_infile->pHDU()) : + static_cast(_infile->currentExtension()); + + std::vector values; + image.readKeys(keywords, values); + + if (values.size() != keywords.size()) { + LERROR("Number of keywords does not match number of values"); + } + + std::unordered_map result; + std::transform( + keywords.begin(), + keywords.end(), + values.begin(), + std::inserter( + result, + result.end() + ), + [](std::string key, T value) { return std::make_pair(key, value); } + ); + return std::make_shared>(std::move(result)); + } catch (const FitsException& e) { + LERROR("Could not read FITS header. " + e.message() ); + } + return nullptr; +} + +template +std::shared_ptr FitsFileReader::readHeaderValue(const std::string key) { + try { + HDU& image = isPrimaryHDU() ? + static_cast(_infile->pHDU()) : + static_cast(_infile->currentExtension()); + + T value; + image.readKey(key, value); + return std::make_unique(value); + } catch (FitsException& e) { + LERROR("Could not read FITS key. " + e.message() ); + } + return nullptr; +} + +template +std::shared_ptr> FitsFileReader::readTable(std::string& path, + const std::vector& columnNames, + int startRow, + int endRow, + int hduIdx, + bool readAll) +{ + // We need to lock reading when using multithreads because CCfits can't handle + // multiple I/O drivers. + std::lock_guard g(_mutex); + + try { + _infile = std::make_unique(path, Read, readAll); + + // Make sure FITS file is not a Primary HDU Object (aka an image). + if (!isPrimaryHDU()) { + ExtHDU& table = _infile->extension(hduIdx); + int numCols = columnNames.size(); + int numRowsInTable = table.rows(); + std::unordered_map> contents; + //LINFO("Read file: " + _infile->name()); + + int firstRow = std::max(startRow, 1); + + if (endRow < firstRow) { + endRow = numRowsInTable; + } + + for (int i = 0; i < numCols; ++i) { + std::vector columnData; + //LINFO("Read column: " + columnNames[i]); + table.column(columnNames[i]).read(columnData, firstRow, endRow); + contents[columnNames[i]] = columnData; + } + + // Create TableData object of table contents. + TableData loadedTable = { + std::move(contents), + static_cast(table.rows()), + table.getRowsize(), + table.name() + }; + + return std::make_shared>(loadedTable); + } + } + catch (FitsException& e) { + LERROR(fmt::format( + "Could not read FITS table from file '{}'. Make sure it's not an image file.", + e.message() + )); + } + return nullptr; +} + +std::vector FitsFileReader::readFitsFile(std::string filePath, int& nValuesPerStar, + int firstRow, int lastRow, + std::vector filterColumnNames, + int multiplier) +{ + std::vector fullData; + srand(1234567890); + if (firstRow <= 0) { + firstRow = 1; + } + + // Define what columns to read. + std::vector allColumnNames = { + "Position_X", + "Position_Y", + "Position_Z", + "Velocity_X", + "Velocity_Y", + "Velocity_Z", + "Gaia_Parallax", + "Gaia_G_Mag", + "Tycho_B_Mag", + "Tycho_V_Mag", + "Gaia_Parallax_Err", + "Gaia_Proper_Motion_RA", + "Gaia_Proper_Motion_RA_Err", + "Gaia_Proper_Motion_Dec", + "Gaia_Proper_Motion_Dec_Err", + "Tycho_B_Mag_Err", + "Tycho_V_Mag_Err" + }; + + // Append additional filter parameters to default rendering parameters. + allColumnNames.insert( + allColumnNames.end(), + filterColumnNames.begin(), + filterColumnNames.end() + ); + + std::string allNames = "Columns to read: \n"; + for (const std::string& colName : allColumnNames) { + allNames += colName + "\n"; + } + LINFO(allNames); + + // Read columns from FITS file. If rows aren't specified then full table will be read. + std::shared_ptr> table = readTable( + filePath, + allColumnNames, + firstRow, + lastRow + ); + + if (!table) { + throw ghoul::RuntimeError(fmt::format("Failed to open Fits file '{}'", filePath)); + } + + int nStars = table->readRows - firstRow + 1; + + int nNullArr = 0; + size_t nColumnsRead = allColumnNames.size(); + size_t defaultCols = 17; // Number of columns that are copied by predefined code. + if (nColumnsRead != defaultCols) { + LINFO("Additional columns will be read! Consider add column in code for " + "significant speedup!"); + } + // Declare how many values to save per star + nValuesPerStar = nColumnsRead + 1; // +1 for B-V color value. + + // Copy columns to local variables. + std::unordered_map>& tableContent = table->contents; + + // Default render parameters! + std::vector posXcol = std::move(tableContent[allColumnNames[0]]); + std::vector posYcol = std::move(tableContent[allColumnNames[1]]); + std::vector posZcol = std::move(tableContent[allColumnNames[2]]); + std::vector velXcol = std::move(tableContent[allColumnNames[3]]); + std::vector velYcol = std::move(tableContent[allColumnNames[4]]); + std::vector velZcol = std::move(tableContent[allColumnNames[5]]); + std::vector parallax = std::move(tableContent[allColumnNames[6]]); + std::vector magCol = std::move(tableContent[allColumnNames[7]]); + std::vector tycho_b = std::move(tableContent[allColumnNames[8]]); + std::vector tycho_v = std::move(tableContent[allColumnNames[9]]); + + // Default filter parameters + // Additional filter parameters are handled as well but slows down reading + std::vector parallax_err = std::move(tableContent[allColumnNames[10]]); + std::vector pr_mot_ra = std::move(tableContent[allColumnNames[11]]); + std::vector pr_mot_ra_err = std::move(tableContent[allColumnNames[12]]); + std::vector pr_mot_dec = std::move(tableContent[allColumnNames[13]]); + std::vector pr_mot_dec_err = std::move(tableContent[allColumnNames[14]]); + std::vector tycho_b_err = std::move(tableContent[allColumnNames[15]]); + std::vector tycho_v_err = std::move(tableContent[allColumnNames[16]]); + + // Construct data array. OBS: ORDERING IS IMPORTANT! This is where slicing happens. + for (int i = 0; i < nStars * multiplier; ++i) { + std::vector values(nValuesPerStar); + size_t idx = 0; + + // Default order for rendering: + // Position [X, Y, Z] + // Absolute Magnitude + // B-V Color + // Velocity [X, Y, Z] + + // Store positions. + values[idx++] = posXcol[i % nStars]; + values[idx++] = posYcol[i % nStars]; + values[idx++] = posZcol[i % nStars]; + + // Return early if star doesn't have a measured position. + if (values[0] == -999 && values[1] == -999 && values[2] == -999) { + nNullArr++; + continue; + } + + // Store color values. + values[idx++] = magCol[i % nStars] == -999 ? 20.f : magCol[i % nStars]; + values[idx++] = tycho_b[i % nStars] - tycho_v[i % nStars]; + + // Store velocity. Convert it to m/s with help by parallax. + values[idx++] = convertMasPerYearToMeterPerSecond( + velXcol[i % nStars], + parallax[i % nStars] + ); + values[idx++] = convertMasPerYearToMeterPerSecond( + velYcol[i % nStars], + parallax[i % nStars] + ); + values[idx++] = convertMasPerYearToMeterPerSecond( + velZcol[i % nStars], + parallax[i % nStars] + ); + + // Store additional parameters to filter by. + values[idx++] = parallax[i % nStars]; + values[idx++] = parallax_err[i % nStars]; + values[idx++] = pr_mot_ra[i % nStars]; + values[idx++] = pr_mot_ra_err[i % nStars]; + values[idx++] = pr_mot_dec[i % nStars]; + values[idx++] = pr_mot_dec_err[i % nStars]; + values[idx++] = tycho_b[i % nStars]; + values[idx++] = tycho_b_err[i % nStars]; + values[idx++] = tycho_v[i % nStars]; + values[idx++] = tycho_v_err[i % nStars]; + + // Read extra columns, if any. This will slow down the sorting tremendously! + for (size_t col = defaultCols; col < nColumnsRead; ++col) { + std::vector vecData = std::move(tableContent[allColumnNames[col]]); + values[idx++] = vecData[i]; + } + + for (size_t j = 0; j < nValuesPerStar; ++j) { + // The astronomers in Vienna use -999 as default value. Change it to 0. + if (values[j] == -999) { + values[j] = 0.f; + } + else if (multiplier > 1) { + values[j] *= static_cast(rand()) / static_cast(RAND_MAX); + } + } + + fullData.insert(fullData.end(), values.begin(), values.end()); + } + + // Define what columns to read. + /*auto allColumnNames = std::vector({ + "ra", + "dec", + "parallax", + "pmra", + "pmdec", + "phot_g_mean_mag", + "phot_bp_mean_mag", + "phot_rp_mean_mag", + "radial_velocity", + }); + // Append additional filter parameters to default rendering parameters. + allColumnNames.insert(allColumnNames.end(), filterColumnNames.begin(), + filterColumnNames.end()); + + std::string allNames = "Columns to read: \n"; + for (auto colName : allColumnNames) { + allNames += colName + "\n"; + } + LINFO(allNames); + + // Read columns from FITS file. If rows aren't specified then full table will be read. + auto table = readTable(filePath, allColumnNames, firstRow, lastRow); + + if (!table) { + throw ghoul::RuntimeError(fmt::format("Failed to open Fits file '{}'", filePath)); + } + + int nStars = table->readRows - firstRow + 1; + + int nNullArr = 0; + size_t nColumnsRead = allColumnNames.size(); + size_t defaultCols = 9; // Number of columns that are copied by predefined code. + if (nColumnsRead != defaultCols) { + LINFO("Additional columns will be read! Consider add column in code for " + "significant speedup!"); + } + // Declare how many values to save per star + nValuesPerStar = 8; + + // Copy columns to local variables. + std::unordered_map>& tableContent = table->contents; + + std::vector ra = std::move(tableContent[allColumnNames[0]]); + std::vector dec = std::move(tableContent[allColumnNames[1]]); + std::vector parallax = std::move(tableContent[allColumnNames[2]]); + std::vector pmra = std::move(tableContent[allColumnNames[3]]); + std::vector pmdec = std::move(tableContent[allColumnNames[4]]); + std::vector meanMagG = std::move(tableContent[allColumnNames[5]]); + std::vector meanMagBp = std::move(tableContent[allColumnNames[6]]); + std::vector meanMagRp = std::move(tableContent[allColumnNames[7]]); + std::vector radial_vel = std::move(tableContent[allColumnNames[8]]); + + // Construct data array. OBS: ORDERING IS IMPORTANT! This is where slicing happens. + for (int i = 0; i < nStars; ++i) { + std::vector values(nValuesPerStar); + size_t idx = 0; + + // Default order for rendering: + // Position [X, Y, Z] + // Mean G-band Magnitude + // Bp-Rp Color + // Velocity [X, Y, Z] + + // Return early if star doesn't have a measured position. + if (std::isnan(ra[i]) || std::isnan(dec[i])) { + nNullArr++; + continue; + } + + // Store positions. Set to a default distance if parallax doesn't exist. + float radiusInKiloParsec = 9.0; + if (!std::isnan(parallax[i])) { + // Parallax is in milliArcseconds -> distance in kiloParsecs + // https://gea.esac.esa.int/archive/documentation/GDR2/Gaia_archive/ + // chap_datamodel/sec_dm_main_tables/ssec_dm_gaia_source.html + radiusInKiloParsec = 1.0 / parallax[i]; + } + // Convert to Galactic Coordinates from Galactic Lon & Lat. + // https://gea.esac.esa.int/archive/documentation/GDR2/Data_processing/ + // chap_cu3ast/sec_cu3ast_intro/ssec_cu3ast_intro_tansforms.html#SSS1 + //values[idx++] = radiusInKiloParsec * cos(glm::radians(b_latitude[i])) * + //cos(glm::radians(l_longitude[i])); // Pos X + //values[idx++] = radiusInKiloParsec * cos(glm::radians(b_latitude[i])) * + //sin(glm::radians(l_longitude[i])); // Pos Y + //values[idx++] = radiusInKiloParsec * sin(glm::radians(b_latitude[i])); // Pos Z + + + // Convert ICRS Equatorial Ra and Dec to Galactic latitude and longitude. + glm::mat3 aPrimG = glm::mat3( + // Col 0 + glm::vec3(-0.0548755604162154, 0.4941094278755837, -0.8676661490190047), + // Col 1 + glm::vec3(-0.8734370902348850, -0.4448296299600112, -0.1980763734312015), + // Col 2 + glm::vec3(-0.4838350155487132, 0.7469822444972189, 0.4559837761750669) + ); + glm::vec3 rICRS = glm::vec3( + cos(glm::radians(ra[i])) * cos(glm::radians(dec[i])), + sin(glm::radians(ra[i])) * cos(glm::radians(dec[i])), + sin(glm::radians(dec[i])) + ); + glm::vec3 rGal = aPrimG * rICRS; + values[idx++] = radiusInKiloParsec * rGal.x; // Pos X + values[idx++] = radiusInKiloParsec * rGal.y; // Pos Y + values[idx++] = radiusInKiloParsec * rGal.z; // Pos Z + + // Store magnitude render value. (Set default to high mag = low brightness) + values[idx++] = std::isnan(meanMagG[i]) ? 20.f : meanMagG[i]; // Mean G-band Mag + + // Store color render value. (Default value is bluish stars) + values[idx++] = std::isnan(meanMagBp[i]) && std::isnan(meanMagRp[i]) ? 0.f + : meanMagBp[i] - meanMagRp[i]; // Bp-Rp Color + + // Store velocity. + if (std::isnan(pmra[i])) pmra[i] = 0.f; + if (std::isnan(pmdec[i])) pmdec[i] = 0.f; + + // Convert Proper Motion from ICRS [Ra,Dec] to Galactic Tanget Vector [l,b]. + glm::vec3 uICRS = glm::vec3( + -sin(glm::radians(ra[i])) * pmra[i] - + cos(glm::radians(ra[i])) * sin(glm::radians(dec[i])) * pmdec[i], + cos(glm::radians(ra[i])) * pmra[i] - + sin(glm::radians(ra[i])) * sin(glm::radians(dec[i])) * pmdec[i], + cos(glm::radians(dec[i])) * pmdec[i] + ); + glm::vec3 pmVecGal = aPrimG * uICRS; + + // Convert to Tangential vector [m/s] from Proper Motion vector [mas/yr] + float tanVelX = 1000.0 * 4.74 * radiusInKiloParsec * pmVecGal.x; + float tanVelY = 1000.0 * 4.74 * radiusInKiloParsec * pmVecGal.y; + float tanVelZ = 1000.0 * 4.74 * radiusInKiloParsec * pmVecGal.z; + + // Calculate True Space Velocity [m/s] if we have the radial velocity + if (!std::isnan(radial_vel[i])) { + // Calculate Radial Velocity in the direction of the star. + // radial_vel is given in [km/s] -> convert to [m/s]. + float radVelX = 1000.0 * radial_vel[i] * rGal.x; + float radVelY = 1000.0 * radial_vel[i] * rGal.y; + float radVelZ = 1000.0 * radial_vel[i] * rGal.z; + + // Use Pythagoras theorem for the final Space Velocity [m/s]. + values[idx++] = sqrt(pow(radVelX, 2) + pow(tanVelX, 2)); // Vel X [U] + values[idx++] = sqrt(pow(radVelY, 2) + pow(tanVelY, 2)); // Vel Y [V] + values[idx++] = sqrt(pow(radVelZ, 2) + pow(tanVelZ, 2)); // Vel Z [W] + } + // Otherwise use the vector [m/s] we got from proper motion. + else { + radial_vel[i] = 0.f; + values[idx++] = tanVelX; // Vel X [U] + values[idx++] = tanVelY; // Vel Y [V] + values[idx++] = tanVelZ; // Vel Z [W] + } + + fullData.insert(fullData.end(), values.begin(), values.end()); + }*/ + + LINFO(fmt::format("{} out of {} read stars were null arrays", nNullArr, nStars)); + LINFO(fmt::format("Multiplier: {}", multiplier)); + + return fullData; +} + +std::vector FitsFileReader::readSpeckFile(std::string filePath, int& nRenderValues) +{ + auto fullData = std::vector(); + + std::ifstream fileStream(filePath); + + if (!fileStream.good()) { + LERROR(fmt::format("Failed to open Speck file '{}'", filePath)); + return fullData; + } + + int nValuesPerStar = 0; + int nNullArr = 0; + size_t nStars = 0; + + // The beginning of the speck file has a header that either contains comments + // (signaled by a preceding '#') or information about the structure of the file + // (signaled by the keywords 'datavar', 'texturevar', 'texture' and 'maxcomment') + std::string line = ""; + while (true) { + std::streampos position = fileStream.tellg(); + std::getline(fileStream, line); + + if (line[0] == '#' || line.empty()) { + continue; + } + + if (line.substr(0, 7) != "datavar" && line.substr(0, 10) != "texturevar" && + line.substr(0, 7) != "texture" && line.substr(0, 10) != "maxcomment") + { + // We read a line that doesn't belong to the header, so we have to jump back + // before the beginning of the current line. + fileStream.seekg(position); + break; + } + + if (line.substr(0, 7) == "datavar") { + // datavar lines are structured as follows: + // datavar # description + // where # is the index of the data variable; so if we repeatedly overwrite + // the 'nValues' variable with the latest index, we will end up with the total + // number of values (+3 since X Y Z are not counted in the Speck file index) + std::stringstream str(line); + + std::string dummy; + str >> dummy; + str >> nValuesPerStar; + nValuesPerStar += 1; // We want the number, but the index is 0 based + } + } + + nValuesPerStar += 3; // X Y Z are not counted in the Speck file indices + + // Order in DR1 file: DR2 - GaiaGroupMembers: + // 0 BVcolor 0 color + // 1 lum 1 lum + // 2 Vabsmag 2 absmag + // 3 Vappmag 3 Gmag + // 4 distly 4 distpc + // 5 distpcPctErr 5 plx + // 6 U 6 ra + // 7 V 7 dec + // 8 W 8 RadVel + // 9 speed 9 Teff + // 10 sptypeindex 10 vx + // 11 lumclassindex 11 vy + // 12 catsource 12 vz + // 13 texture 13 speed + // 14 texture + + do { + std::vector readValues(nValuesPerStar); + nStars++; + + std::getline(fileStream, line); + std::stringstream str(line); + + // Read values. + for (int i = 0; i < nValuesPerStar; ++i) { + str >> readValues[i]; + } + + // Check if star is a nullArray. + bool nullArray = true; + for (size_t i = 0; i < readValues.size(); ++i) { + if (readValues[i] != 0.0) { + nullArray = false; + break; + } + } + + // Insert to data if we found some values. + if (!nullArray) { + // Re-order data here because Octree expects the data in correct order when + // read. + // Default order for rendering: + // Position [X, Y, Z] + // Absolute Magnitude + // B-V Color + // Velocity [X, Y, Z] + + nRenderValues = 8; + std::vector renderValues(nRenderValues); + + // Gaia DR1 data from AMNH measures positions in Parsec, but + // RenderableGaiaStars expects kiloParsec (because fits file from Vienna had + // in kPc). + // Thus we need to convert positions twice atm. + renderValues[0] = readValues[0] / 1000.0; // PosX + renderValues[1] = readValues[1] / 1000.0; // PosY + renderValues[2] = readValues[2] / 1000.0; // PosZ + renderValues[3] = readValues[6]; // AbsMag + renderValues[4] = readValues[3]; // color + renderValues[5] = readValues[13] * readValues[16]; // Vel X + renderValues[6] = readValues[14] * readValues[16]; // Vel Y + renderValues[7] = readValues[15] * readValues[16]; // Vel Z + + fullData.insert(fullData.end(), renderValues.begin(), renderValues.end()); + } + else { + nNullArr++; + } + + } while (!fileStream.eof()); + + LINFO(fmt::format("{} out of {} read stars were null arrays", nNullArr, nStars)); + + return fullData; +} + +// This is pretty annoying, the read method is not derived from the HDU class +// in CCfits - need to explicitly cast to the sub classes to access read +template +const std::shared_ptr> FitsFileReader::readImageInternal(ExtHDU& image) { + try { + std::valarray contents; + image.read(contents); + ImageData im = { std::move(contents), image.axis(0), image.axis(1) }; + return std::make_shared>(im); + } catch (const FitsException& e){ + LERROR("Could not read FITS image EXTHDU. " + e.message() ); + } + return nullptr; +} + +template +const std::shared_ptr> FitsFileReader::readImageInternal(PHDU& image) { + try { + std::valarray contents; + image.read(contents); + ImageData im = { std::move(contents), image.axis(0), image.axis(1) }; + return std::make_shared>(im); + } catch (const FitsException& e){ + LERROR("Could not read FITS image PHDU. " + e.message() ); + } + return nullptr; +} + +} // namespace openspace diff --git a/modules/gaia/CMakeLists.txt b/modules/gaia/CMakeLists.txt new file mode 100644 index 0000000000..e83e55bc08 --- /dev/null +++ b/modules/gaia/CMakeLists.txt @@ -0,0 +1,71 @@ +########################################################################################## +# # +# OpenSpace # +# # +# Copyright (c) 2014-2018 # +# # +# 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(${OPENSPACE_CMAKE_EXT_DIR}/module_definition.cmake) + +set(HEADER_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/gaiamodule.h + ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderablegaiastars.h + ${CMAKE_CURRENT_SOURCE_DIR}/rendering/octreemanager.h + ${CMAKE_CURRENT_SOURCE_DIR}/rendering/octreeculler.h + ${CMAKE_CURRENT_SOURCE_DIR}/tasks/readfilejob.h + ${CMAKE_CURRENT_SOURCE_DIR}/tasks/readfitstask.h + ${CMAKE_CURRENT_SOURCE_DIR}/tasks/readspecktask.h + ${CMAKE_CURRENT_SOURCE_DIR}/tasks/constructoctreetask.h + ${CMAKE_CURRENT_SOURCE_DIR}/rendering/gaiaoptions.h +) +source_group("Header Files" FILES ${HEADER_FILES}) + +set(SOURCE_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/gaiamodule.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/rendering/renderablegaiastars.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/rendering/octreemanager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/rendering/octreeculler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/tasks/readfilejob.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/tasks/readfitstask.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/tasks/readspecktask.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/tasks/constructoctreetask.cpp +) +source_group("Source Files" FILES ${SOURCE_FILES}) + +set(SHADER_FILES + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_vbo_vs.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_ssbo_vs.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_billboard_nofbo_fs.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_billboard_fs.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_billboard_ge.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_point_fs.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_point_ge.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_tonemapping_vs.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_tonemapping_point_fs.glsl + ${CMAKE_CURRENT_SOURCE_DIR}/shaders/gaia_tonemapping_billboard_fs.glsl +) +source_group("Shader Files" FILES ${SHADER_FILES}) + +create_new_module( + "Gaia" + gaia + STATIC + ${HEADER_FILES} ${SOURCE_FILES} ${SHADER_FILES} +) diff --git a/modules/gaia/gaiamodule.cpp b/modules/gaia/gaiamodule.cpp new file mode 100644 index 0000000000..d581ac2d6c --- /dev/null +++ b/modules/gaia/gaiamodule.cpp @@ -0,0 +1,72 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openspace { + +GaiaModule::GaiaModule() : OpenSpaceModule(Name) {} + +void GaiaModule::internalInitialize(const ghoul::Dictionary&) { + auto fRenderable = FactoryManager::ref().factory(); + ghoul_assert(fRenderable, "No renderable factory existed"); + fRenderable->registerClass("RenderableGaiaStars"); + + auto fTask = FactoryManager::ref().factory(); + ghoul_assert(fRenderable, "No task factory existed"); + fTask->registerClass("ReadFitsTask"); + fTask->registerClass("ReadSpeckTask"); + fTask->registerClass("ConstructOctreeTask"); +} + +std::vector GaiaModule::documentations() const { + return { + RenderableGaiaStars::Documentation(), + ReadFitsTask::Documentation(), + ReadSpeckTask::Documentation(), + ConstructOctreeTask::Documentation(), + }; +} + +scripting::LuaLibrary GaiaModule::luaLibrary() const { + scripting::LuaLibrary res; + res.name = "gaia"; + res.scripts = { + absPath("${MODULE_GAIAMISSION}/scripts/filtering.lua") + }; + return res; +} + +} // namespace openspace diff --git a/modules/gaia/gaiamodule.h b/modules/gaia/gaiamodule.h new file mode 100644 index 0000000000..038bd8660c --- /dev/null +++ b/modules/gaia/gaiamodule.h @@ -0,0 +1,50 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_GAIA___GAIAMODULE___H__ +#define __OPENSPACE_MODULE_GAIA___GAIAMODULE___H__ + +#include + +#include + +namespace openspace { + +class GaiaModule : public OpenSpaceModule { +public: + constexpr static const char* Name = "Gaia"; + + GaiaModule(); + virtual ~GaiaModule() = default; + + std::vector documentations() const override; + scripting::LuaLibrary luaLibrary() const override; + +private: + void internalInitialize(const ghoul::Dictionary&) override; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_GAIA___GAIAMODULE___H__ diff --git a/modules/gaia/include.cmake b/modules/gaia/include.cmake new file mode 100644 index 0000000000..a754a6bc03 --- /dev/null +++ b/modules/gaia/include.cmake @@ -0,0 +1,4 @@ +set (OPENSPACE_DEPENDENCIES + fitsfilereader + globebrowsing +) diff --git a/modules/gaia/rendering/gaiaoptions.h b/modules/gaia/rendering/gaiaoptions.h new file mode 100644 index 0000000000..f6beab2e1b --- /dev/null +++ b/modules/gaia/rendering/gaiaoptions.h @@ -0,0 +1,54 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_GAIA___GAIAOPTIONS___H__ +#define __OPENSPACE_MODULE_GAIA___GAIAOPTIONS___H__ + +namespace openspace::gaia { + +enum RenderOption { + Static = 0, + Color = 1, + Motion = 2 +}; + +enum FileReaderOption { + Fits = 0, + Speck = 1, + BinaryRaw = 2, + BinaryOctree = 3, + StreamOctree = 4 +}; + +enum ShaderOption { + Point_SSBO = 0, + Point_VBO = 1, + Billboard_SSBO = 2, + Billboard_VBO = 3, + Billboard_SSBO_noFBO = 4 +}; + +} // namespace openspace::gaiamission + +#endif // __OPENSPACE_MODULE_GAIA___GAIAOPTIONS___H__ diff --git a/modules/gaia/rendering/octreeculler.cpp b/modules/gaia/rendering/octreeculler.cpp new file mode 100644 index 0000000000..c3a28004f9 --- /dev/null +++ b/modules/gaia/rendering/octreeculler.cpp @@ -0,0 +1,86 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include + +namespace { + constexpr const char* _loggerCat = "OctreeCuller"; +} // namespace + +namespace openspace { + +namespace { + bool intersects(const globebrowsing::AABB3& bb, const globebrowsing::AABB3& o) { + return (bb.min.x <= o.max.x) && (o.min.x <= bb.max.x) + && (bb.min.y <= o.max.y) && (o.min.y <= bb.max.y) + && (bb.min.z <= o.max.z) && (o.min.z <= bb.max.z); + } + + void expand(globebrowsing::AABB3& bb, const glm::vec3& p) { + bb.min = glm::min(bb.min, p); + bb.max = glm::max(bb.max, p); + } +} // namespace + +OctreeCuller::OctreeCuller(globebrowsing::AABB3 viewFrustum) + : _viewFrustum(std::move(viewFrustum)) +{} + +bool OctreeCuller::isVisible(const std::vector& corners, + const glm::dmat4& mvp) +{ + createNodeBounds(corners, mvp); + return intersects(_viewFrustum, _nodeBounds); +} + +glm::vec2 OctreeCuller::getNodeSizeInPixels(const std::vector& corners, + const glm::dmat4& mvp, + const glm::vec2& screenSize) +{ + + createNodeBounds(corners, mvp); + + // Screen space is mapped to [-1, 1] so divide by 2 and multiply with screen size. + glm::vec3 size = (_nodeBounds.max - _nodeBounds.min) / 2.f; + size = glm::abs(size); + return glm::vec2(size.x * screenSize.x, size.y * screenSize.y); +} + +void OctreeCuller::createNodeBounds(const std::vector& corners, + const glm::dmat4& mvp) +{ + // Create a bounding box in clipping space from node boundaries. + _nodeBounds = globebrowsing::AABB3(); + + for (size_t i = 0; i < 8; ++i) { + glm::dvec4 cornerClippingSpace = mvp * corners[i]; + glm::dvec4 ndc = (1.f / glm::abs(cornerClippingSpace.w)) * cornerClippingSpace; + expand(_nodeBounds, glm::dvec3(ndc)); + } +} + +} // namespace openspace diff --git a/modules/gaia/rendering/octreeculler.h b/modules/gaia/rendering/octreeculler.h new file mode 100644 index 0000000000..4800eda85f --- /dev/null +++ b/modules/gaia/rendering/octreeculler.h @@ -0,0 +1,76 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_GAIA___OCTREECULLER___H__ +#define __OPENSPACE_MODULE_GAIA___OCTREECULLER___H__ + +#include +#include + +// TODO: Move /geometry/* to libOpenSpace so as not to depend on globebrowsing. + +namespace openspace { + +/** + * Culls all octree nodes that are completely outside the view frustum. + * + * The frustum culling uses a 2D axis aligned bounding box for the OctreeNode in + * screen space. + */ + +class OctreeCuller { +public: + + /** + * \param viewFrustum is the view space in normalized device coordinates space. + * Hence it is an axis aligned bounding box and not a real frustum. + */ + OctreeCuller(globebrowsing::AABB3 viewFrustum); + + ~OctreeCuller() = default; + + /** + * \return true if any part of the node is visible in the current view. + */ + bool isVisible(const std::vector& corners, const glm::dmat4& mvp); + + /** + * \return the size [in pixels] of the node in clipping space. + */ + glm::vec2 getNodeSizeInPixels(const std::vector& corners, + const glm::dmat4& mvp, const glm::vec2& screenSize); + +private: + /** + * Creates an axis-aligned bounding box containing all \p corners in clipping space. + */ + void createNodeBounds(const std::vector& corners, const glm::dmat4& mvp); + + const globebrowsing::AABB3 _viewFrustum; + globebrowsing::AABB3 _nodeBounds; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_GAIA___OCTREECULLER___H__ diff --git a/modules/gaia/rendering/octreemanager.cpp b/modules/gaia/rendering/octreemanager.cpp new file mode 100644 index 0000000000..31331cb90f --- /dev/null +++ b/modules/gaia/rendering/octreemanager.cpp @@ -0,0 +1,1415 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr const char* _loggerCat = "OctreeManager"; +} // namespace + +namespace openspace { + +OctreeManager::~OctreeManager() {} + +void OctreeManager::initOctree(long long cpuRamBudget, int maxDist, int maxStarsPerNode) { + if (_root) { + LDEBUG("Clear existing Octree"); + clearAllData(); + } + + LDEBUG("Initializing new Octree"); + _root = std::make_shared(); + _root->octreePositionIndex = 8; + + // Initialize the culler. The NDC.z of the comparing corners are always -1 or 1. + globebrowsing::AABB3 box; + box.min = glm::vec3(-1.f, -1.f, 0.f); + box.max = glm::vec3(1.f, 1.f, 1e2); + _culler = std::make_unique(box); + _removedKeysInPrevCall = std::set(); + _leastRecentlyFetchedNodes = std::queue(); + + // Reset default values when rebuilding the Octree during runtime. + _numInnerNodes = 0; + _numLeafNodes = 0; + _totalDepth = 0; + _valuesPerStar = POS_SIZE + COL_SIZE + VEL_SIZE; + _maxCpuRamBudget = cpuRamBudget; + _cpuRamBudget = cpuRamBudget; + _parentNodeOfCamera = 8; + + if (maxDist > 0) { + MAX_DIST = static_cast(maxDist); + } + if (maxStarsPerNode > 0) { + MAX_STARS_PER_NODE = static_cast(maxStarsPerNode); + } + + for (size_t i = 0; i < 8; ++i) { + _numLeafNodes++; + _root->Children[i] = std::make_shared(); + _root->Children[i]->posData = std::vector(); + _root->Children[i]->colData = std::vector(); + _root->Children[i]->velData = std::vector(); + _root->Children[i]->magOrder = std::vector>(); + _root->Children[i]->isLeaf = true; + _root->Children[i]->isLoaded = false; + _root->Children[i]->hasLoadedDescendant = false; + _root->Children[i]->bufferIndex = DEFAULT_INDEX; + _root->Children[i]->octreePositionIndex = 80 + i; + _root->Children[i]->numStars = 0; + _root->Children[i]->halfDimension = MAX_DIST / 2.f; + _root->Children[i]->originX = (i % 2 == 0) ? + _root->Children[i]->halfDimension : + -_root->Children[i]->halfDimension; + _root->Children[i]->originY = (i % 4 < 2) ? + _root->Children[i]->halfDimension : + -_root->Children[i]->halfDimension; + _root->Children[i]->originZ = (i < 4) ? + _root->Children[i]->halfDimension : + -_root->Children[i]->halfDimension; + } +} + +void OctreeManager::initBufferIndexStack(long long maxNodes, bool useVBO, + bool datasetFitInMemory) +{ + // Clear stack if we've used it before. + _biggestChunkIndexInUse = 0; + _freeSpotsInBuffer = std::stack(); + _rebuildBuffer = true; + _useVBO = useVBO; + _datasetFitInMemory = datasetFitInMemory; + + // Build stack back-to-front. + for (long long idx = maxNodes - 1; idx >= 0; --idx) { + _freeSpotsInBuffer.push(static_cast(idx)); + } + _maxStackSize = _freeSpotsInBuffer.size(); + LINFO("StackSize: " + std::to_string(maxNodes)); +} + +void OctreeManager::insert(const std::vector& starValues) { + size_t index = getChildIndex(starValues[0], starValues[1], starValues[2]); + + insertInNode(_root->Children[index], starValues); +} + +void OctreeManager::sliceLodData(size_t branchIndex) { + if (branchIndex != 8) { + sliceNodeLodCache(_root->Children[branchIndex]); + } + else { + for (int i = 0; i < 7; ++i) { + sliceNodeLodCache(_root->Children[i]); + } + } +} + +void OctreeManager::printStarsPerNode() const { + auto accumulatedString = std::string(); + + for (int i = 0; i < 8; ++i) { + std::string prefix = "{" + std::to_string(i); + accumulatedString += printStarsPerNode(_root->Children[i], prefix); + } + LINFO(fmt::format("Number of stars per node: \n{}", accumulatedString)); + LINFO(fmt::format("Number of leaf nodes: {}", std::to_string(_numLeafNodes))); + LINFO(fmt::format("Number of inner nodes: {}", std::to_string(_numInnerNodes))); + LINFO(fmt::format("Depth of tree: {}", std::to_string(_totalDepth))); +} + +void OctreeManager::fetchSurroundingNodes(const glm::dvec3& cameraPos, + size_t chunkSizeInBytes, + const glm::ivec2& additionalNodes) +{ + + // If entire dataset fits in RAM then load the entire dataset asynchronously now. + // Nodes will be rendered when they've been made available. + if (_datasetFitInMemory) { + // Only traverse Octree once! + if (_parentNodeOfCamera == 8) { + // Fetch first layer of children + fetchChildrenNodes(_root, 0); + + for (int i = 0; i < 8; ++i) { + // Check so branch doesn't have a single layer. + if (_root->Children[i]->isLeaf) { + continue; + } + + // Use multithreading to load files and detach thread from main execution + // so it can execute independently. Thread will be destroyed when + // finished! + std::thread( + &OctreeManager::fetchChildrenNodes, + this, + _root->Children[i], + -1 + ).detach(); + } + _parentNodeOfCamera = 0; + } + return; + } + + // Get leaf node in which the camera resides. + glm::vec3 fCameraPos = static_cast( + cameraPos / (1000.0 * distanceconstants::Parsec) + ); + size_t idx = getChildIndex(fCameraPos.x, fCameraPos.y, fCameraPos.z); + std::shared_ptr node = _root->Children[idx]; + + while (!node->isLeaf) { + idx = getChildIndex( + fCameraPos.x, + fCameraPos.y, + fCameraPos.z, + node->originX, + node->originY, + node->originZ + ); + node = node->Children[idx]; + } + unsigned long long leafId = node->octreePositionIndex; + unsigned long long firstParentId = leafId / 10; + + // Return early if camera resides in the same first parent as before! + // Otherwise camera has moved and may need to load more nodes! + if (_parentNodeOfCamera == firstParentId) { + return; + } + _parentNodeOfCamera = firstParentId; + + // Each parent level may be root, make sure to propagate it in that case! + unsigned long long secondParentId = (firstParentId == 8) ? 8 : leafId / 100; + unsigned long long thirdParentId = (secondParentId == 8) ? 8 : leafId / 1000; + unsigned long long fourthParentId = (thirdParentId == 8) ? 8 : leafId / 10000; + unsigned long long fifthParentId = (fourthParentId == 8) ? 8 : leafId / 100000; + + // Get the number of levels to fetch from user input. + int additionalLevelsToFetch = additionalNodes.y; + + // Get more descendants when closer to root. + if (_parentNodeOfCamera < 80000) { + additionalLevelsToFetch++; + } + + // Get the 3^3 closest parents and load all their (eventual) children. + for (int x = -1; x <= 1; x += 1) { + for (int y = -2; y <= 2; y += 2) { + for (int z = -4; z <= 4; z += 4) { + // Fetch all stars the 216 closest leaf nodes. + findAndFetchNeighborNode( + firstParentId, + x, + y, + z, + additionalLevelsToFetch + ); + // Fetch LOD stars from 208 parents one and two layer(s) up. + if (x != 0 || y != 0 || z != 0) { + if (additionalNodes.x > 0) { + findAndFetchNeighborNode( + secondParentId, + x, + y, + z, + additionalLevelsToFetch + ); + } + if (additionalNodes.x > 1) { + findAndFetchNeighborNode( + thirdParentId, + x, + y, + z, + additionalLevelsToFetch + ); + } + if (additionalNodes.x > 2) { + findAndFetchNeighborNode( + fourthParentId, + x, + y, + z, + additionalLevelsToFetch + ); + } + if (additionalNodes.x > 3) { + findAndFetchNeighborNode( + fifthParentId, + x, + y, + z, + additionalLevelsToFetch + ); + } + } + } + } + } + + // Check if we should remove any nodes from RAM. + long long tenthOfRamBudget = _maxCpuRamBudget / 10; + if (_cpuRamBudget < tenthOfRamBudget) { + long long bytesToTenthOfRam = tenthOfRamBudget - _cpuRamBudget; + size_t nNodesToRemove = static_cast(bytesToTenthOfRam / chunkSizeInBytes); + std::vector nodesToRemove; + while (nNodesToRemove > 0) { + // Dequeue nodes that were least recently fetched by findAndFetchNeighborNode. + nodesToRemove.push_back(_leastRecentlyFetchedNodes.front()); + _leastRecentlyFetchedNodes.pop(); + nNodesToRemove--; + } + // Use asynchronous removal. + if (!nodesToRemove.empty()) { + std::thread(&OctreeManager::removeNodesFromRam, this, nodesToRemove).detach(); + } + } +} + +void OctreeManager::findAndFetchNeighborNode(unsigned long long firstParentId, int x, + int y, int z, int additionalLevelsToFetch) +{ + unsigned long long parentId = firstParentId; + auto indexStack = std::stack(); + + // Fetch first layer children if we're already at root. + if (parentId == 8) { + fetchChildrenNodes(_root, 0); + return; + } + + //----------------- Change first index -------------------// + int nodeIndex = parentId % 10; + + int dx = (nodeIndex % 2 == 0) ? 1 : -1; + int dy = (nodeIndex % 4 < 2) ? 2 : -2; + int dz = (nodeIndex < 4) ? 4 : -4; + + // Determine if we need to switch any side (find a higher common parent). + bool needToSwitchX = (x == dx); + bool needToSwitchY = (y == dy); + bool needToSwitchZ = (z == dz); + + if (!needToSwitchX) { + x = -x; + } + if (!needToSwitchY) { + y = -y; + } + if (!needToSwitchZ) { + z = -z; + } + + // Update node index and store it at the back of our stack. + nodeIndex += x + y + z; + indexStack.push(nodeIndex); + parentId /= 10; + + //--------- Change all indices until we find a common parent --------------// + while (parentId != 8 && (needToSwitchX || needToSwitchY || needToSwitchZ) ) { + nodeIndex = parentId % 10; + + dx = (nodeIndex % 2 == 0) ? 1 : -1; + dy = (nodeIndex % 4 < 2) ? 2 : -2; + dz = (nodeIndex < 4) ? 4 : -4; + + if (needToSwitchX) { + if (x != dx) { + needToSwitchX = false; + } + nodeIndex += dx; + } + if (needToSwitchY) { + if (y != dy) { + needToSwitchY = false; + } + nodeIndex += dy; + } + if (needToSwitchZ) { + if (z != dz) { + needToSwitchZ = false; + } + nodeIndex += dz; + } + + indexStack.push(nodeIndex); + parentId /= 10; + } + + // Take care of edge cases. If we got to the root but still need to switch to a + // common parent then no neighbor exists in that direction. + if (needToSwitchX || needToSwitchY || needToSwitchZ) { + return; + } + + // Continue to root if we didn't reach it. + while (parentId != 8) { + nodeIndex = parentId % 10; + indexStack.push(nodeIndex); + parentId /= 10; + } + + // Traverse to that parent node (as long as such a child exists!). + std::shared_ptr node = _root; + while (!indexStack.empty() && !node->Children[indexStack.top()]->isLeaf) { + node = node->Children[indexStack.top()]; + node->hasLoadedDescendant = true; + indexStack.pop(); + } + + // Fetch all children nodes from found parent. Use multithreading to load files + // asynchronously! Detach thread from main execution so it can execute independently. + // Thread will then be destroyed when it has finished! + std::thread( + &OctreeManager::fetchChildrenNodes, + this, + node, + additionalLevelsToFetch + ).detach(); +} + +std::map> OctreeManager::traverseData(const glm::dmat4& mvp, + const glm::vec2& screenSize, + int& deltaStars, + gaia::RenderOption option, + float lodPixelThreshold) +{ + auto renderData = std::map>(); + bool innerRebuild = false; + _minTotalPixelsLod = lodPixelThreshold; + + // Reclaim indices from previous render call. + for (auto removedKey = _removedKeysInPrevCall.rbegin(); + removedKey != _removedKeysInPrevCall.rend(); ++removedKey) { + + // Uses a reverse loop to try to decrease the biggest chunk. + if (*removedKey == _biggestChunkIndexInUse - 1) { + _biggestChunkIndexInUse = *removedKey; + LDEBUG(fmt::format( + "Decreased size to: {} Free Spots in VBO: {}", + _biggestChunkIndexInUse, _freeSpotsInBuffer.size() + )); + } + _freeSpotsInBuffer.push(*removedKey); + } + // Clear cache of removed keys before next render call. + if (!_removedKeysInPrevCall.empty()) { + _removedKeysInPrevCall.clear(); + } + + // Rebuild VBO from scratch if we're not using most of it but have a high max index. + if ((_biggestChunkIndexInUse > _maxStackSize * 4 / 5) && + (_freeSpotsInBuffer.size() > _maxStackSize * 5 / 6)) + { + LDEBUG(fmt::format( + "Rebuilding VBO. Biggest Chunk: {} 4/5: {} FreeSpotsInVBO: {} 5/6: {}", + _biggestChunkIndexInUse, _maxStackSize * 4 / 5, _freeSpotsInBuffer.size(), + _maxStackSize * 5 / 6 + )); + initBufferIndexStack(_maxStackSize, _useVBO, _datasetFitInMemory); + innerRebuild = true; + } + + // Check if entire tree is too small to see, and if so remove it. + std::vector corners(8); + float fMaxDist = static_cast(MAX_DIST); + for (int i = 0; i < 8; ++i) { + float x = (i % 2 == 0) ? fMaxDist : -fMaxDist; + float y = (i % 4 < 2) ? fMaxDist : -fMaxDist; + float z = (i < 4) ? fMaxDist : -fMaxDist; + glm::dvec3 pos = glm::dvec3(x, y, z) * 1000.0 * distanceconstants::Parsec; + corners[i] = glm::dvec4(pos, 1.0); + } + if (!_culler->isVisible(corners, mvp)) return renderData; + glm::vec2 nodeSize = _culler->getNodeSizeInPixels(corners, mvp, screenSize); + float totalPixels = nodeSize.x * nodeSize.y; + if (totalPixels < _minTotalPixelsLod * 2) { + // Remove LOD from first layer of children. + for (int i = 0; i < 8; ++i) { + std::map> tmpData = removeNodeFromCache( + _root->Children[i], + deltaStars + ); + renderData.insert(tmpData.begin(), tmpData.end()); + } + return renderData; + } + + for (size_t i = 0; i < 8; ++i) { + if (i < _traversedBranchesInRenderCall) { + continue; + } + + std::map> tmpData = checkNodeIntersection( + _root->Children[i], + mvp, + screenSize, + deltaStars, + option + ); + + // Avoid freezing when switching render mode for large datasets by only fetching + // one branch at a time when rebuilding buffer. + if (_rebuildBuffer) { + //renderData = std::move(tmpData); + _traversedBranchesInRenderCall++; + //break; + } + + // Observe that if there exists identical keys in renderData then those values in + // tmpData will be ignored! Thus we store the removed keys until next render call! + renderData.insert(tmpData.begin(), tmpData.end()); + } + + if (_rebuildBuffer) { + if (_useVBO) { + // We need to overwrite bigger indices that had data before! No need for SSBO. + std::map> idxToRemove; + for (int idx : _removedKeysInPrevCall) { + idxToRemove[idx] = std::vector(); + } + + // This will only insert indices that doesn't already exist in map + // (i.e. > biggestIdx). + renderData.insert(idxToRemove.begin(), idxToRemove.end()); + } + if (innerRebuild) { + deltaStars = 0; + } + + // Clear potential removed keys for both VBO and SSBO! + _removedKeysInPrevCall.clear(); + + LDEBUG(fmt::format( + "After rebuilding branch {} - Biggest chunk: {} Free spots in buffer: {}", + _traversedBranchesInRenderCall, _biggestChunkIndexInUse, + _freeSpotsInBuffer.size() + )); + + // End rebuild when all branches has been fetched. + if (_traversedBranchesInRenderCall == 8) { + _rebuildBuffer = false; + _traversedBranchesInRenderCall = 0; + } + } + return renderData; +} + +std::vector OctreeManager::getAllData(gaia::RenderOption option) { + std::vector fullData; + + for (size_t i = 0; i < 8; ++i) { + auto tmpData = getNodeData(_root->Children[i], option); + fullData.insert(fullData.end(), tmpData.begin(), tmpData.end()); + } + return fullData; +} + +void OctreeManager::clearAllData(int branchIndex) { + // Don't clear everything if not needed. + if (branchIndex != -1) { + clearNodeData(_root->Children[branchIndex]); + } + else { + for (size_t i = 0; i < 8; ++i) { + clearNodeData(_root->Children[i]); + } + } +} + +void OctreeManager::writeToFile(std::ofstream& outFileStream, bool writeData) { + outFileStream.write(reinterpret_cast(&_valuesPerStar), sizeof(int32_t)); + outFileStream.write( + reinterpret_cast(&MAX_STARS_PER_NODE), + sizeof(int32_t) + ); + outFileStream.write(reinterpret_cast(&MAX_DIST), sizeof(int32_t)); + + // Use pre-traversal (Morton code / Z-order). + for (size_t i = 0; i < 8; ++i) { + writeNodeToFile(outFileStream, _root->Children[i], writeData); + } +} + +void OctreeManager::writeNodeToFile(std::ofstream& outFileStream, + std::shared_ptr node, bool writeData) +{ + // Write node structure. + bool isLeaf = node->isLeaf; + int32_t numStars = static_cast(node->numStars); + outFileStream.write(reinterpret_cast(&isLeaf), sizeof(bool)); + outFileStream.write(reinterpret_cast(&numStars), sizeof(int32_t)); + + // Write node data if specified + if (writeData) { + std::vector nodeData = node->posData; + nodeData.insert(nodeData.end(), node->colData.begin(), node->colData.end()); + nodeData.insert(nodeData.end(), node->velData.begin(), node->velData.end()); + int32_t nDataSize = static_cast(nodeData.size()); + size_t nBytes = nDataSize * sizeof(nodeData[0]); + + outFileStream.write(reinterpret_cast(&nDataSize), sizeof(int32_t)); + if (nDataSize > 0) { + outFileStream.write(reinterpret_cast(nodeData.data()), nBytes); + } + } + + // Write children to file (in Morton order) if we're in an inner node. + if (!node->isLeaf) { + for (size_t i = 0; i < 8; ++i) { + writeNodeToFile(outFileStream, node->Children[i], writeData); + } + } +} + +int OctreeManager::readFromFile(std::ifstream& inFileStream, bool readData, + const std::string& folderPath) +{ + int nStarsRead = 0; + int oldMaxdist = static_cast(MAX_DIST); + + // If we're not reading data then we need to stream from files later on. + _streamOctree = !readData; + if (_streamOctree) { + _streamFolderPath = folderPath; + } + + _valuesPerStar = 0; + inFileStream.read(reinterpret_cast(&_valuesPerStar), sizeof(int32_t)); + inFileStream.read(reinterpret_cast(&MAX_STARS_PER_NODE), sizeof(int32_t)); + inFileStream.read(reinterpret_cast(&MAX_DIST), sizeof(int32_t)); + + LDEBUG(fmt::format( + "Max stars per node in read Octree: {} - Radius of root layer: {}", + MAX_STARS_PER_NODE, MAX_DIST + )); + + // Octree Manager root halfDistance must be updated before any nodes are created! + if (MAX_DIST != oldMaxdist) { + for (size_t i = 0; i < 8; ++i) { + _root->Children[i]->halfDimension = MAX_DIST / 2.f; + _root->Children[i]->originX = (i % 2 == 0) ? + _root->Children[i]->halfDimension : + -_root->Children[i]->halfDimension; + _root->Children[i]->originY = (i % 4 < 2) ? + _root->Children[i]->halfDimension : + -_root->Children[i]->halfDimension; + _root->Children[i]->originZ = (i < 4) ? + _root->Children[i]->halfDimension : + -_root->Children[i]->halfDimension; + } + } + + if (_valuesPerStar != (POS_SIZE + COL_SIZE + VEL_SIZE)) { + LERROR("Read file doesn't have the same structure of render parameters!"); + } + + // Use the same technique to construct octree from file. + for (size_t i = 0; i < 8; ++i) { + nStarsRead += readNodeFromFile(inFileStream, _root->Children[i], readData); + } + return nStarsRead; +} + +int OctreeManager::readNodeFromFile(std::ifstream& inFileStream, + std::shared_ptr node, bool readData) +{ + // Read node structure. + bool isLeaf; + int32_t numStars = 0; + + inFileStream.read(reinterpret_cast(&isLeaf), sizeof(bool)); + inFileStream.read(reinterpret_cast(&numStars), sizeof(int32_t)); + + node->isLeaf = isLeaf; + node->numStars = numStars; + + // Read node data if specified. + if (readData) { + int32_t nDataSize = 0; + inFileStream.read(reinterpret_cast(&nDataSize), sizeof(int32_t)); + + if (nDataSize > 0) { + std::vector fetchedData(nDataSize, 0.0f); + size_t nBytes = nDataSize * sizeof(fetchedData[0]); + + inFileStream.read(reinterpret_cast(&fetchedData[0]), nBytes); + + int starsInNode = static_cast(nDataSize / _valuesPerStar); + auto posEnd = fetchedData.begin() + (starsInNode * POS_SIZE); + auto colEnd = posEnd + (starsInNode * COL_SIZE); + auto velEnd = colEnd + (starsInNode * VEL_SIZE); + node->posData = std::vector(fetchedData.begin(), posEnd); + node->colData = std::vector(posEnd, colEnd); + node->velData = std::vector(colEnd, velEnd); + } + } + + // Create children if we're in an inner node and read from the corresponding nodes. + if (!node->isLeaf) { + numStars = 0; + createNodeChildren(node); + for (size_t i = 0; i < 8; ++i) { + numStars += readNodeFromFile(inFileStream, node->Children[i], readData); + } + } + + // Return the number of stars in the entire branch, no need to check children. + return numStars; +} + +void OctreeManager::writeToMultipleFiles(const std::string& outFolderPath, + size_t branchIndex) +{ + // Write entire branch to disc, with one file per node. + std::string outFilePrefix = outFolderPath + std::to_string(branchIndex); + // More threads doesn't make it much faster, disk speed still the limiter. + writeNodeToMultipleFiles(outFilePrefix, _root->Children[branchIndex], false); + + // Clear all data in branch. + LINFO(fmt::format("Clear all data from branch {} in octree", branchIndex)); + clearNodeData(_root->Children[branchIndex]); +} + +void OctreeManager::writeNodeToMultipleFiles(const std::string& outFilePrefix, + std::shared_ptr node, + bool threadWrites) +{ + // Prepare node data, save nothing else. + std::vector nodeData = node->posData; + nodeData.insert(nodeData.end(), node->colData.begin(), node->colData.end()); + nodeData.insert(nodeData.end(), node->velData.begin(), node->velData.end()); + int32_t nDataSize = static_cast(nodeData.size()); + size_t nBytes = nDataSize * sizeof(nodeData[0]); + + // Only open output stream if we have any values to write. + if (nDataSize > 0) { + // Use Morton code to name file (placement in Octree). + std::string outPath = outFilePrefix + BINARY_SUFFIX; + std::ofstream outFileStream(outPath, std::ofstream::binary); + if (outFileStream.good()) { + // LINFO("Write " + std::to_string(nDataSize) + " values to " + outPath); + outFileStream.write( + reinterpret_cast(&nDataSize), + sizeof(int32_t) + ); + outFileStream.write(reinterpret_cast(nodeData.data()), nBytes); + + outFileStream.close(); + } + else { + LERROR(fmt::format("Error opening file: {} as output data file.", outPath)); + } + } + + // Recursively write children to file (in Morton order) if we're in an inner node. + if (!node->isLeaf) { + std::vector writeThreads(8); + for (size_t i = 0; i < 8; ++i) { + std::string newOutFilePrefix = outFilePrefix + std::to_string(i); + if (threadWrites) { + // Divide writing to new threads to speed up the process. + std::thread t( + &OctreeManager::writeNodeToMultipleFiles, + this, + newOutFilePrefix, + node->Children[i], + false + ); + writeThreads[i] = std::move(t); + } + else { + writeNodeToMultipleFiles(newOutFilePrefix, node->Children[i], false); + } + } + if (threadWrites) { + // Make sure all threads are done. + for (int thread = 0; thread < 8; ++thread) { + writeThreads[thread].join(); + } + } + } +} + +void OctreeManager::fetchChildrenNodes( + std::shared_ptr parentNode, + int additionalLevelsToFetch) +{ + // Lock node to make sure nobody else are trying to load the same children. + std::lock_guard lock(parentNode->loadingLock); + + for (int i = 0; i < 8; ++i) { + // Fetch node data if we're streaming and it doesn't exist in RAM yet. + // (As long as there is any RAM budget left and node actually has any data!) + if (!parentNode->Children[i]->isLoaded && + (parentNode->Children[i]->numStars > 0) && + _cpuRamBudget > static_cast(parentNode->Children[i]->numStars + * (POS_SIZE + COL_SIZE + VEL_SIZE) * 4)) + { + fetchNodeDataFromFile(parentNode->Children[i]); + } + + // Fetch all Children's Children if recursive is set to true! + if (additionalLevelsToFetch != 0 && !parentNode->Children[i]->isLeaf) { + fetchChildrenNodes(parentNode->Children[i], --additionalLevelsToFetch); + } + } +} + +void OctreeManager::fetchNodeDataFromFile(std::shared_ptr node) { + // Remove root ID ("8") from index before loading file. + std::string posId = std::to_string(node->octreePositionIndex); + posId.erase(posId.begin()); + + std::string inFilePath = _streamFolderPath + posId + BINARY_SUFFIX; + std::ifstream inFileStream(inFilePath, std::ifstream::binary); + // LINFO("Fetch node data file: " + inFilePath); + + if (inFileStream.good()) { + // Read node data. + int32_t nDataSize = 0; + + // Octree knows if we have any data in this node = it exists. + // Otherwise don't call this function! + inFileStream.read(reinterpret_cast(&nDataSize), sizeof(int32_t)); + + std::vector readData(nDataSize, 0.f); + int nBytes = nDataSize * sizeof(readData[0]); + if (nDataSize > 0) { + inFileStream.read(reinterpret_cast(&readData[0]), nBytes); + } + + int starsInNode = static_cast(nDataSize / _valuesPerStar); + auto posEnd = readData.begin() + (starsInNode * POS_SIZE); + auto colEnd = posEnd + (starsInNode * COL_SIZE); + auto velEnd = colEnd + (starsInNode * VEL_SIZE); + node->posData = std::vector(readData.begin(), posEnd); + node->colData = std::vector(posEnd, colEnd); + node->velData = std::vector(colEnd, velEnd); + + // Keep track of nodes that are loaded and update CPU RAM budget. + node->isLoaded = true; + if (!_datasetFitInMemory) { + std::lock_guard g(_leastRecentlyFetchedNodesMutex); + _leastRecentlyFetchedNodes.push(node->octreePositionIndex); + } + _cpuRamBudget -= nBytes; + } + else { + LERROR("Error opening node data file: " + inFilePath); + } +} + +void OctreeManager::removeNodesFromRam( + const std::vector& nodesToRemove) +{ + // LINFO("Removed " + std::to_string(nodesToRemove.size()) + " nodes from RAM."); + + for (unsigned long long nodePosIndex : nodesToRemove) { + std::stack indexStack; + while (nodePosIndex != 8) { + int nodeIndex = nodePosIndex % 10; + indexStack.push(nodeIndex); + nodePosIndex /= 10; + } + + // Traverse to node and remove it. + std::shared_ptr node = _root; + std::vector> ancestors; + while (!indexStack.empty()) { + ancestors.push_back(node); + node = node->Children[indexStack.top()]; + indexStack.pop(); + } + removeNode(node); + + propagateUnloadedNodes(ancestors); + } +} + +void OctreeManager::removeNode(std::shared_ptr node) { + // Lock node to make sure nobody else is trying to access it while removing. + std::lock_guard lock(node->loadingLock); + + int nBytes = static_cast( + node->numStars * _valuesPerStar * sizeof(node->posData[0]) + ); + // Keep track of which nodes that are loaded and update CPU RAM budget. + node->isLoaded = false; + _cpuRamBudget += nBytes; + + // Clear data + node->posData.clear(); + node->posData.shrink_to_fit(); + node->colData.clear(); + node->colData.shrink_to_fit(); + node->velData.clear(); + node->velData.shrink_to_fit(); +} + +void OctreeManager::propagateUnloadedNodes( + std::vector> ancestorNodes) +{ + std::shared_ptr parentNode = ancestorNodes.back(); + while (parentNode->octreePositionIndex != 8) { + // Check if any children of inner node is still loaded, or has loaded descendants. + if (parentNode->Children[0]->isLoaded || parentNode->Children[1]->isLoaded || + parentNode->Children[2]->isLoaded || parentNode->Children[3]->isLoaded || + parentNode->Children[4]->isLoaded || parentNode->Children[5]->isLoaded || + parentNode->Children[6]->isLoaded || parentNode->Children[7]->isLoaded || + parentNode->Children[0]->hasLoadedDescendant || + parentNode->Children[1]->hasLoadedDescendant || + parentNode->Children[2]->hasLoadedDescendant || + parentNode->Children[3]->hasLoadedDescendant || + parentNode->Children[4]->hasLoadedDescendant || + parentNode->Children[5]->hasLoadedDescendant || + parentNode->Children[6]->hasLoadedDescendant || + parentNode->Children[7]->hasLoadedDescendant) + { + return; + } + // Else all children has been unloaded and we can update parent flag. + parentNode->hasLoadedDescendant = false; + // LINFO("Removed ancestor: " + std::to_string(parentNode->octreePositionIndex)); + + // Propagate change upwards. + ancestorNodes.pop_back(); + parentNode = ancestorNodes.back(); + } +} + +size_t OctreeManager::numLeafNodes() const { + return _numLeafNodes; +} + +size_t OctreeManager::numInnerNodes() const { + return _numInnerNodes; +} + +size_t OctreeManager::totalNodes() const { + return _numLeafNodes + _numInnerNodes; +} + +size_t OctreeManager::totalDepth() const { + return _totalDepth; +} + +size_t OctreeManager::maxDist() const { + return MAX_DIST; +} + +size_t OctreeManager::maxStarsPerNode() const { + return MAX_STARS_PER_NODE; +} + +size_t OctreeManager::biggestChunkIndexInUse() const { + return _biggestChunkIndexInUse; +} + +size_t OctreeManager::numFreeSpotsInBuffer() const { + return _freeSpotsInBuffer.size(); +} + +long long OctreeManager::cpuRamBudget() const { + return _cpuRamBudget; +} + +bool OctreeManager::isRebuildOngoing() const { + return _rebuildBuffer; +} + +size_t OctreeManager::getChildIndex(float posX, float posY, float posZ, float origX, + float origY, float origZ) +{ + size_t index = 0; + if (posX < origX) { + index += 1; + } + if (posY < origY) { + index += 2; + } + if (posZ < origZ) { + index += 4; + } + return index; +} + +bool OctreeManager::insertInNode(std::shared_ptr node, + const std::vector& starValues, int depth) +{ + if (node->isLeaf && node->numStars < MAX_STARS_PER_NODE) { + // Node is a leaf and it's not yet full -> insert star. + storeStarData(node, starValues); + + if (depth > _totalDepth) { + _totalDepth = depth; + } + return true; + } + else if (node->isLeaf) { + // Too many stars in leaf node, subdivide into 8 new nodes. + // Create children and clean up parent. + createNodeChildren(node); + + // Distribute stars from parent node into children. + for (size_t n = 0; n < MAX_STARS_PER_NODE; ++n) { + // Position data. + auto posBegin = node->posData.begin() + n * POS_SIZE; + auto posEnd = posBegin + POS_SIZE; + std::vector tmpValues(posBegin, posEnd); + // Color data. + auto colBegin = node->colData.begin() + n * COL_SIZE; + auto colEnd = colBegin + COL_SIZE; + tmpValues.insert(tmpValues.end(), colBegin, colEnd); + // Velocity data. + auto velBegin = node->velData.begin() + n * VEL_SIZE; + auto velEnd = velBegin + VEL_SIZE; + tmpValues.insert(tmpValues.end(), velBegin, velEnd); + + // Find out which child that will inherit the data and store it. + size_t index = getChildIndex( + tmpValues[0], + tmpValues[1], + tmpValues[2], + node->originX, + node->originY, + node->originZ + ); + insertInNode(node->Children[index], tmpValues, depth); + } + + // Sort magnitudes in inner node. + // (The last value will be used as comparison for what to store in LOD cache.) + std::sort(node->magOrder.begin(), node->magOrder.end()); + } + + // Node is an inner node, keep recursion going. + // This will also take care of the new star when a subdivision has taken place. + size_t index = getChildIndex( + starValues[0], + starValues[1], + starValues[2], + node->originX, + node->originY, + node->originZ + ); + + // Determine if new star should be kept in our LOD cache. + // Keeps track of the brightest nodes in children. + if (starValues[POS_SIZE] < node->magOrder[MAX_STARS_PER_NODE - 1].first) { + storeStarData(node, starValues); + } + + return insertInNode(node->Children[index], starValues, ++depth); +} + +void OctreeManager::sliceNodeLodCache(std::shared_ptr node) { + // Slice stored LOD data in inner nodes. + if (!node->isLeaf) { + // Sort by magnitude. Inverse relation (i.e. a lower magnitude means a brighter + // star!) + std::sort(node->magOrder.begin(), node->magOrder.end()); + node->magOrder.resize(MAX_STARS_PER_NODE); + + std::vector tmpPos; + std::vector tmpCol; + std::vector tmpVel; + // Ordered map contain the MAX_STARS_PER_NODE brightest stars in all children! + for (auto const &[absMag, placement] : node->magOrder) { + auto posBegin = node->posData.begin() + placement * POS_SIZE; + auto colBegin = node->colData.begin() + placement * COL_SIZE; + auto velBegin = node->velData.begin() + placement * VEL_SIZE; + tmpPos.insert(tmpPos.end(), posBegin, posBegin + POS_SIZE); + tmpCol.insert(tmpCol.end(), colBegin, colBegin + COL_SIZE); + tmpVel.insert(tmpVel.end(), velBegin, velBegin + VEL_SIZE); + } + node->posData = std::move(tmpPos); + node->colData = std::move(tmpCol); + node->velData = std::move(tmpVel); + node->numStars = node->magOrder.size(); // = MAX_STARS_PER_NODE + + for (int i = 0; i < 8; ++i) { + sliceNodeLodCache(node->Children[i]); + } + } +} + +void OctreeManager::storeStarData(std::shared_ptr node, + const std::vector& starValues) +{ + // Insert star data at the back of vectors and store a vector with pairs consisting of + // star magnitude and insert index for later sorting and slicing of LOD cache. + float mag = starValues[POS_SIZE]; + node->magOrder.insert(node->magOrder.end(), std::make_pair(mag, node->numStars)); + node->numStars++; + + // If LOD is growing too large then sort it and resize to [chunk size] to avoid too + // much RAM usage and increase threshold for adding new stars. + if (node->magOrder.size() > MAX_STARS_PER_NODE * 2) { + std::sort(node->magOrder.begin(), node->magOrder.end()); + node->magOrder.resize(MAX_STARS_PER_NODE); + } + + auto posEnd = starValues.begin() + POS_SIZE; + auto colEnd = posEnd + COL_SIZE; + node->posData.insert(node->posData.end(), starValues.begin(), posEnd); + node->colData.insert(node->colData.end(), posEnd, colEnd); + node->velData.insert(node->velData.end(), colEnd, starValues.end()); +} + +std::string OctreeManager::printStarsPerNode(std::shared_ptr node, + const std::string& prefix) const +{ + + // Print both inner and leaf nodes. + auto str = prefix + "} : " + std::to_string(node->numStars); + + if (node->isLeaf) { + return str + " - [Leaf] \n"; + } + else { + str += fmt::format("LOD: {} - [Parent]\n", node->posData.size() / POS_SIZE); + for (int i = 0; i < 8; ++i) { + auto pref = prefix + "->" + std::to_string(i); + str += printStarsPerNode(node->Children[i], pref); + } + return str; + } +} + +std::map> OctreeManager::checkNodeIntersection( + std::shared_ptr node, + const glm::dmat4& mvp, + const glm::vec2& screenSize, + int& deltaStars, + gaia::RenderOption option) +{ + std::map> fetchedData; + //int depth = static_cast(log2( MAX_DIST / node->halfDimension )); + + // Calculate the corners of the node. + std::vector corners(8); + for (int i = 0; i < 8; ++i) { + const float x = (i % 2 == 0) ? + node->originX + node->halfDimension : + node->originX - node->halfDimension; + const float y = (i % 4 < 2) ? + node->originY + node->halfDimension : + node->originY - node->halfDimension; + const float z = (i < 4) ? + node->originZ + node->halfDimension : + node->originZ - node->halfDimension; + glm::dvec3 pos = glm::dvec3(x, y, z) * 1000.0 * distanceconstants::Parsec; + corners[i] = glm::dvec4(pos, 1.0); + } + + // Check if node is visible from camera. If not then return early. + if (!(_culler->isVisible(corners, mvp))) { + // Check if this node or any of its children existed in cache previously. + // If so, then remove them from cache and add those indices to stack. + fetchedData = removeNodeFromCache(node, deltaStars); + return fetchedData; + } + + // Remove node if it has been unloaded while still in view. + // (While streaming big datasets.) + if (node->bufferIndex != DEFAULT_INDEX && !node->isLoaded && _streamOctree && + !_datasetFitInMemory) + { + fetchedData = removeNodeFromCache(node, deltaStars); + return fetchedData; + } + + // Take care of inner nodes. + if (!(node->isLeaf)) { + glm::vec2 nodeSize = _culler->getNodeSizeInPixels(corners, mvp, screenSize); + float totalPixels = nodeSize.x * nodeSize.y; + + // Check if we should return any LOD cache data. If we're streaming a big dataset + // from files and inner node is visible and loaded, then it should be rendered + // (as long as it doesn't have loaded children because then we should traverse to + // lowest loaded level and render it instead)! + if ((totalPixels < _minTotalPixelsLod) || (_streamOctree && + !_datasetFitInMemory && node->isLoaded && !node->hasLoadedDescendant)) + { + // Get correct insert index from stack if node didn't exist already. Otherwise + // we will overwrite the old data. Key merging is not a problem here. + if ((node->bufferIndex == DEFAULT_INDEX) || _rebuildBuffer) { + // Return empty if we couldn't claim a buffer stream index. + if (!updateBufferIndex(node)) { + return fetchedData; + } + + // We're in an inner node, remove indices from potential children in cache + for (int i = 0; i < 8; ++i) { + std::map> tmpData = removeNodeFromCache( + node->Children[i], + deltaStars + ); + fetchedData.insert(tmpData.begin(), tmpData.end()); + } + + // Insert data and adjust stars added in this frame. + fetchedData[node->bufferIndex] = constructInsertData( + node, + option, + deltaStars + ); + } + return fetchedData; + } + } + // Return node data if node is a leaf. + else { + // If node already is in cache then skip it, otherwise store it. + if ((node->bufferIndex == DEFAULT_INDEX) || _rebuildBuffer) { + // Return empty if we couldn't claim a buffer stream index. + if (!updateBufferIndex(node)) { + return fetchedData; + } + + // Insert data and adjust stars added in this frame. + fetchedData[node->bufferIndex] = constructInsertData( + node, + option, + deltaStars + ); + } + return fetchedData; + } + + // We're in a big, visible inner node -> remove it from cache if it existed. + // But not its children -> set recursive check to false. + fetchedData = removeNodeFromCache(node, deltaStars, false); + + // Recursively check if children should be rendered. + for (size_t i = 0; i < 8; ++i) { + // Observe that if there exists identical keys in fetchedData then those values in + // tmpData will be ignored! Thus we store the removed keys until next render call! + std::map> tmpData = checkNodeIntersection( + node->Children[i], + mvp, + screenSize, + deltaStars, + option + ); + fetchedData.insert(tmpData.begin(), tmpData.end()); + } + return fetchedData; +} + +std::map> OctreeManager::removeNodeFromCache( + std::shared_ptr node, + int& deltaStars, + bool recursive) +{ + std::map> keysToRemove; + + // If we're in rebuilding mode then there is no need to remove any nodes. + //if (_rebuildBuffer) return keysToRemove; + + // Check if this node was rendered == had a specified index. + if (node->bufferIndex != DEFAULT_INDEX) { + + // Reclaim that index. We need to wait until next render call to use it again! + _removedKeysInPrevCall.insert(node->bufferIndex); + + // Insert dummy node at offset index that should be removed from render. + keysToRemove[node->bufferIndex] = std::vector(); + + // Reset index and adjust stars removed this frame. + node->bufferIndex = DEFAULT_INDEX; + deltaStars -= static_cast(node->numStars); + } + + // Check children recursively if we're in an inner node. + if (!(node->isLeaf) && recursive) { + for (int i = 0; i < 8; ++i) { + std::map> tmpData = removeNodeFromCache( + node->Children[i], + deltaStars + ); + keysToRemove.insert(tmpData.begin(), tmpData.end()); + } + } + return keysToRemove; +} + +std::vector OctreeManager::getNodeData(std::shared_ptr node, + gaia::RenderOption option) +{ + // Return node data if node is a leaf. + if (node->isLeaf) { + int dStars = 0; + return constructInsertData(node, option, dStars); + } + + // If we're not in a leaf, get data from all children recursively. + auto nodeData = std::vector(); + for (size_t i = 0; i < 8; ++i) { + std::vector tmpData = getNodeData(node->Children[i], option); + nodeData.insert(nodeData.end(), tmpData.begin(), tmpData.end()); + } + return nodeData; +} + +void OctreeManager::clearNodeData(std::shared_ptr node) { + // Clear data and its allocated memory. + node->posData.clear(); + node->posData.shrink_to_fit(); + node->colData.clear(); + node->colData.shrink_to_fit(); + node->velData.clear(); + node->velData.shrink_to_fit(); + + // Clear magnitudes as well! + //std::vector>().swap(node->magOrder); + node->magOrder.clear(); + + if (!node->isLeaf) { + // Remove data from all children recursively. + for (size_t i = 0; i < 8; ++i) { + clearNodeData(node->Children[i]); + } + } +} + +void OctreeManager::createNodeChildren(std::shared_ptr node) { + for (size_t i = 0; i < 8; ++i) { + _numLeafNodes++; + node->Children[i] = std::make_shared(); + node->Children[i]->isLeaf = true; + node->Children[i]->isLoaded = false; + node->Children[i]->hasLoadedDescendant = false; + node->Children[i]->bufferIndex = DEFAULT_INDEX; + node->Children[i]->octreePositionIndex = (node->octreePositionIndex * 10) + i; + node->Children[i]->numStars = 0; + node->Children[i]->posData = std::vector(); + node->Children[i]->colData = std::vector(); + node->Children[i]->velData = std::vector(); + node->Children[i]->magOrder = std::vector>(); + node->Children[i]->halfDimension = node->halfDimension / 2.f; + + // Calculate new origin. + node->Children[i]->originX = node->originX; + node->Children[i]->originX += (i % 2 == 0) ? + node->Children[i]->halfDimension : + -node->Children[i]->halfDimension; + node->Children[i]->originY = node->originY; + node->Children[i]->originY += (i % 4 < 2) ? + node->Children[i]->halfDimension : + -node->Children[i]->halfDimension; + node->Children[i]->originZ = node->originZ; + node->Children[i]->originZ += (i < 4) ? + node->Children[i]->halfDimension : + -node->Children[i]->halfDimension; + } + + // Clean up parent. + node->isLeaf = false; + _numLeafNodes--; + _numInnerNodes++; +} + +bool OctreeManager::updateBufferIndex(std::shared_ptr node) { + if (node->bufferIndex != DEFAULT_INDEX) { + // If we're rebuilding Buffer Index Cache then store indices to overwrite later. + _removedKeysInPrevCall.insert(node->bufferIndex); + } + + // Make sure node isn't loading/unloading as we're checking isLoaded flag. + std::lock_guard lock(node->loadingLock); + + // Return false if there are no more spots in our buffer, or if we're streaming and + // node isn't loaded yet, or if node doesn't have any stars. + if (_freeSpotsInBuffer.empty() || (_streamOctree && !node->isLoaded) || + node->numStars == 0) { + return false; + } + + // Get correct insert index from stack. + node->bufferIndex = _freeSpotsInBuffer.top(); + _freeSpotsInBuffer.pop(); + + // Keep track of how many chunks are in use (ceiling). + if (_freeSpotsInBuffer.empty()) { + _biggestChunkIndexInUse++; + } + else if (_freeSpotsInBuffer.top() > _biggestChunkIndexInUse) { + _biggestChunkIndexInUse = _freeSpotsInBuffer.top(); + } + return true; +} + +std::vector OctreeManager::constructInsertData(std::shared_ptr node, + gaia::RenderOption option, + int& deltaStars) +{ + // Return early if node doesn't contain any stars! + if (node->numStars == 0) { + return std::vector(); + } + + // Fill chunk by appending zeroes to data so we overwrite possible earlier values. + // And more importantly so our attribute pointers knows where to read! + auto insertData = std::vector(node->posData.begin(), node->posData.end()); + if (_useVBO) { + insertData.resize(POS_SIZE * MAX_STARS_PER_NODE, 0.f); + } + if (option != gaia::RenderOption::Static) { + insertData.insert(insertData.end(), node->colData.begin(), node->colData.end()); + if (_useVBO) { + insertData.resize((POS_SIZE + COL_SIZE) * MAX_STARS_PER_NODE, 0.f); + } + if (option == gaia::RenderOption::Motion) { + insertData.insert( + insertData.end(), + node->velData.begin(), + node->velData.end() + ); + if (_useVBO) { + insertData.resize( + (POS_SIZE + COL_SIZE + VEL_SIZE) * MAX_STARS_PER_NODE, 0.f + ); + } + } + } + + // Update deltaStars. + deltaStars += static_cast(node->numStars); + return insertData; +} + +} // namespace openspace diff --git a/modules/gaia/rendering/octreemanager.h b/modules/gaia/rendering/octreemanager.h new file mode 100644 index 0000000000..bf60722acc --- /dev/null +++ b/modules/gaia/rendering/octreemanager.h @@ -0,0 +1,391 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_GAIA___OCTREEMANAGER___H__ +#define __OPENSPACE_MODULE_GAIA___OCTREEMANAGER___H__ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace openspace { + +class OctreeCuller; + +class OctreeManager { +public: + struct OctreeNode { + std::shared_ptr Children[8]; + std::vector posData; + std::vector colData; + std::vector velData; + std::vector> magOrder; + float originX; + float originY; + float originZ; + float halfDimension; + size_t numStars; + bool isLeaf; + bool isLoaded; + bool hasLoadedDescendant; + std::mutex loadingLock; + int bufferIndex; + unsigned long long octreePositionIndex; + }; + + OctreeManager() = default; + ~OctreeManager(); + + /** + * Initializes a one layer Octree with root and 8 children that covers all stars. + * + * \param maxDist together with \param maxstarsPerNode (if defined) determines the + * depth of the tree as well as how many nodes will be created. + */ + void initOctree(long long cpuRamBudget = 0, int maxDist = 0, int maxStarsPerNode = 0); + + /** + * Initializes a stack of size \param maxNodes that keeps track of all free spot in + * buffer stream. Can be used to trigger a rebuild of buffer(s). + * + * \param useVBO defines if VBO or SSBO is used as buffer(s) + * \param datasetFitInMemory defines if streaming of nodes during runtime is used + */ + void initBufferIndexStack(long long maxNodes, bool useVBO, bool datasetFitInMemory); + + /** + * Inserts star values in correct position in Octree. Makes use of a recursive + * traversal strategy. Internally calls insertInNode() + */ + void insert(const std::vector& starValues); + + /** + * Slices LOD data so only the MAX_STARS_PER_NODE brightest stars are stored in inner + * nodes. If \p branchIndex is defined then only that branch will be sliced. + * Calls sliceNodeLodCache() internally. + */ + void sliceLodData(size_t branchIndex = 8); + + /** + * Prints the whole tree structure, including number of stars per node, number of + * nodes, tree depth and if node is a leaf. + * Calls printStarsPerNode(node, prefix) internally. + */ + void printStarsPerNode() const; + + /** + * Used while streaming nodes from files. Checks if any nodes need to be loaded or + * unloaded. If entire dataset fits in RAM then the whole dataset will be loaded + * asynchronously. Otherwise only nodes close to the camera will be fetched. + * When RAM stars to fill up least-recently used nodes will start to unload. + * Calls findAndFetchNeighborNode() and + * removeNodesFromRam() internally. + */ + void fetchSurroundingNodes(const glm::dvec3& cameraPos, size_t chunkSizeInBytes, + const glm::ivec2& additionalNodes); + + /** + * Builds render data structure by traversing the Octree and checking for intersection + * with view frustum. Every vector in map contains data for one node. + * The corresponding integer key is the index where chunk should be inserted into + * streaming buffer. Calls checkNodeIntersection() for every branch. + * \pdeltaStars keeps track of how many stars that were added/removed this render + * call. + */ + std::map> traverseData(const glm::dmat4& mvp, + const glm::vec2& screenSize, int& deltaStars, gaia::RenderOption option, + float lodPixelThreshold); + + /** + * Builds full render data structure by traversing all leaves in the Octree. + */ + std::vector getAllData(gaia::RenderOption option); + + /** + * Removes all data from Octree, or only from a specific branch if specified. + * \param branchIndex defined which branch to clear if defined. + */ + void clearAllData(int branchIndex = -1); + + /** + * Write entire Octree structure to a binary file. \param writeData defines if data + * should be included or if only structure should be written to the file. + * Calls writeNodeToFile() which recursively writes all nodes. + */ + void writeToFile(std::ofstream& outFileStream, bool writeData); + + /** + * Read a constructed Octree from a file. \returns the total number of (distinct) + * stars read. + * + * \param readData defines if full data or only structure should be read. + * Calls readNodeFromFile() which recursively reads all nodes. + */ + int readFromFile(std::ifstream& inFileStream, bool readData, + const std::string& folderPath = std::string()); + + /** + * Write specified part of Octree to multiple files, including all data. + * \param branchIndex defines which branch to write. + * Clears specified branch after writing is done. + * Calls writeNodeToMultipleFiles() for the specified branch. + */ + void writeToMultipleFiles(const std::string& outFolderPath, size_t branchIndex); + + /** + * Getters. + */ + size_t numLeafNodes() const; + size_t numInnerNodes() const; + size_t totalNodes() const; + size_t totalDepth() const; + size_t maxDist() const; + size_t maxStarsPerNode() const; + size_t biggestChunkIndexInUse() const; + size_t numFreeSpotsInBuffer() const; + bool isRebuildOngoing() const; + + /** + * \returns current CPU RAM budget in bytes. + */ + long long cpuRamBudget() const; + +private: + const size_t POS_SIZE = 3; + const size_t COL_SIZE = 2; + const size_t VEL_SIZE = 3; + + // MAX_DIST [kPc] - Determines the depth of Octree together with MAX_STARS_PER_NODE. + // A smaller distance is better (i.e. a smaller total depth) and a smaller MAX_STARS + // is also better (i.e. finer borders and fewer nodes/less data needs to be uploaded + // to the GPU), but MAX_STARS still needs to be big enough to be able to swallow all + // stars that falls outside of top border nodes, otherwise it causes a stack overflow + // when building Octree. However, fewer total nodes (i.e. bigger Stars/Node) reduces + // traversing time which is preferable, especially with big datasets + // DR1_TGAS [2M] - A MAX_DIST of 5 kPc works fine with down to 1 kSPN. + // DR1_full [1.2B] - A MAX_DIST of 10 kPc works fine with most SPN. + // DR2_rv [7.2M] - A MAX_DIST of 15 kPc works fine with down to 10 kSPN. + // DR2_subset [42.9M] - A MAX_DIST of 100 kPc works fine with 20 kSPN. + // DR2_full [1.7B] - A MAX_DIST of 250 kPc works fine with 150 kSPN. + size_t MAX_DIST = 2; // [kPc] + size_t MAX_STARS_PER_NODE = 2000; + + const int DEFAULT_INDEX = -1; + const std::string BINARY_SUFFIX = ".bin"; + + /** + * \returns the correct index of child node. Maps [1,1,1] to 0 and [-1,-1,-1] to 7. + */ + size_t getChildIndex(float posX, float posY, float posZ, float origX = 0.f, + float origY = 0.f, float origZ = 0.f); + + /** + * Private help function for insert(). Inserts star into node if leaf and + * numStars < MAX_STARS_PER_NODE. If a leaf goes above the threshold it is subdivided + * into 8 new nodes. + * If node is an inner node, then star is stores in LOD cache if it is among the + * brightest stars in all children. + */ + bool insertInNode(std::shared_ptr node, + const std::vector& starValues, int depth = 1); + + /** + * Slices LOD cache data in node to the MAX_STARS_PER_NODE brightest stars. This needs + * to be called after the last star has been inserted into Octree but before it is + * saved to file(s). Slices all descendants recursively. + */ + void sliceNodeLodCache(std::shared_ptr node); + + /** + * Private help function for insertInNode(). Stores star data in node and + * keeps track of the brightest stars all children. + */ + void storeStarData(std::shared_ptr node, + const std::vector& starValues); + + /** + * Private help function for printStarsPerNode(). \returns an accumulated + * string containing all descendant nodes. + */ + std::string printStarsPerNode(std::shared_ptr node, + const std::string& prefix) const; + + /** + * Private help function for traverseData(). Recursively checks which + * nodes intersect with the view frustum (interpreted as an AABB) and decides if data + * should be optimized away or not. Keeps track of which nodes that are visible and + * loaded (if streaming). \param deltaStars keeps track of how many stars that were + * added/removed this render call. + */ + std::map> checkNodeIntersection( + std::shared_ptr node, const glm::dmat4& mvp, + const glm::vec2& screenSize, int& deltaStars, gaia::RenderOption option); + + /** + * Checks if specified node existed in cache, and removes it if that's the case. + * If node is an inner node then all children will be checked recursively as well as + * long as \param recursive is not set to false. \param deltaStars keeps track of how + * many stars that were removed. + */ + std::map> removeNodeFromCache( + std::shared_ptr node, int& deltaStars, bool recursive = true); + + /** + * Get data in node and its descendants regardless if they are visible or not. + */ + std::vector getNodeData(std::shared_ptr node, + gaia::RenderOption option); + + /** + * Clear data from node and its descendants and shrink vectors to deallocate memory. + */ + void clearNodeData(std::shared_ptr node); + + /** + * Contruct default children nodes for specified node. + */ + void createNodeChildren(std::shared_ptr node); + + /** + * Checks if node should be inserted into stream or not. \returns true if it should, + * (i.e. it doesn't already exists, there is room for it in the buffer and node data + * is loaded if streaming). \returns false otherwise. + */ + bool updateBufferIndex(std::shared_ptr node); + + /** + * Node should be inserted into stream. This function \returns the data to be + * inserted. If VBOs are used then the chunks will be appended by zeros, otherwise + * only the star data corresponding to RenderOption \param option will be inserted. + * + * \param deltaStars keeps track of how many stars that were added. + */ + std::vector constructInsertData(std::shared_ptr node, + gaia::RenderOption option, int& deltaStars); + + /** + * Write a node to outFileStream. \param writeData defines if data should be included + * or if only structure should be written. + */ + void writeNodeToFile(std::ofstream& outFileStream, std::shared_ptr node, + bool writeData); + + /** + * Read a node from file and its potential children. \param readData defines if full + * data or only structure should be read. + * \returns accumulated sum of all read stars in node and its descendants. + */ + int readNodeFromFile(std::ifstream& inFileStream, std::shared_ptr node, + bool readData); + + /** + * Write node data to a file. \param outFilePrefix specifies the accumulated path + * and name of the file. If \param threadWrites is set to true then one new thread + * will be created for each child to write its descendents. + */ + void writeNodeToMultipleFiles(const std::string& outFilePrefix, + std::shared_ptr node, bool threadWrites); + + /** + * Finds the neighboring node on the same level (or a higher level if there is no + * corresponding level) in the specified direction. Also fetches data from found node + * if it's not already loaded. \param additionalLevelsToFetch determines if any + * descendants of the found node should be fetched as well (if they exists). + */ + void findAndFetchNeighborNode(unsigned long long firstParentId, int x, int y, int z, + int additionalLevelsToFetch); + + /** + * Fetches data from all children of \param parentNode, as long as it's not already + * fetched, it exists and it can fit in RAM. + * \param additionalLevelsToFetch determines how many levels of descendants to fetch. + * If it is set to 0 no additional level will be fetched. + * If it is set to a negative value then all descendants will be fetched recursively. + * Calls fetchNodeDataFromFile() for every child that passes the tests. + */ + void fetchChildrenNodes(std::shared_ptr parentNode, + int additionalLevelsToFetch); + + /** + * Fetches data for specified node from file. + * OBS! Only call if node file exists (i.e. node has any data, node->numStars > 0) + * and is not already loaded. + */ + void fetchNodeDataFromFile(std::shared_ptr node); + + /** + * Loops though all nodes in \param nodesToRemove and clears them from RAM. + * Also checks if any ancestor should change the hasLoadedDescendant flag + * by calling propagateUnloadedNodes() with all ancestors. + */ + void removeNodesFromRam(const std::vector& nodesToRemove); + + /** + * Removes data in specified node from main memory and updates RAM budget and flags + * accordingly. + */ + void removeNode(std::shared_ptr node); + + /** + * Loops through \param ancestorNodes backwards and checks if parent node has any + * loaded descendants left. If not, then flag hasLoadedDescendant will be + * set to false for that parent node and next parent in line will be checked. + */ + void propagateUnloadedNodes(std::vector> ancestorNodes); + + std::shared_ptr _root; + std::unique_ptr _culler; + std::stack _freeSpotsInBuffer; + std::set _removedKeysInPrevCall; + std::queue _leastRecentlyFetchedNodes; + std::mutex _leastRecentlyFetchedNodesMutex; + + size_t _totalDepth = 0; + size_t _numLeafNodes = 0; + size_t _numInnerNodes = 0; + size_t _biggestChunkIndexInUse = 0; + size_t _valuesPerStar = 0; + float _minTotalPixelsLod = 0.f; + + size_t _maxStackSize = 0; + bool _rebuildBuffer = false; + bool _useVBO = false; + bool _streamOctree = false; + bool _datasetFitInMemory = false; + long long _cpuRamBudget = 0; + long long _maxCpuRamBudget = 0; + unsigned long long _parentNodeOfCamera = 8; + std::string _streamFolderPath; + size_t _traversedBranchesInRenderCall = 0; + +}; // class OctreeManager + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_GAIA___OCTREEMANAGER___H__ diff --git a/modules/gaia/rendering/renderablegaiastars.cpp b/modules/gaia/rendering/renderablegaiastars.cpp new file mode 100644 index 0000000000..ca61e2fc56 --- /dev/null +++ b/modules/gaia/rendering/renderablegaiastars.cpp @@ -0,0 +1,2529 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr const char* _loggerCat = "RenderableGaiaStars"; + + constexpr size_t PositionSize = 3; + constexpr size_t ColorSize = 2; + constexpr size_t VelocitySize = 3; + + constexpr openspace::properties::Property::PropertyInfo FilePathInfo = { + "File", + "File Path", + "The path to the file with data for the stars to be rendered." + }; + + constexpr openspace::properties::Property::PropertyInfo FileReaderOptionInfo = { + "FileReaderOption", + "File Reader Option", + "This value tells the renderable what format the input data file has. " + "'Fits' will read a FITS file, construct an Octree from it and render full " + "data. 'Speck' will read a SPECK file, construct an Octree from it and render " + "full data. 'BinaryRaw' will read a preprocessed binary file with ordered star " + "data, construct an Octree and render it. 'BinaryOctree' will read a constructed " + "Octree from binary file and render full data. 'StreamOctree' will read an index " + "file with full Octree structure and then stream nodes during runtime. (This " + "option is suited for bigger datasets.)" + }; + + constexpr openspace::properties::Property::PropertyInfo RenderOptionInfo = { + "RenderOption", + "Render Option", + "This value determines which predefined columns to use in rendering. If " + "'Static' only the position of the stars is used. 'Color' uses position + color " + "parameters and 'Motion' uses pos, color as well as velocity for the stars." + }; + + constexpr openspace::properties::Property::PropertyInfo ShaderOptionInfo = { + "ShaderOption", + "Shader Option", + "This value determines which shaders to use while rendering. If 'Point_*' is " + "chosen then gl_Points will be rendered and then spread out with a bloom " + "filter. If 'Billboard_*' is chosen then the geometry shaders will generate " + "screen-faced billboards for all stars. For '*_SSBO' the data will be stored in " + "Shader Storage Buffer Objects while '*_VBO' uses Vertex Buffer Objects for the " + "streaming. OBS! SSBO won't work on APPLE!" + }; + + constexpr openspace::properties::Property::PropertyInfo PsfTextureInfo = { + "Texture", + "Point Spread Function Texture", + "The path to the texture that should be used as a point spread function for the " + "stars." + }; + + constexpr openspace::properties::Property::PropertyInfo LuminosityMultiplierInfo = { + "LuminosityMultiplier", + "Luminosity Multiplier", + "Factor by which to multiply the luminosity with. [Works in Color and Motion " + "modes]" + }; + + constexpr openspace::properties::Property::PropertyInfo MagnitudeBoostInfo = { + "MagnitudeBoost", + "Magnitude Boost", + "Sets what percent of the star magnitude that will be used as boost to star " + "size. [Works only with billboards in Color and Motion modes]" + }; + + constexpr openspace::properties::Property::PropertyInfo CutOffThresholdInfo = { + "CutOffThreshold", + "Cut Off Threshold", + "Set threshold for when to cut off star rendering. " + "Stars closer than this threshold are given full opacity. " + "Farther away, stars dim proportionally to the 4-logarithm of their distance." + }; + + constexpr openspace::properties::Property::PropertyInfo SharpnessInfo = { + "Sharpness", + "Sharpness", + "Adjust star sharpness. [Works only with billboards]" + }; + + constexpr openspace::properties::Property::PropertyInfo BillboardSizeInfo = { + "BillboardSize", + "Billboard Size", + "Set the billboard size of all stars. [Works only with billboards]" + }; + + constexpr openspace::properties::Property::PropertyInfo CloseUpBoostDistInfo = { + "CloseUpBoostDist", + "Close-Up Boost Distance [pc]", + "Set the distance where stars starts to increase in size. Unit is Parsec." + "[Works only with billboards]" + }; + + constexpr openspace::properties::Property::PropertyInfo TmPointFilterSizeInfo = { + "FilterSize", + "Filter Size [px]", + "Set the filter size in pixels used in tonemapping for point splatting rendering." + "[Works only with points]" + }; + + constexpr openspace::properties::Property::PropertyInfo TmPointSigmaInfo = { + "Sigma", + "Normal Distribution Sigma", + "Set the normal distribution sigma used in tonemapping for point splatting " + "rendering. [Works only with points]" + }; + + constexpr openspace::properties::Property::PropertyInfo AdditionalNodesInfo = { + "AdditionalNodes", + "Additional Nodes", + "Determines how many additional nodes around the camera that will be fetched " + "from disk. The first value determines how many additional layers of parents " + "that will be fetched. The second value determines how many layers of descendant " + "that will be fetched from the found parents." + }; + + constexpr openspace::properties::Property::PropertyInfo TmPointPxThresholdInfo = { + "PixelWeightThreshold", + "Pixel Weight Threshold", + "Set the threshold for how big the elliptic weight of a pixel has to be to " + "contribute to the final elliptic shape. A smaller value gives a more visually " + "pleasing result while a bigger value will speed up the rendering on skewed " + "frustums (aka Domes)." + }; + + constexpr openspace::properties::Property::PropertyInfo ColorTextureInfo = { + "ColorMap", + "Color Texture", + "The path to the texture that is used to convert from the magnitude of the star " + "to its color. The texture is used as a one dimensional lookup function." + }; + + constexpr openspace::properties::Property::PropertyInfo FirstRowInfo = { + "FirstRow", + "First Row to Read", + "Defines the first row that will be read from the specified FITS file." + "No need to define if data already has been processed." + "[Works only with FileReaderOption::Fits]" + }; + + constexpr openspace::properties::Property::PropertyInfo LastRowInfo = { + "LastRow", + "Last Row to Read", + "Defines the last row that will be read from the specified FITS file." + "Has to be equal to or greater than FirstRow. No need to define if " + "data already has been processed." + "[Works only with FileReaderOption::Fits]" + }; + + constexpr openspace::properties::Property::PropertyInfo ColumnNamesInfo = { + "ColumnNames", + "Column Names", + "A list of strings with the names of all the columns that are to be " + "read from the specified FITS file. No need to define if data already " + "has been processed." + "[Works only with FileReaderOption::Fits]" + }; + + constexpr openspace::properties::Property::PropertyInfo NumRenderedStarsInfo = { + "NumRenderedStars", + "Rendered Stars", + "The number of rendered stars in the current frame." + }; + + constexpr openspace::properties::Property::PropertyInfo CpuRamBudgetInfo = { + "CpuRamBudget", + "CPU RAM Budget", + "Current remaining budget (bytes) on the CPU RAM for loading more node data " + "files." + }; + + constexpr openspace::properties::Property::PropertyInfo GpuStreamBudgetInfo = { + "GpuStreamBudget", + "GPU Stream Budget", + "Current remaining memory budget [in number of chunks] on the GPU for streaming " + "additional stars." + }; + + constexpr openspace::properties::Property::PropertyInfo LodPixelThresholdInfo = { + "LodPixelThreshold", + "LOD Pixel Threshold", + "The number of total pixels a nodes AABB can have in clipping space before its " + "parent is fetched as LOD cache." + }; + + constexpr openspace::properties::Property::PropertyInfo MaxGpuMemoryPercentInfo = { + "MaxGpuMemoryPercent", + "Max GPU Memory", + "Sets the max percent of existing GPU memory budget that the streaming will use." + }; + + constexpr openspace::properties::Property::PropertyInfo MaxCpuMemoryPercentInfo = { + "MaxCpuMemoryPercent", + "Max CPU Memory", + "Sets the max percent of existing CPU memory budget that the streaming of files " + "will use." + }; + + constexpr openspace::properties::Property::PropertyInfo FilterPosXInfo = { + "FilterPosX", + "PosX Threshold", + "If defined then only stars with Position X values between [min, max] " + "will be rendered (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). Measured in kiloParsec." + }; + + constexpr openspace::properties::Property::PropertyInfo FilterPosYInfo = { + "FilterPosY", + "PosY Threshold", + "If defined then only stars with Position Y values between [min, max] " + "will be rendered (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). Measured in kiloParsec." + }; + + constexpr openspace::properties::Property::PropertyInfo FilterPosZInfo = { + "FilterPosZ", + "PosZ Threshold", + "If defined then only stars with Position Z values between [min, max] " + "will be rendered (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). Measured in kiloParsec." + }; + + constexpr openspace::properties::Property::PropertyInfo FilterGMagInfo = { + "FilterGMag", + "GMag Threshold", + "If defined then only stars with G mean magnitude values between [min, max] " + "will be rendered (if min is set to 20.0 it is read as -Inf, " + "if max is set to 20.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }; + + constexpr openspace::properties::Property::PropertyInfo FilterBpRpInfo = { + "FilterBpRp", + "Bp-Rp Threshold", + "If defined then only stars with Bp-Rp color values between [min, max] " + "will be rendered (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }; + + constexpr openspace::properties::Property::PropertyInfo FilterDistInfo = { + "FilterDist", + "Dist Threshold", + "If defined then only stars with Distances values between [min, max] " + "will be rendered (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). Measured in kParsec." + }; + + constexpr openspace::properties::Property::PropertyInfo ReportGlErrorsInfo = { + "ReportGlErrors", + "Report GL Errors", + "If set to true, any OpenGL errors will be reported if encountered" + }; +} // namespace + +namespace openspace { + +documentation::Documentation RenderableGaiaStars::Documentation() { + using namespace documentation; + return { + "RenderableGaiaStars", + "gaiamission_renderablegaiastars", + { + { + "Type", + new StringEqualVerifier("RenderableGaiaStars"), + Optional::No + }, + { + FilePathInfo.identifier, + new StringVerifier, + Optional::No, + FilePathInfo.description + }, + { + FileReaderOptionInfo.identifier, + new StringInListVerifier({ + "Fits", "Speck", "BinaryRaw", "BinaryOctree", "StreamOctree" + }), + Optional::No, + FileReaderOptionInfo.description + }, + { + RenderOptionInfo.identifier, + new StringInListVerifier({ + "Static", "Color", "Motion" + }), + Optional::Yes, + RenderOptionInfo.description + }, + { + ShaderOptionInfo.identifier, + new StringInListVerifier({ + "Point_SSBO", "Point_VBO", "Billboard_SSBO", "Billboard_VBO", + "Billboard_SSBO_noFBO" + }), + Optional::Yes, + ShaderOptionInfo.description + }, + { + PsfTextureInfo.identifier, + new StringVerifier, + Optional::No, + PsfTextureInfo.description + }, + { + ColorTextureInfo.identifier, + new StringVerifier, + Optional::No, + ColorTextureInfo.description + }, + { + LuminosityMultiplierInfo.identifier, + new DoubleVerifier, + Optional::Yes, + LuminosityMultiplierInfo.description + }, + { + MagnitudeBoostInfo.identifier, + new DoubleVerifier, + Optional::Yes, + MagnitudeBoostInfo.description + }, + { + CutOffThresholdInfo.identifier, + new DoubleVerifier, + Optional::Yes, + CutOffThresholdInfo.description + }, + { + SharpnessInfo.identifier, + new DoubleVerifier, + Optional::Yes, + SharpnessInfo.description + }, + { + BillboardSizeInfo.identifier, + new DoubleVerifier, + Optional::Yes, + BillboardSizeInfo.description + }, + { + CloseUpBoostDistInfo.identifier, + new DoubleVerifier, + Optional::Yes, + CloseUpBoostDistInfo.description + }, + { + TmPointFilterSizeInfo.identifier, + new IntVerifier, + Optional::Yes, + TmPointFilterSizeInfo.description + }, + { + TmPointSigmaInfo.identifier, + new DoubleVerifier, + Optional::Yes, + TmPointSigmaInfo.description + }, + { + AdditionalNodesInfo.identifier, + new Vector2Verifier, + Optional::Yes, + AdditionalNodesInfo.description + }, + { + TmPointPxThresholdInfo.identifier, + new DoubleVerifier, + Optional::Yes, + TmPointPxThresholdInfo.description + }, + { + FirstRowInfo.identifier, + new IntVerifier, + Optional::Yes, + FirstRowInfo.description + }, + { + LastRowInfo.identifier, + new IntVerifier, + Optional::Yes, + LastRowInfo.description + }, + { + ColumnNamesInfo.identifier, + new StringListVerifier, + Optional::Yes, + ColumnNamesInfo.description + }, + { + LodPixelThresholdInfo.identifier, + new DoubleVerifier, + Optional::Yes, + LodPixelThresholdInfo.description + }, + { + MaxGpuMemoryPercentInfo.identifier, + new DoubleVerifier, + Optional::Yes, + MaxGpuMemoryPercentInfo.description + }, + { + MaxCpuMemoryPercentInfo.identifier, + new DoubleVerifier, + Optional::Yes, + MaxCpuMemoryPercentInfo.description + }, + { + FilterPosXInfo.identifier, + new Vector2Verifier, + Optional::Yes, + FilterPosXInfo.description + }, + { + FilterPosYInfo.identifier, + new Vector2Verifier, + Optional::Yes, + FilterPosYInfo.description + }, + { + FilterPosZInfo.identifier, + new Vector2Verifier, + Optional::Yes, + FilterPosZInfo.description + }, + { + FilterGMagInfo.identifier, + new Vector2Verifier, + Optional::Yes, + FilterGMagInfo.description + }, + { + FilterBpRpInfo.identifier, + new Vector2Verifier, + Optional::Yes, + FilterBpRpInfo.description + }, + { + FilterDistInfo.identifier, + new Vector2Verifier, + Optional::Yes, + FilterDistInfo.description + }, + { + ReportGlErrorsInfo.identifier, + new BoolVerifier, + Optional::Yes, + ReportGlErrorsInfo.description + } + } + }; +} + +RenderableGaiaStars::RenderableGaiaStars(const ghoul::Dictionary& dictionary) + : Renderable(dictionary) + , _filePath(FilePathInfo) + , _fileReaderOption( + FileReaderOptionInfo, + properties::OptionProperty::DisplayType::Dropdown + ) + , _renderOption(RenderOptionInfo, properties::OptionProperty::DisplayType::Dropdown) + , _shaderOption(ShaderOptionInfo, properties::OptionProperty::DisplayType::Dropdown) + , _pointSpreadFunctionTexturePath(PsfTextureInfo) + , _colorTexturePath(ColorTextureInfo) + , _luminosityMultiplier(LuminosityMultiplierInfo, 35.f, 1.f, 1000.f) + , _magnitudeBoost(MagnitudeBoostInfo, 25.f, 0.f, 100.f) + , _cutOffThreshold(CutOffThresholdInfo, 38.f, 0.f, 50.f) + , _sharpness(SharpnessInfo, 1.45f, 0.f, 5.f) + , _billboardSize(BillboardSizeInfo, 10.f, 1.f, 100.f) + , _closeUpBoostDist(CloseUpBoostDistInfo, 300.f, 1.f, 1000.f) + , _tmPointFilterSize(TmPointFilterSizeInfo, 7, 1, 19) + , _tmPointSigma(TmPointSigmaInfo, 0.7f, 0.1f, 3.f) + , _additionalNodes(AdditionalNodesInfo, glm::ivec2(1), glm::ivec2(0), glm::ivec2(4)) + , _tmPointPixelWeightThreshold(TmPointPxThresholdInfo, 0.001f, 0.000001f, 0.01f) + , _lodPixelThreshold(LodPixelThresholdInfo, 250.f, 0.f, 5000.f) + , _maxGpuMemoryPercent(MaxGpuMemoryPercentInfo, 0.45f, 0.f, 1.f) + , _maxCpuMemoryPercent(MaxCpuMemoryPercentInfo, 0.5f, 0.f, 1.f) + , _posXThreshold(FilterPosXInfo, glm::vec2(0.f), glm::vec2(-10.f), glm::vec2(10.f)) + , _posYThreshold(FilterPosYInfo, glm::vec2(0.f), glm::vec2(-10.f), glm::vec2(10.f)) + , _posZThreshold(FilterPosZInfo, glm::vec2(0.f), glm::vec2(-10.f), glm::vec2(10.f)) + , _gMagThreshold(FilterGMagInfo, glm::vec2(20.f), glm::vec2(-10.f), glm::vec2(30.f)) + , _bpRpThreshold(FilterBpRpInfo, glm::vec2(0.f), glm::vec2(-10.f), glm::vec2(30.f)) + , _distThreshold(FilterDistInfo, glm::vec2(0.f), glm::vec2(0.f), glm::vec2(100.f)) + , _firstRow(FirstRowInfo, 0, 0, 2539913) // DR1-max: 2539913 + , _lastRow(LastRowInfo, 0, 0, 2539913) + , _columnNamesList(ColumnNamesInfo) + , _nRenderedStars(NumRenderedStarsInfo, 0, 0, 2000000000) // 2 Billion stars + , _cpuRamBudgetProperty(CpuRamBudgetInfo, 0.f, 0.f, 1.f) + , _gpuStreamBudgetProperty(GpuStreamBudgetInfo, 0.f, 0.f, 1.f) + , _reportGlErrors(ReportGlErrorsInfo, false) + , _accumulatedIndices(1, 0) +{ + using File = ghoul::filesystem::File; + + documentation::testSpecificationAndThrow( + Documentation(), + dictionary, + "RenderableGaiaStars" + ); + + _filePath = absPath(dictionary.value(FilePathInfo.identifier)); + _dataFile = std::make_unique(_filePath); + _dataFile->setCallback([&](const File&) { _dataIsDirty = true; }); + + _filePath.onChange([&]() { _dataIsDirty = true; }); + addProperty(_filePath); + + _fileReaderOption.addOptions({ + { gaia::FileReaderOption::Fits, "Fits" }, + { gaia::FileReaderOption::Speck, "Speck" }, + { gaia::FileReaderOption::BinaryRaw, "BinaryRaw" }, + { gaia::FileReaderOption::BinaryOctree, "BinaryOctree" }, + { gaia::FileReaderOption::StreamOctree, "StreamOctree" } + }); + if (dictionary.hasKey(FileReaderOptionInfo.identifier)) { + const std::string fileReaderOption = dictionary.value( + FileReaderOptionInfo.identifier + ); + if (fileReaderOption == "Fits") { + _fileReaderOption = gaia::FileReaderOption::Fits; + } + else if (fileReaderOption == "Speck") { + _fileReaderOption = gaia::FileReaderOption::Speck; + } + else if (fileReaderOption == "BinaryRaw") { + _fileReaderOption = gaia::FileReaderOption::BinaryRaw; + } + else if (fileReaderOption == "BinaryOctree") { + _fileReaderOption = gaia::FileReaderOption::BinaryOctree; + } + else { + _fileReaderOption = gaia::FileReaderOption::StreamOctree; + } + } + + _renderOption.addOptions({ + { gaia::RenderOption::Static, "Static" }, + { gaia::RenderOption::Color, "Color" }, + { gaia::RenderOption::Motion, "Motion" } + }); + if (dictionary.hasKey(RenderOptionInfo.identifier)) { + const std::string renderOption = dictionary.value( + RenderOptionInfo.identifier + ); + if (renderOption == "Static") { + _renderOption = gaia::RenderOption::Static; + } + else if (renderOption == "Color") { + _renderOption = gaia::RenderOption::Color; + } + else { + _renderOption = gaia::RenderOption::Motion; + } + } + _renderOption.onChange([&]() { _buffersAreDirty = true; }); + addProperty(_renderOption); + +#ifndef __APPLE__ + _shaderOption.addOptions({ + { gaia::ShaderOption::Point_SSBO, "Point_SSBO" }, + { gaia::ShaderOption::Point_VBO, "Point_VBO" }, + { gaia::ShaderOption::Billboard_SSBO, "Billboard_SSBO" }, + { gaia::ShaderOption::Billboard_VBO, "Billboard_VBO" }, + { gaia::ShaderOption::Billboard_SSBO_noFBO, "Billboard_SSBO_noFBO" } + }); +#else // __APPLE__ + _shaderOption.addOptions({ + { gaia::ShaderOption::Point_VBO, "Point_VBO" }, + { gaia::ShaderOption::Billboard_VBO, "Billboard_VBO" }, + }); +#endif // __APPLE__ + + if (dictionary.hasKey(ShaderOptionInfo.identifier)) { + const std::string shaderOption = dictionary.value( + ShaderOptionInfo.identifier + ); + +#ifndef __APPLE__ + if (shaderOption == "Point_SSBO") { + _shaderOption = gaia::ShaderOption::Point_SSBO; + } + else if (shaderOption == "Billboard_SSBO") { + _shaderOption = gaia::ShaderOption::Billboard_SSBO; + } + else if (shaderOption == "Billboard_SSBO_noFBO") { + _shaderOption = gaia::ShaderOption::Billboard_SSBO_noFBO; + } +#endif // __APPLE__ + + if (shaderOption == "Point_VBO") { + _shaderOption = gaia::ShaderOption::Point_VBO; + } + else { + _shaderOption = gaia::ShaderOption::Billboard_VBO; + } + } + _shaderOption.onChange([&]() { + _buffersAreDirty = true; + _shadersAreDirty = true; + }); + addProperty(_shaderOption); + + _pointSpreadFunctionTexturePath = absPath(dictionary.value( + PsfTextureInfo.identifier + )); + _pointSpreadFunctionFile = std::make_unique(_pointSpreadFunctionTexturePath); + + _pointSpreadFunctionTexturePath.onChange( + [&](){ _pointSpreadFunctionTextureIsDirty = true; } + ); + _pointSpreadFunctionFile->setCallback( + [&](const File&) { _pointSpreadFunctionTextureIsDirty = true; } + ); + + _colorTexturePath = absPath(dictionary.value( + ColorTextureInfo.identifier + )); + _colorTextureFile = std::make_unique(_colorTexturePath); + _colorTexturePath.onChange([&]() { _colorTextureIsDirty = true; }); + _colorTextureFile->setCallback([&](const File&) { _colorTextureIsDirty = true; }); + + if (dictionary.hasKey(LuminosityMultiplierInfo.identifier)) { + _luminosityMultiplier = static_cast( + dictionary.value(LuminosityMultiplierInfo.identifier) + ); + } + + if (dictionary.hasKey(MagnitudeBoostInfo.identifier)) { + _magnitudeBoost = static_cast( + dictionary.value(MagnitudeBoostInfo.identifier) + ); + } + + if (dictionary.hasKey(CutOffThresholdInfo.identifier)) { + _cutOffThreshold = static_cast( + dictionary.value(CutOffThresholdInfo.identifier) + ); + } + + if (dictionary.hasKey(SharpnessInfo.identifier)) { + _sharpness = static_cast( + dictionary.value(SharpnessInfo.identifier) + ); + } + + if (dictionary.hasKey(BillboardSizeInfo.identifier)) { + _billboardSize = static_cast( + dictionary.value(BillboardSizeInfo.identifier) + ); + } + + if (dictionary.hasKey(CloseUpBoostDistInfo.identifier)) { + _closeUpBoostDist = static_cast( + dictionary.value(CloseUpBoostDistInfo.identifier) + ); + } + + if (dictionary.hasKey(TmPointFilterSizeInfo.identifier)) { + _tmPointFilterSize = static_cast( + dictionary.value(TmPointFilterSizeInfo.identifier) + ); + } + + if (dictionary.hasKey(TmPointSigmaInfo.identifier)) { + _tmPointSigma = static_cast( + dictionary.value(TmPointSigmaInfo.identifier) + ); + } + if (dictionary.hasKey(TmPointPxThresholdInfo.identifier)) { + _tmPointPixelWeightThreshold = static_cast( + dictionary.value(TmPointPxThresholdInfo.identifier) + ); + } + + if (dictionary.hasKey(AdditionalNodesInfo.identifier)) { + _additionalNodes = static_cast( + dictionary.value(AdditionalNodesInfo.identifier) + ); + } + + if (dictionary.hasKey(LodPixelThresholdInfo.identifier)) { + _lodPixelThreshold = static_cast( + dictionary.value(LodPixelThresholdInfo.identifier) + ); + } + + if (dictionary.hasKey(MaxGpuMemoryPercentInfo.identifier)) { + _maxGpuMemoryPercent = static_cast( + dictionary.value(MaxGpuMemoryPercentInfo.identifier) + ); + } + _maxGpuMemoryPercent.onChange([&]() { + if (_ssboData != 0) { + glDeleteBuffers(1, &_ssboData); + glGenBuffers(1, &_ssboData); + LDEBUG(fmt::format( + "Re-generating Data Shader Storage Buffer Object id '{}'", _ssboData + )); + } + + // Find out our new budget. Use dedicated video memory instead of current + // available to always be consistant with previous call(s). + GLint nDedicatedVidMemoryInKB = 0; + glGetIntegerv(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX, &nDedicatedVidMemoryInKB); + float dedicatedVidMem = static_cast( + static_cast(nDedicatedVidMemoryInKB) * 1024 + ); + + // TODO: Need to fix what happens if we can't query! For now use 2 GB by default. + _gpuMemoryBudgetInBytes = dedicatedVidMem > 0 ? + static_cast(dedicatedVidMem * _maxGpuMemoryPercent) : + 2147483648; + _buffersAreDirty = true; + _maxStreamingBudgetInBytes = 0; + }); + + if (dictionary.hasKey(MaxCpuMemoryPercentInfo.identifier)) { + _maxCpuMemoryPercent = static_cast( + dictionary.value(MaxCpuMemoryPercentInfo.identifier) + ); + } + + if (dictionary.hasKey(FilterPosXInfo.identifier)) { + _posXThreshold = dictionary.value(FilterPosXInfo.identifier); + } + addProperty(_posXThreshold); + + if (dictionary.hasKey(FilterPosYInfo.identifier)) { + _posXThreshold = dictionary.value(FilterPosYInfo.identifier); + } + addProperty(_posYThreshold); + + if (dictionary.hasKey(FilterPosZInfo.identifier)) { + _posZThreshold = dictionary.value(FilterPosZInfo.identifier); + } + addProperty(_posZThreshold); + + if (dictionary.hasKey(FilterGMagInfo.identifier)) { + _gMagThreshold = dictionary.value(FilterGMagInfo.identifier); + } + addProperty(_gMagThreshold); + + if (dictionary.hasKey(FilterBpRpInfo.identifier)) { + _bpRpThreshold = dictionary.value(FilterBpRpInfo.identifier); + } + addProperty(_bpRpThreshold); + + if (dictionary.hasKey(FilterDistInfo.identifier)) { + _distThreshold = dictionary.value(FilterDistInfo.identifier); + } + addProperty(_distThreshold); + + // Only add properties correlated to fits files if we're reading from a fits file. + if (_fileReaderOption == gaia::FileReaderOption::Fits) { + if (dictionary.hasKey(FirstRowInfo.identifier)) { + _firstRow = static_cast( + dictionary.value(FirstRowInfo.identifier) + ); + } + _firstRow.onChange([&]() { _dataIsDirty = true; }); + addProperty(_firstRow); + + if (dictionary.hasKey(LastRowInfo.identifier)) { + _lastRow = static_cast(dictionary.value(LastRowInfo.identifier)); + } + _lastRow.onChange([&]() { _dataIsDirty = true; }); + addProperty(_lastRow); + + if (dictionary.hasKey(ColumnNamesInfo.identifier)) { + ghoul::Dictionary tmpDict = dictionary.value( + ColumnNamesInfo.identifier + ); + + // Ugly fix for ASCII sorting when there are more columns read than 10. + std::set intKeys; + for (const std::string& key : tmpDict.keys()) { + intKeys.insert(std::stoi(key)); + } + + for (int key : intKeys) { + _columnNames.push_back(tmpDict.value(std::to_string(key))); + } + + // Copy values to the StringListproperty to be shown in the Property list. + _columnNamesList = _columnNames; + // OBS - This is not used atm! + } + + if (_firstRow > _lastRow) { + throw ghoul::RuntimeError("User defined FirstRow is bigger than LastRow."); + } + } + + if (dictionary.hasKey(ReportGlErrorsInfo.identifier)) { + _reportGlErrors = dictionary.value(ReportGlErrorsInfo.identifier); + } + addProperty(_reportGlErrors); + + // Add a read-only property for the number of rendered stars per frame. + _nRenderedStars.setReadOnly(true); + addProperty(_nRenderedStars); + + // Add CPU RAM Budget Property and GPU Stream Budget Property to menu. + _cpuRamBudgetProperty.setReadOnly(true); + addProperty(_cpuRamBudgetProperty); + _gpuStreamBudgetProperty.setReadOnly(true); + addProperty(_gpuStreamBudgetProperty); +} + +RenderableGaiaStars::~RenderableGaiaStars() {} + +bool RenderableGaiaStars::isReady() const { + return _program && _programTM; +} + +void RenderableGaiaStars::initializeGL() { + //using IgnoreError = ghoul::opengl::ProgramObject::IgnoreError; + //_program->setIgnoreUniformLocationError(IgnoreError::Yes); + + // Add common properties to menu. + addProperty(_colorTexturePath); + addProperty(_luminosityMultiplier); + addProperty(_cutOffThreshold); + addProperty(_lodPixelThreshold); + addProperty(_maxGpuMemoryPercent); + + // Construct shader program depending on user-defined shader option. + const int option = _shaderOption; + switch (option) { + case gaia::ShaderOption::Point_SSBO: { + _program = ghoul::opengl::ProgramObject::Build( + "GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_ssbo_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_point_fs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_point_ge.glsl") + ); + _uniformCache.maxStarsPerNode = _program->uniformLocation("maxStarsPerNode"); + _uniformCache.valuesPerStar = _program->uniformLocation("valuesPerStar"); + _uniformCache.nChunksToRender = _program->uniformLocation("nChunksToRender"); + + _programTM = global::renderEngine.buildRenderProgram( + "ToneMapping", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_point_fs.glsl") + ); + _uniformCacheTM.screenSize = _programTM->uniformLocation("screenSize"); + _uniformCacheTM.filterSize = _programTM->uniformLocation("filterSize"); + _uniformCacheTM.sigma = _programTM->uniformLocation("sigma"); + _uniformCacheTM.projection = _programTM->uniformLocation("projection"); + _uniformCacheTM.pixelWeightThreshold = + _programTM->uniformLocation("pixelWeightThreshold"); + + addProperty(_tmPointFilterSize); + addProperty(_tmPointSigma); + addProperty(_tmPointPixelWeightThreshold); + break; + } + case gaia::ShaderOption::Point_VBO: { + _program = ghoul::opengl::ProgramObject::Build( + "GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_vbo_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_point_fs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_point_ge.glsl") + ); + + _programTM = global::renderEngine.buildRenderProgram("ToneMapping", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_point_fs.glsl") + ); + _uniformCacheTM.screenSize = _programTM->uniformLocation("screenSize"); + _uniformCacheTM.filterSize = _programTM->uniformLocation("filterSize"); + _uniformCacheTM.sigma = _programTM->uniformLocation("sigma"); + _uniformCacheTM.projection = _programTM->uniformLocation("projection"); + _uniformCacheTM.pixelWeightThreshold = + _programTM->uniformLocation("pixelWeightThreshold"); + + addProperty(_tmPointFilterSize); + addProperty(_tmPointSigma); + addProperty(_tmPointPixelWeightThreshold); + break; + } + case gaia::ShaderOption::Billboard_SSBO: { + _program = ghoul::opengl::ProgramObject::Build( + "GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_ssbo_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_fs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_ge.glsl") + ); + _uniformCache.cameraPos = _program->uniformLocation("cameraPos"); + _uniformCache.cameraLookUp = _program->uniformLocation("cameraLookUp"); + _uniformCache.magnitudeBoost = _program->uniformLocation("magnitudeBoost"); + _uniformCache.sharpness = _program->uniformLocation("sharpness"); + _uniformCache.billboardSize = _program->uniformLocation("billboardSize"); + _uniformCache.closeUpBoostDist = _program->uniformLocation( + "closeUpBoostDist" + ); + _uniformCache.psfTexture = _program->uniformLocation("psfTexture"); + + _uniformCache.maxStarsPerNode = _program->uniformLocation("maxStarsPerNode"); + _uniformCache.valuesPerStar = _program->uniformLocation("valuesPerStar"); + _uniformCache.nChunksToRender = _program->uniformLocation("nChunksToRender"); + + _programTM = global::renderEngine.buildRenderProgram( + "ToneMapping", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_vs.glsl"), + absPath( + "${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_billboard_fs.glsl" + ) + ); + + addProperty(_magnitudeBoost); + addProperty(_sharpness); + addProperty(_billboardSize); + addProperty(_closeUpBoostDist); + //addProperty(_pointSpreadFunctionTexturePath); + break; + } + case gaia::ShaderOption::Billboard_SSBO_noFBO: { + _program = global::renderEngine.buildRenderProgram("GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_ssbo_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_nofbo_fs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_ge.glsl") + ); + _uniformCache.cameraPos = _program->uniformLocation("cameraPos"); + _uniformCache.cameraLookUp = _program->uniformLocation("cameraLookUp"); + _uniformCache.magnitudeBoost = _program->uniformLocation("magnitudeBoost"); + _uniformCache.sharpness = _program->uniformLocation("sharpness"); + _uniformCache.billboardSize = _program->uniformLocation("billboardSize"); + _uniformCache.closeUpBoostDist = _program->uniformLocation( + "closeUpBoostDist" + ); + _uniformCache.psfTexture = _program->uniformLocation("psfTexture"); + + _uniformCache.maxStarsPerNode = _program->uniformLocation("maxStarsPerNode"); + _uniformCache.valuesPerStar = _program->uniformLocation("valuesPerStar"); + _uniformCache.nChunksToRender = _program->uniformLocation("nChunksToRender"); + + addProperty(_magnitudeBoost); + addProperty(_sharpness); + addProperty(_billboardSize); + addProperty(_closeUpBoostDist); + break; + } + case gaia::ShaderOption::Billboard_VBO: { + _program = ghoul::opengl::ProgramObject::Build( + "GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_vbo_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_fs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_ge.glsl") + ); + _uniformCache.cameraPos = _program->uniformLocation("cameraPos"); + _uniformCache.cameraLookUp = _program->uniformLocation("cameraLookUp"); + _uniformCache.magnitudeBoost = _program->uniformLocation("magnitudeBoost"); + _uniformCache.sharpness = _program->uniformLocation("sharpness"); + _uniformCache.billboardSize = _program->uniformLocation("billboardSize"); + _uniformCache.closeUpBoostDist = _program->uniformLocation( + "closeUpBoostDist" + ); + _uniformCache.psfTexture = _program->uniformLocation("psfTexture"); + + _programTM = global::renderEngine.buildRenderProgram("ToneMapping", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_vs.glsl"), + absPath( + "${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_billboard_fs.glsl" + ) + ); + + addProperty(_magnitudeBoost); + addProperty(_sharpness); + addProperty(_billboardSize); + addProperty(_closeUpBoostDist); + //addProperty(_pointSpreadFunctionTexturePath); + break; + } + } + + // Common uniforms for all shaders: + _uniformCache.model = _program->uniformLocation("model"); + _uniformCache.view = _program->uniformLocation("view"); + _uniformCache.projection = _program->uniformLocation("projection"); + _uniformCache.time = _program->uniformLocation("time"); + _uniformCache.renderOption = _program->uniformLocation("renderOption"); + _uniformCache.viewScaling = _program->uniformLocation("viewScaling"); + _uniformCache.cutOffThreshold = _program->uniformLocation("cutOffThreshold"); + _uniformCache.luminosityMultiplier = _program->uniformLocation( + "luminosityMultiplier" + ); + _uniformCache.colorTexture = _program->uniformLocation("colorTexture"); + + _uniformFilterCache.posXThreshold = _program->uniformLocation("posXThreshold"); + _uniformFilterCache.posYThreshold = _program->uniformLocation("posYThreshold"); + _uniformFilterCache.posZThreshold = _program->uniformLocation("posZThreshold"); + _uniformFilterCache.gMagThreshold = _program->uniformLocation("gMagThreshold"); + _uniformFilterCache.bpRpThreshold = _program->uniformLocation("bpRpThreshold"); + _uniformFilterCache.distThreshold = _program->uniformLocation("distThreshold"); + + _uniformCacheTM.renderedTexture = _programTM->uniformLocation("renderedTexture"); + + + // Find out how much GPU memory this computer has (Nvidia cards). + GLint nDedicatedVidMemoryInKB = 0; + glGetIntegerv(GL_GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX, &nDedicatedVidMemoryInKB); + GLint nTotalMemoryInKB = 0; + glGetIntegerv(GL_GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX, &nTotalMemoryInKB); + GLint nCurrentAvailMemoryInKB = 0; + glGetIntegerv( + GL_GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX, + &nCurrentAvailMemoryInKB + ); + + LDEBUG(fmt::format( + "nDedicatedVidMemoryKB: {} - nTotalMemoryKB: {} - nCurrentAvailMemoryKB: {}", + nDedicatedVidMemoryInKB, nTotalMemoryInKB, nCurrentAvailMemoryInKB + )); + + // Set ceiling for video memory to use in streaming. + float dedicatedVidMem = static_cast( + static_cast(nDedicatedVidMemoryInKB) * 1024 + ); + // TODO: Need to fix what happens if we can't query! For now use 2 GB by default. + _gpuMemoryBudgetInBytes = dedicatedVidMem > 0 ? + static_cast(dedicatedVidMem * _maxGpuMemoryPercent) : + 2147483648; + + // Set ceiling for how much of the installed CPU RAM to use for streaming + long long installedRam = static_cast(CpuCap.installedMainMemory()) * + 1024 * 1024; + // TODO: What to do if we can't query? As for now we use 4 GB by default. + _cpuRamBudgetInBytes = installedRam > 0 ? + static_cast(static_cast(installedRam) * _maxCpuMemoryPercent) : + 4294967296; + _cpuRamBudgetProperty.setMaxValue(static_cast(_cpuRamBudgetInBytes)); + + LDEBUG(fmt::format( + "GPU Memory Budget (bytes): {} - CPU RAM Budget (bytes): {}", + _gpuMemoryBudgetInBytes, _cpuRamBudgetInBytes + )); +} + +void RenderableGaiaStars::deinitializeGL() { + if (_vboPos != 0) { + glDeleteBuffers(1, &_vboPos); + _vboPos = 0; + } + if (_vboCol != 0) { + glDeleteBuffers(1, &_vboCol); + _vboCol = 0; + } + if (_vboVel != 0) { + glDeleteBuffers(1, &_vboVel); + _vboVel = 0; + } + if (_ssboIdx != 0) { + glDeleteBuffers(1, &_ssboIdx); + _ssboIdx = 0; + glDeleteBuffers(1, &_ssboData); + _ssboData = 0; + } + if (_vao != 0) { + glDeleteVertexArrays(1, &_vao); + _vao = 0; + } + if (_vaoEmpty != 0) { + glDeleteVertexArrays(1, &_vaoEmpty); + _vaoEmpty = 0; + } + + glDeleteBuffers(1, &_vboQuad); + _vboQuad = 0; + glDeleteVertexArrays(1, &_vaoQuad); + _vaoQuad = 0; + glDeleteFramebuffers(1, &_fbo); + _fbo = 0; + + _dataFile = nullptr; + _pointSpreadFunctionTexture = nullptr; + _colorTexture = nullptr; + _fboTexture = nullptr; + + if (_program) { + global::renderEngine.removeRenderProgram(_program.get()); + _program = nullptr; + } + if (_programTM) { + global::renderEngine.removeRenderProgram(_programTM.get()); + _programTM = nullptr; + } +} + +void RenderableGaiaStars::render(const RenderData& data, RendererTasks&) { + checkGlErrors("Before render"); + + // Save current FBO. + GLint defaultFbo; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &defaultFbo); + + glm::dmat4 model = glm::translate(glm::dmat4(1.0), data.modelTransform.translation) * + glm::dmat4(data.modelTransform.rotation) * + glm::dmat4(glm::scale(glm::dmat4(1.0), glm::dvec3(data.modelTransform.scale))); + + float viewScaling = data.camera.scaling(); + glm::dmat4 view = data.camera.combinedViewMatrix(); + glm::dmat4 projection = data.camera.projectionMatrix(); + + glm::dmat4 modelViewProjMat = projection * view * model; + glm::vec2 screenSize = glm::vec2(global::renderEngine.renderingResolution()); + + // Wait until camera has stabilized before we traverse the Octree/stream from files. + const double rotationDiff = abs(length(_previousCameraRotation) - + length(data.camera.rotationQuaternion())); + if (_firstDrawCalls && rotationDiff > 1e-10) { + _previousCameraRotation = data.camera.rotationQuaternion(); + return; + } + else _firstDrawCalls = false; + + // Update which nodes that are stored in memory as the camera moves around + // (if streaming) + if (_fileReaderOption == gaia::FileReaderOption::StreamOctree) { + glm::dvec3 cameraPos = data.camera.positionVec3(); + size_t chunkSizeBytes = _chunkSize * sizeof(GLfloat); + _octreeManager.fetchSurroundingNodes(cameraPos, chunkSizeBytes, _additionalNodes); + + // Update CPU Budget property. + _cpuRamBudgetProperty = static_cast(_octreeManager.cpuRamBudget()); + } + + // Traverse Octree and build a map with new nodes to render, uses mvp matrix to decide + const int renderOption = _renderOption; + int deltaStars = 0; + std::map> updateData = _octreeManager.traverseData( + modelViewProjMat, + screenSize, + deltaStars, + gaia::RenderOption(renderOption), + _lodPixelThreshold + ); + + // Update number of rendered stars. + _nStarsToRender += deltaStars; + _nRenderedStars = _nStarsToRender; + + // Update GPU Stream Budget property. + _gpuStreamBudgetProperty = static_cast(_octreeManager.numFreeSpotsInBuffer()); + + int nChunksToRender = static_cast(_octreeManager.biggestChunkIndexInUse()); + int maxStarsPerNode = static_cast(_octreeManager.maxStarsPerNode()); + int valuesPerStar = static_cast(_nRenderValuesPerStar); + + // Switch rendering technique depending on user-defined shader option. + const int shaderOption = _shaderOption; + if (shaderOption == gaia::ShaderOption::Billboard_SSBO || + shaderOption == gaia::ShaderOption::Point_SSBO || + shaderOption == gaia::ShaderOption::Billboard_SSBO_noFBO) + { +#ifndef __APPLE__ + //------------------------ RENDER WITH SSBO --------------------------- + // Update SSBO Index array with accumulated stars in all chunks. + glBindBuffer(GL_SHADER_STORAGE_BUFFER, _ssboIdx); + int lastValue = _accumulatedIndices.back(); + _accumulatedIndices.resize(nChunksToRender + 1, lastValue); + + // Update vector with accumulated indices. + for (const auto& [offset, subData] : updateData) { + int newValue = static_cast(subData.size() / _nRenderValuesPerStar) + + _accumulatedIndices[offset]; + int changeInValue = newValue - _accumulatedIndices[offset + 1]; + _accumulatedIndices[offset + 1] = newValue; + // Propagate change. + for (int i = offset + 1; i < nChunksToRender; ++i) { + _accumulatedIndices[i + 1] += changeInValue; + } + } + + // Fix number of stars rendered if it doesn't correspond to our buffers. + if (_accumulatedIndices.back() != _nStarsToRender) { + _nStarsToRender = _accumulatedIndices.back(); + _nRenderedStars = _nStarsToRender; + } + + size_t indexBufferSize = _accumulatedIndices.size() * sizeof(GLint); + + // Update SSBO Index (stars per chunk). + glBufferData( + GL_SHADER_STORAGE_BUFFER, + indexBufferSize, + _accumulatedIndices.data(), + GL_STREAM_DRAW + ); + + // Use orphaning strategy for data SSBO. + glBindBuffer(GL_SHADER_STORAGE_BUFFER, _ssboData); + + glBufferData( + GL_SHADER_STORAGE_BUFFER, + _maxStreamingBudgetInBytes, + nullptr, + GL_STREAM_DRAW + ); + + // Update SSBO with one insert per chunk/node. + // The key in map holds the offset index. + for (const auto &[offset, subData] : updateData) { + // We don't need to fill chunk with zeros for SSBOs! + // Just check if we have any values to update. + if (!subData.empty()) { + glBufferSubData( + GL_SHADER_STORAGE_BUFFER, + offset * _chunkSize * sizeof(GLfloat), + subData.size() * sizeof(GLfloat), + subData.data() + ); + } + } + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); +#endif // !__APPLE__ + } + else { + //---------------------- RENDER WITH VBO ----------------------------- + // Update VBOs with new nodes. + // This will overwrite old data that's not visible anymore as well. + glBindVertexArray(_vao); + + // Always update Position VBO. + glBindBuffer(GL_ARRAY_BUFFER, _vboPos); + float posMemoryShare = static_cast(PositionSize) / _nRenderValuesPerStar; + size_t posChunkSize = maxStarsPerNode * PositionSize; + long long posStreamingBudget = static_cast( + _maxStreamingBudgetInBytes * posMemoryShare + ); + + // Use buffer orphaning to update a subset of total data. + glBufferData( + GL_ARRAY_BUFFER, + posStreamingBudget, + nullptr, + GL_STREAM_DRAW + ); + + // Update buffer with one insert per chunk/node. + //The key in map holds the offset index. + for (const auto& [offset, subData] : updateData) { + // Fill chunk by appending zeroes so we overwrite possible earlier values. + // Only required when removing nodes because chunks are filled up in octree + // fetch on add. + std::vector vectorData(subData.begin(), subData.end()); + vectorData.resize(posChunkSize, 0.f); + glBufferSubData( + GL_ARRAY_BUFFER, + offset * posChunkSize * sizeof(GLfloat), + posChunkSize * sizeof(GLfloat), + vectorData.data() + ); + } + + // Update Color VBO if render option is 'Color' or 'Motion'. + if (renderOption != gaia::RenderOption::Static) { + glBindBuffer(GL_ARRAY_BUFFER, _vboCol); + float colMemoryShare = static_cast(ColorSize) / _nRenderValuesPerStar; + size_t colChunkSize = maxStarsPerNode * ColorSize; + long long colStreamingBudget = static_cast( + _maxStreamingBudgetInBytes * colMemoryShare + ); + + // Use buffer orphaning to update a subset of total data. + glBufferData( + GL_ARRAY_BUFFER, + colStreamingBudget, + nullptr, + GL_STREAM_DRAW + ); + + // Update buffer with one insert per chunk/node. + //The key in map holds the offset index. + for (const auto& [offset, subData] : updateData) { + // Fill chunk by appending zeroes so we overwrite possible earlier values. + std::vector vectorData(subData.begin(), subData.end()); + vectorData.resize(posChunkSize + colChunkSize, 0.f); + glBufferSubData( + GL_ARRAY_BUFFER, + offset * colChunkSize * sizeof(GLfloat), + colChunkSize * sizeof(GLfloat), + vectorData.data() + posChunkSize + ); + } + + // Update Velocity VBO if specified. + if (renderOption == gaia::RenderOption::Motion) { + glBindBuffer(GL_ARRAY_BUFFER, _vboVel); + float velMemoryShare = static_cast(VelocitySize) / + _nRenderValuesPerStar; + size_t velChunkSize = maxStarsPerNode * VelocitySize; + long long velStreamingBudget = static_cast( + _maxStreamingBudgetInBytes * velMemoryShare + ); + + // Use buffer orphaning to update a subset of total data. + glBufferData( + GL_ARRAY_BUFFER, + velStreamingBudget, + nullptr, + GL_STREAM_DRAW + ); + + // Update buffer with one insert per chunk/node. + //The key in map holds the offset index. + for (const auto& [offset, subData] : updateData) { + // Fill chunk by appending zeroes. + std::vector vectorData(subData.begin(), subData.end()); + vectorData.resize(_chunkSize, 0.f); + glBufferSubData( + GL_ARRAY_BUFFER, + offset * velChunkSize * sizeof(GLfloat), + velChunkSize * sizeof(GLfloat), + vectorData.data() + posChunkSize + colChunkSize + ); + } + } + } + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + } + + checkGlErrors("After buffer updates"); + + // Activate shader program and send uniforms. + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + glDepthMask(false); + _program->activate(); + + _program->setUniform(_uniformCache.model, model); + _program->setUniform(_uniformCache.view, view); + _program->setUniform(_uniformCache.projection, projection); + _program->setUniform( + _uniformCache.time, + static_cast(data.time.j2000Seconds()) + ); + _program->setUniform(_uniformCache.renderOption, _renderOption); + _program->setUniform(_uniformCache.viewScaling, viewScaling); + _program->setUniform(_uniformCache.cutOffThreshold, _cutOffThreshold); + _program->setUniform(_uniformCache.luminosityMultiplier, _luminosityMultiplier); + + // Send filterValues. + _program->setUniform(_uniformFilterCache.posXThreshold, _posXThreshold); + _program->setUniform(_uniformFilterCache.posYThreshold, _posYThreshold); + _program->setUniform(_uniformFilterCache.posZThreshold, _posZThreshold); + _program->setUniform(_uniformFilterCache.gMagThreshold, _gMagThreshold); + _program->setUniform(_uniformFilterCache.bpRpThreshold, _bpRpThreshold); + _program->setUniform(_uniformFilterCache.distThreshold, _distThreshold); + + ghoul::opengl::TextureUnit colorUnit; + if (_colorTexture) { + colorUnit.activate(); + _colorTexture->bind(); + _program->setUniform(_uniformCache.colorTexture, colorUnit); + } + + // Specify how many stars we will render. Will be overwritten if rendering billboards + GLsizei nShaderCalls = _nStarsToRender; + + ghoul::opengl::TextureUnit psfUnit; + switch (shaderOption) { + case gaia::ShaderOption::Point_SSBO: { + _program->setUniform(_uniformCache.maxStarsPerNode, maxStarsPerNode); + _program->setUniform(_uniformCache.valuesPerStar, valuesPerStar); + _program->setUniform(_uniformCache.nChunksToRender, nChunksToRender); + break; + } + case gaia::ShaderOption::Point_VBO: { + // Specify how many potential stars we have to render. + nShaderCalls = maxStarsPerNode * nChunksToRender; + break; + } + case gaia::ShaderOption::Billboard_SSBO: + case gaia::ShaderOption::Billboard_SSBO_noFBO: { + _program->setUniform( + _uniformCache.cameraPos, + data.camera.positionVec3() + ); + _program->setUniform( + _uniformCache.cameraLookUp, + data.camera.lookUpVectorWorldSpace() + ); + _program->setUniform(_uniformCache.maxStarsPerNode, maxStarsPerNode); + _program->setUniform(_uniformCache.valuesPerStar, valuesPerStar); + _program->setUniform(_uniformCache.nChunksToRender, nChunksToRender); + + _program->setUniform(_uniformCache.closeUpBoostDist, + _closeUpBoostDist * static_cast(distanceconstants::Parsec) + ); + _program->setUniform(_uniformCache.billboardSize, _billboardSize); + _program->setUniform(_uniformCache.magnitudeBoost, _magnitudeBoost); + _program->setUniform(_uniformCache.sharpness, _sharpness); + + psfUnit.activate(); + _pointSpreadFunctionTexture->bind(); + _program->setUniform(_uniformCache.psfTexture, psfUnit); + break; + } + case gaia::ShaderOption::Billboard_VBO: { + _program->setUniform( + _uniformCache.cameraPos, + data.camera.positionVec3() + ); + _program->setUniform( + _uniformCache.cameraLookUp, + data.camera.lookUpVectorWorldSpace() + ); + _program->setUniform(_uniformCache.closeUpBoostDist, + _closeUpBoostDist * static_cast(distanceconstants::Parsec) + ); + _program->setUniform(_uniformCache.billboardSize, _billboardSize); + _program->setUniform(_uniformCache.magnitudeBoost, _magnitudeBoost); + _program->setUniform(_uniformCache.sharpness, _sharpness); + + psfUnit.activate(); + _pointSpreadFunctionTexture->bind(); + _program->setUniform(_uniformCache.psfTexture, psfUnit); + + // Specify how many potential stars we have to render. + nShaderCalls = maxStarsPerNode * nChunksToRender; + break; + } + } + + if (shaderOption != gaia::ShaderOption::Billboard_SSBO_noFBO) { + // Render to FBO. + glBindFramebuffer(GL_FRAMEBUFFER, _fbo); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + + //glEnable(GL_PROGRAM_POINT_SIZE); + // A non-zero named vao MUST ALWAYS be bound! + if (_useVBO) { + glBindVertexArray(_vao); + } + else { + glBindVertexArray(_vaoEmpty); + } + + glDrawArrays(GL_POINTS, 0, nShaderCalls); + glBindVertexArray(0); + //glDisable(GL_PROGRAM_POINT_SIZE); + _program->deactivate(); + + if (shaderOption != gaia::ShaderOption::Billboard_SSBO_noFBO) { + // Use ToneMapping shaders and render to default FBO again! + _programTM->activate(); + + glBindFramebuffer(GL_FRAMEBUFFER, defaultFbo); + + ghoul::opengl::TextureUnit fboTexUnit; + if (_fboTexture) { + fboTexUnit.activate(); + _fboTexture->bind(); + _programTM->setUniform(_uniformCacheTM.renderedTexture, fboTexUnit); + } + + if (shaderOption == gaia::ShaderOption::Point_SSBO || + shaderOption == gaia::ShaderOption::Point_VBO) + { + _programTM->setUniform(_uniformCacheTM.screenSize, screenSize); + _programTM->setUniform(_uniformCacheTM.filterSize, _tmPointFilterSize); + _programTM->setUniform(_uniformCacheTM.sigma, _tmPointSigma); + _programTM->setUniform(_uniformCacheTM.projection, projection); + _programTM->setUniform( + _uniformCacheTM.pixelWeightThreshold, + _tmPointPixelWeightThreshold + ); + } + + glBindVertexArray(_vaoQuad); + glDrawArrays(GL_TRIANGLES, 0, 6); // 2 triangles + glBindVertexArray(0); + + _programTM->deactivate(); + } + + glDepthMask(true); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + checkGlErrors("After render"); +} + +void RenderableGaiaStars::checkGlErrors(const std::string& identifier) const { + if (_reportGlErrors) { + GLenum error = glGetError(); + if (error != GL_NO_ERROR) { + switch (error) { + case GL_INVALID_ENUM: + LINFO(identifier + " - GL_INVALID_ENUM"); + break; + case GL_INVALID_VALUE: + LINFO(identifier + " - GL_INVALID_VALUE"); + break; + case GL_INVALID_OPERATION: + LINFO(identifier + " - GL_INVALID_OPERATION"); + break; + case GL_INVALID_FRAMEBUFFER_OPERATION: + LINFO(identifier + " - GL_INVALID_FRAMEBUFFER_OPERATION"); + break; + case GL_OUT_OF_MEMORY: + LINFO(identifier + " - GL_OUT_OF_MEMORY"); + break; + default: + LINFO(identifier + " - Unknown error"); + break; + } + } + } +} + +void RenderableGaiaStars::update(const UpdateData&) { + const int shaderOption = _shaderOption; + const int renderOption = _renderOption; + + // Don't update anything if we are in the middle of a rebuild. + if (_octreeManager.isRebuildOngoing()) { + return; + } + + if (_dataIsDirty) { + LDEBUG("Regenerating data"); + // Reload data file. This may reconstruct the Octree as well. + bool success = readDataFile(); + if (!success) { + throw ghoul::RuntimeError("Error loading Gaia Star data"); + } + _dataIsDirty = false; + // Make sure we regenerate buffers if data has reloaded! + _buffersAreDirty = true; + } + + if (_program->isDirty() || _shadersAreDirty) { + switch (shaderOption) { + case gaia::ShaderOption::Point_SSBO: { +#ifndef __APPLE__ + std::unique_ptr program = + ghoul::opengl::ProgramObject::Build( + "GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_ssbo_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_point_fs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_point_ge.glsl") + ); + if (_program) { + global::renderEngine.removeRenderProgram(_program.get()); + } + _program = std::move(program); + + _uniformCache.maxStarsPerNode = _program->uniformLocation( + "maxStarsPerNode" + ); + _uniformCache.valuesPerStar = _program->uniformLocation( + "valuesPerStar" + ); + _uniformCache.nChunksToRender = _program->uniformLocation( + "nChunksToRender" + ); + + // If rebuild was triggered by switching ShaderOption then ssboBinding may + // not have been initialized yet. Binding will happen in later in + // buffersAredirty. + if (!_shadersAreDirty) { + _program->setSsboBinding( + "ssbo_idx_data", + _ssboIdxBinding->bindingNumber() + ); + _program->setSsboBinding( + "ssbo_comb_data", + _ssboDataBinding->bindingNumber() + ); + } + if (hasProperty(&_magnitudeBoost)) { + removeProperty(_magnitudeBoost); + } + if (hasProperty(&_sharpness)) { + removeProperty(_sharpness); + } + if (hasProperty(&_billboardSize)) { + removeProperty(_billboardSize); + } + if (hasProperty(&_closeUpBoostDist)) { + removeProperty(_closeUpBoostDist); + } + if (hasProperty(&_pointSpreadFunctionTexturePath)) { + removeProperty(_pointSpreadFunctionTexturePath); + } + if (!hasProperty(&_tmPointFilterSize)) { + addProperty(_tmPointFilterSize); + } + if (!hasProperty(&_tmPointSigma)) { + addProperty(_tmPointSigma); + } + if (!hasProperty(&_tmPointPixelWeightThreshold)) { + addProperty(_tmPointPixelWeightThreshold); + } + #endif // !__APPLE__ + break; + } + case gaia::ShaderOption::Point_VBO: { + std::unique_ptr program = + ghoul::opengl::ProgramObject::Build( + "GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_vbo_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_point_fs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_point_ge.glsl") + ); + if (_program) { + global::renderEngine.removeRenderProgram(_program.get()); + } + _program = std::move(program); + + if (hasProperty(&_magnitudeBoost)) { + removeProperty(_magnitudeBoost); + } + if (hasProperty(&_sharpness)) { + removeProperty(_sharpness); + } + if (hasProperty(&_billboardSize)) { + removeProperty(_billboardSize); + } + if (hasProperty(&_closeUpBoostDist)) { + removeProperty(_closeUpBoostDist); + } + if (hasProperty(&_pointSpreadFunctionTexturePath)) { + removeProperty(_pointSpreadFunctionTexturePath); + } + if (!hasProperty(&_tmPointFilterSize)) { + addProperty(_tmPointFilterSize); + } + if (!hasProperty(&_tmPointSigma)) { + addProperty(_tmPointSigma); + } + if (!hasProperty(&_tmPointPixelWeightThreshold)) { + addProperty(_tmPointPixelWeightThreshold); + } + break; + } + case gaia::ShaderOption::Billboard_SSBO: + case gaia::ShaderOption::Billboard_SSBO_noFBO: { + #ifndef __APPLE__ + std::unique_ptr program; + if (shaderOption == gaia::ShaderOption::Billboard_SSBO) { + program = ghoul::opengl::ProgramObject::Build( + "GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_ssbo_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_fs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_ge.glsl") + ); + } + else { + program = global::renderEngine.buildRenderProgram("GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_ssbo_vs.glsl"), + absPath( + "${MODULE_GAIAMISSION}/shaders/gaia_billboard_nofbo_fs.glsl" + ), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_ge.glsl") + ); + } + + if (_program) { + global::renderEngine.removeRenderProgram(_program.get()); + } + _program = std::move(program); + + _uniformCache.cameraPos = _program->uniformLocation("cameraPos"); + _uniformCache.cameraLookUp = _program->uniformLocation("cameraLookUp"); + _uniformCache.magnitudeBoost = _program->uniformLocation( + "magnitudeBoost" + ); + _uniformCache.sharpness = _program->uniformLocation("sharpness"); + _uniformCache.billboardSize = _program->uniformLocation("billboardSize"); + _uniformCache.closeUpBoostDist = _program->uniformLocation( + "closeUpBoostDist" + ); + _uniformCache.psfTexture = _program->uniformLocation("psfTexture"); + + _uniformCache.maxStarsPerNode = _program->uniformLocation( + "maxStarsPerNode" + ); + _uniformCache.valuesPerStar = _program->uniformLocation("valuesPerStar"); + _uniformCache.nChunksToRender = _program->uniformLocation( + "nChunksToRender" + ); + + // If rebuild was triggered by switching ShaderOption then ssboBinding + // may not have been initialized yet. Binding will happen in later in + // buffersAredirty. + if (!_shadersAreDirty) { + _program->setSsboBinding( + "ssbo_idx_data", + _ssboIdxBinding->bindingNumber() + ); + _program->setSsboBinding( + "ssbo_comb_data", + _ssboDataBinding->bindingNumber() + ); + } + + if (!hasProperty(&_magnitudeBoost)) { + addProperty(_magnitudeBoost); + } + if (!hasProperty(&_sharpness)) { + addProperty(_sharpness); + } + if (!hasProperty(&_billboardSize)) { + addProperty(_billboardSize); + } + if (!hasProperty(&_closeUpBoostDist)) { + addProperty(_closeUpBoostDist); + } + if (!hasProperty(&_pointSpreadFunctionTexturePath)) { + addProperty(_pointSpreadFunctionTexturePath); + } + if (hasProperty(&_tmPointFilterSize)) { + removeProperty(_tmPointFilterSize); + } + if (hasProperty(&_tmPointSigma)) { + removeProperty(_tmPointSigma); + } + if (hasProperty(&_tmPointPixelWeightThreshold)) { + removeProperty(_tmPointPixelWeightThreshold); + } + #endif // !__APPLE__ + break; + } + case gaia::ShaderOption::Billboard_VBO: { + std::unique_ptr program = + ghoul::opengl::ProgramObject::Build( + "GaiaStar", + absPath("${MODULE_GAIAMISSION}/shaders/gaia_vbo_vs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_fs.glsl"), + absPath("${MODULE_GAIAMISSION}/shaders/gaia_billboard_ge.glsl") + ); + if (_program) { + global::renderEngine.removeRenderProgram(_program.get()); + } + _program = std::move(program); + + _uniformCache.cameraPos = _program->uniformLocation("cameraPos"); + _uniformCache.cameraLookUp = _program->uniformLocation("cameraLookUp"); + _uniformCache.magnitudeBoost = _program->uniformLocation( + "magnitudeBoost" + ); + _uniformCache.sharpness = _program->uniformLocation("sharpness"); + _uniformCache.billboardSize = _program->uniformLocation("billboardSize"); + _uniformCache.closeUpBoostDist = _program->uniformLocation( + "closeUpBoostDist" + ); + _uniformCache.psfTexture = _program->uniformLocation("psfTexture"); + + if (!hasProperty(&_magnitudeBoost)) { + addProperty(_magnitudeBoost); + } + if (!hasProperty(&_sharpness)) { + addProperty(_sharpness); + } + if (!hasProperty(&_billboardSize)) { + addProperty(_billboardSize); + } + if (!hasProperty(&_closeUpBoostDist)) { + addProperty(_closeUpBoostDist); + } + if (!hasProperty(&_pointSpreadFunctionTexturePath)) { + addProperty(_pointSpreadFunctionTexturePath); + } + if (hasProperty(&_tmPointFilterSize)) { + removeProperty(_tmPointFilterSize); + } + if (hasProperty(&_tmPointSigma)) { + removeProperty(_tmPointSigma); + } + if (hasProperty(&_tmPointPixelWeightThreshold)) { + removeProperty(_tmPointPixelWeightThreshold); + } + break; + } + } + + // Common uniforms for all shaders: + _uniformCache.model = _program->uniformLocation("model"); + _uniformCache.view = _program->uniformLocation("view"); + _uniformCache.projection = _program->uniformLocation("projection"); + _uniformCache.time = _program->uniformLocation("time"); + _uniformCache.renderOption = _program->uniformLocation("renderOption"); + _uniformCache.viewScaling = _program->uniformLocation("viewScaling"); + _uniformCache.cutOffThreshold = _program->uniformLocation("cutOffThreshold"); + _uniformCache.luminosityMultiplier = _program->uniformLocation( + "luminosityMultiplier" + ); + _uniformCache.colorTexture = _program->uniformLocation("colorTexture"); + // Filter uniforms: + _uniformFilterCache.posXThreshold = _program->uniformLocation("posXThreshold"); + _uniformFilterCache.posYThreshold = _program->uniformLocation("posYThreshold"); + _uniformFilterCache.posZThreshold = _program->uniformLocation("posZThreshold"); + _uniformFilterCache.gMagThreshold = _program->uniformLocation("gMagThreshold"); + _uniformFilterCache.bpRpThreshold = _program->uniformLocation("bpRpThreshold"); + _uniformFilterCache.distThreshold = _program->uniformLocation("distThreshold"); + } + + if (_programTM->isDirty() || _shadersAreDirty) { + switch (shaderOption) { + case gaia::ShaderOption::Point_SSBO: + case gaia::ShaderOption::Point_VBO: { + std::unique_ptr programTM = + global::renderEngine.buildRenderProgram( + "ToneMapping", + absPath( + "${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_vs.glsl" + ), + absPath( + "${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_point_fs.glsl" + ) + ); + if (_programTM) { + global::renderEngine.removeRenderProgram(_programTM.get()); + } + _programTM = std::move(programTM); + + _uniformCacheTM.screenSize = _programTM->uniformLocation("screenSize"); + _uniformCacheTM.filterSize = _programTM->uniformLocation("filterSize"); + _uniformCacheTM.sigma = _programTM->uniformLocation("sigma"); + _uniformCacheTM.projection = _programTM->uniformLocation("projection"); + _uniformCacheTM.pixelWeightThreshold = _programTM->uniformLocation( + "pixelWeightThreshold" + ); + break; + } + case gaia::ShaderOption::Billboard_SSBO: + case gaia::ShaderOption::Billboard_VBO: { + std::string vs = absPath( + "${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_vs.glsl" + ); + std::string fs = absPath( + "${MODULE_GAIAMISSION}/shaders/gaia_tonemapping_billboard_fs.glsl" + ); + std::unique_ptr programTM = + global::renderEngine.buildRenderProgram("ToneMapping", vs, fs); + if (_programTM) { + global::renderEngine.removeRenderProgram(_programTM.get()); + } + _programTM = std::move(programTM); + break; + } + } + // Common uniforms: + _uniformCacheTM.renderedTexture = _programTM->uniformLocation("renderedTexture"); + + _shadersAreDirty = false; + } + + if (_buffersAreDirty) { + LDEBUG("Regenerating buffers"); + + // Set values per star slice depending on render option. + if (renderOption == gaia::RenderOption::Static) { + _nRenderValuesPerStar = PositionSize; + } + else if (renderOption == gaia::RenderOption::Color) { + _nRenderValuesPerStar = PositionSize + ColorSize; + } + else { // (renderOption == gaia::RenderOption::Motion) + _nRenderValuesPerStar = PositionSize + ColorSize + VelocitySize; + } + + // Calculate memory budgets. + _chunkSize = _octreeManager.maxStarsPerNode() * _nRenderValuesPerStar; + long long totalChunkSizeInBytes = _octreeManager.totalNodes() * + _chunkSize * sizeof(GLfloat); + _maxStreamingBudgetInBytes = std::min( + totalChunkSizeInBytes, + _gpuMemoryBudgetInBytes + ); + long long maxNodesInStream = _maxStreamingBudgetInBytes / + (_chunkSize * sizeof(GLfloat)); + + _gpuStreamBudgetProperty.setMaxValue(static_cast(maxNodesInStream)); + bool datasetFitInMemory = + static_cast(_totalDatasetSizeInBytes) < (_cpuRamBudgetInBytes * 0.9f); + + if (!datasetFitInMemory && !hasProperty(&_additionalNodes)) { + addProperty(_additionalNodes); + } + else if (hasProperty(&_additionalNodes)) { + removeProperty(_additionalNodes); + } + + LDEBUG(fmt::format( + "Chunk size: {} - Max streaming budget (bytes): {} - Max nodes in stream: {}", + _chunkSize, _maxStreamingBudgetInBytes, maxNodesInStream + )); + + // ------------------ RENDER WITH SSBO ----------------------- + if (shaderOption == gaia::ShaderOption::Billboard_SSBO || + shaderOption == gaia::ShaderOption::Point_SSBO || + shaderOption == gaia::ShaderOption::Billboard_SSBO_noFBO) + { +#ifndef __APPLE__ + _useVBO = false; + + // Trigger a rebuild of buffer data from octree. + // With SSBO we won't fill the chunks. + _octreeManager.initBufferIndexStack( + maxNodesInStream, + _useVBO, + datasetFitInMemory + ); + _nStarsToRender = 0; + + // Generate SSBO Buffers and bind them. + if (_vaoEmpty == 0) { + glGenVertexArrays(1, &_vaoEmpty); + LDEBUG(fmt::format("Generating Empty Vertex Array id '{}'", _vaoEmpty)); + } + if (_ssboIdx == 0) { + glGenBuffers(1, &_ssboIdx); + LDEBUG(fmt::format( + "Generating Index Shader Storage Buffer Object id '{}'", _ssboIdx + )); + } + if (_ssboData == 0) { + glGenBuffers(1, &_ssboData); + LDEBUG(fmt::format( + "Generating Data Shader Storage Buffer Object id '{}'", _ssboData + )); + } + + // Bind SSBO blocks to our shader positions. + // Number of stars per chunk (a.k.a. Index). + glBindBuffer(GL_SHADER_STORAGE_BUFFER, _ssboIdx); + + _ssboIdxBinding = std::make_unique + >(); + glBindBufferBase( + GL_SHADER_STORAGE_BUFFER, + _ssboIdxBinding->bindingNumber(), + _ssboIdx + ); + _program->setSsboBinding("ssbo_idx_data", _ssboIdxBinding->bindingNumber()); + + // Combined SSBO with all data. + glBindBuffer(GL_SHADER_STORAGE_BUFFER, _ssboData); + + _ssboDataBinding = std::make_unique + >(); + glBindBufferBase( + GL_SHADER_STORAGE_BUFFER, + _ssboDataBinding->bindingNumber(), + _ssboData + ); + _program->setSsboBinding("ssbo_comb_data", _ssboDataBinding->bindingNumber()); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); + + // Deallocate VBO Buffers if any existed. + if (_vboPos != 0) { + glBindBuffer(GL_ARRAY_BUFFER, _vboPos); + glBufferData( + GL_ARRAY_BUFFER, + 0, + nullptr, + GL_STREAM_DRAW + ); + } + if (_vboCol != 0) { + glBindBuffer(GL_ARRAY_BUFFER, _vboCol); + glBufferData( + GL_ARRAY_BUFFER, + 0, + nullptr, + GL_STREAM_DRAW + ); + } + if (_vboVel != 0) { + glBindBuffer(GL_ARRAY_BUFFER, _vboVel); + glBufferData( + GL_ARRAY_BUFFER, + 0, + nullptr, + GL_STREAM_DRAW + ); + } + glBindBuffer(GL_ARRAY_BUFFER, 0); +#endif // !__APPLE__ + } + else { // ------------------ RENDER WITH VBO ----------------------- + _useVBO = true; + + // Trigger a rebuild of buffer data from octree. + // With VBO we will fill the chunks. + _octreeManager.initBufferIndexStack( + maxNodesInStream, + _useVBO, + datasetFitInMemory + ); + _nStarsToRender = 0; + + // Generate VAO and VBOs + if (_vao == 0) { + glGenVertexArrays(1, &_vao); + LDEBUG(fmt::format("Generating Vertex Array id '{}'", _vao)); + } + if (_vboPos == 0) { + glGenBuffers(1, &_vboPos); + LDEBUG(fmt::format( + "Generating Position Vertex Buffer Object id '{}'", _vboPos + )); + } + if (_vboCol == 0) { + glGenBuffers(1, &_vboCol); + LDEBUG(fmt::format( + "Generating Color Vertex Buffer Object id '{}'", _vboCol + )); + } + if (_vboVel == 0) { + glGenBuffers(1, &_vboVel); + LDEBUG(fmt::format( + "Generating Velocity Vertex Buffer Object id '{}'", _vboVel + )); + } + + // Bind our different VBOs to our vertex array layout. + glBindVertexArray(_vao); + + switch (renderOption) { + case gaia::RenderOption::Static: { + glBindBuffer(GL_ARRAY_BUFFER, _vboPos); + GLint positionAttrib = _program->attributeLocation("in_position"); + glEnableVertexAttribArray(positionAttrib); + + glVertexAttribPointer( + positionAttrib, + PositionSize, + GL_FLOAT, + GL_FALSE, + 0, + nullptr + ); + + break; + } + case gaia::RenderOption::Color: { + glBindBuffer(GL_ARRAY_BUFFER, _vboPos); + GLint positionAttrib = _program->attributeLocation("in_position"); + glEnableVertexAttribArray(positionAttrib); + + glVertexAttribPointer( + positionAttrib, + PositionSize, + GL_FLOAT, + GL_FALSE, + 0, + nullptr + ); + + glBindBuffer(GL_ARRAY_BUFFER, _vboCol); + GLint brightnessAttrib = _program->attributeLocation("in_brightness"); + glEnableVertexAttribArray(brightnessAttrib); + + glVertexAttribPointer( + brightnessAttrib, + ColorSize, + GL_FLOAT, + GL_FALSE, + 0, + nullptr + ); + break; + } + case gaia::RenderOption::Motion: { + glBindBuffer(GL_ARRAY_BUFFER, _vboPos); + GLint positionAttrib = _program->attributeLocation("in_position"); + glEnableVertexAttribArray(positionAttrib); + + glVertexAttribPointer( + positionAttrib, + PositionSize, + GL_FLOAT, + GL_FALSE, + 0, + nullptr + ); + + glBindBuffer(GL_ARRAY_BUFFER, _vboCol); + GLint brightnessAttrib = _program->attributeLocation("in_brightness"); + glEnableVertexAttribArray(brightnessAttrib); + + glVertexAttribPointer( + brightnessAttrib, + ColorSize, + GL_FLOAT, + GL_FALSE, + 0, + nullptr + ); + + glBindBuffer(GL_ARRAY_BUFFER, _vboVel); + GLint velocityAttrib = _program->attributeLocation("in_velocity"); + glEnableVertexAttribArray(velocityAttrib); + + glVertexAttribPointer( + velocityAttrib, + VelocitySize, + GL_FLOAT, + GL_FALSE, + 0, + nullptr + ); + break; + } + } + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + +#ifndef __APPLE__ + // Deallocate SSBO buffers if they existed. + if (_ssboIdx != 0) { + glBindBuffer(GL_SHADER_STORAGE_BUFFER, _ssboIdx); + glBufferData( + GL_SHADER_STORAGE_BUFFER, + 0, + nullptr, + GL_STREAM_DRAW + ); + } + if (_ssboData != 0) { + glBindBuffer(GL_SHADER_STORAGE_BUFFER, _ssboData); + glBufferData( + GL_SHADER_STORAGE_BUFFER, + 0, + nullptr, + GL_STREAM_DRAW + ); + } + glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); +#endif //!__APPLE__ + } + + // Generate VAO and VBO for Quad. + if (_vaoQuad == 0) { + glGenVertexArrays(1, &_vaoQuad); + LDEBUG(fmt::format("Generating Quad Vertex Array id '{}'", _vaoQuad)); + } + if (_vboQuad == 0) { + glGenBuffers(1, &_vboQuad); + LDEBUG(fmt::format("Generating Quad Vertex Buffer Object id '{}'", _vboQuad)); + } + + // Bind VBO and VAO for Quad rendering. + glBindVertexArray(_vaoQuad); + glBindBuffer(GL_ARRAY_BUFFER, _vboQuad); + + // Quad for fullscreen. + static const GLfloat vbo_quad_data[] = { + -1.0f, -1.0f, 0.0f, + 1.0f, -1.0f, 0.0f, + -1.0f, 1.0f, 0.0f, + -1.0f, 1.0f, 0.0f, + 1.0f, -1.0f, 0.0f, + 1.0f, 1.0f, 0.0f, + }; + + glBufferData( + GL_ARRAY_BUFFER, + sizeof(vbo_quad_data), + vbo_quad_data, + GL_STATIC_DRAW + ); + + GLint tmPositionAttrib = _programTM->attributeLocation("in_position"); + glEnableVertexAttribArray(tmPositionAttrib); + glVertexAttribPointer( + tmPositionAttrib, + 3, + GL_FLOAT, + GL_FALSE, + 0, + nullptr + ); + + glEnableVertexAttribArray(0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + // Generate Framebuffer Object and Texture. + if (_fbo == 0) { + glGenFramebuffers(1, &_fbo); + LDEBUG(fmt::format("Generating Framebuffer Object id '{}'", _fbo)); + } + if (!_fboTexture) { + // Generate a new texture and attach it to our FBO. + glm::vec2 screenSize = glm::vec2(global::renderEngine.renderingResolution()); + _fboTexture = std::make_unique( + glm::uvec3(screenSize, 1), + ghoul::opengl::Texture::Format::RGBA, + GL_RGBA32F, + GL_FLOAT + ); + _fboTexture->uploadTexture(); + LDEBUG("Generating Framebuffer Texture!"); + } + // Bind render texture to FBO. + glBindFramebuffer(GL_FRAMEBUFFER, _fbo); + glBindTexture(GL_TEXTURE_2D, *_fboTexture); + glFramebufferTexture( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + *_fboTexture, + 0 + ); + GLenum textureBuffers[1] = { GL_COLOR_ATTACHMENT0 }; + glDrawBuffers(1, textureBuffers); + + // Check that our framebuffer is ok. + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + LERROR("Error when generating GaiaStar Framebuffer."); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + _buffersAreDirty = false; + } + + if (_pointSpreadFunctionTextureIsDirty) { + LDEBUG("Reloading Point Spread Function texture"); + _pointSpreadFunctionTexture = nullptr; + if (!_pointSpreadFunctionTexturePath.value().empty()) { + _pointSpreadFunctionTexture = ghoul::io::TextureReader::ref().loadTexture( + absPath(_pointSpreadFunctionTexturePath) + ); + + if (_pointSpreadFunctionTexture) { + LDEBUG(fmt::format( + "Loaded texture from '{}'", absPath(_pointSpreadFunctionTexturePath) + )); + _pointSpreadFunctionTexture->uploadTexture(); + } + _pointSpreadFunctionTexture->setFilter( + ghoul::opengl::Texture::FilterMode::AnisotropicMipMap + ); + + _pointSpreadFunctionFile = std::make_unique( + _pointSpreadFunctionTexturePath + ); + _pointSpreadFunctionFile->setCallback( + [&](const ghoul::filesystem::File&) { + _pointSpreadFunctionTextureIsDirty = true; + } + ); + } + _pointSpreadFunctionTextureIsDirty = false; + } + + if (_colorTextureIsDirty) { + LDEBUG("Reloading Color Texture"); + _colorTexture = nullptr; + if (!_colorTexturePath.value().empty()) { + _colorTexture = ghoul::io::TextureReader::ref().loadTexture( + absPath(_colorTexturePath) + ); + if (_colorTexture) { + LDEBUG(fmt::format( + "Loaded texture from '{}'", absPath(_colorTexturePath) + )); + _colorTexture->uploadTexture(); + } + + _colorTextureFile = std::make_unique( + _colorTexturePath + ); + _colorTextureFile->setCallback( + [&](const ghoul::filesystem::File&) { _colorTextureIsDirty = true; } + ); + } + _colorTextureIsDirty = false; + } + + if (global::windowDelegate.windowHasResized()) { + // Update FBO texture resolution if we haven't already. + glm::vec2 screenSize = glm::vec2(global::renderEngine.renderingResolution()); + const bool hasChanged = glm::any( + glm::notEqual(_fboTexture->dimensions(), glm::uvec3(screenSize, 1.0)) + ); + + if (hasChanged) { + _fboTexture = std::make_unique( + glm::uvec3(screenSize, 1), + ghoul::opengl::Texture::Format::RGBA, + GL_RGBA32F, + GL_FLOAT + ); + _fboTexture->uploadTexture(); + LDEBUG("Re-Generating Gaia Framebuffer Texture!"); + + glBindFramebuffer(GL_FRAMEBUFFER, _fbo); + glBindTexture(GL_TEXTURE_2D, *_fboTexture); + glFramebufferTexture( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + *_fboTexture, + 0 + ); + GLenum textureBuffers[1] = { GL_COLOR_ATTACHMENT0 }; + glDrawBuffers(1, textureBuffers); + + // Check that our framebuffer is ok. + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + LERROR("Error when re-generating GaiaStar Framebuffer."); + } + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + } +} + +bool RenderableGaiaStars::readDataFile() { + const int fileReaderOption = _fileReaderOption; + int nReadStars = 0; + + _octreeManager.initOctree(_cpuRamBudgetInBytes); + + LINFO("Loading data file: " + _filePath.value()); + + switch (fileReaderOption) { + case gaia::FileReaderOption::Fits: + // Read raw fits file and construct Octree. + nReadStars = readFitsFile(_filePath); + break; + case gaia::FileReaderOption::Speck: + // Read raw speck file and construct Octree. + nReadStars = readSpeckFile(_filePath); + break; + case gaia::FileReaderOption::BinaryRaw: + // Stars are stored in an ordered binary file. + nReadStars = readBinaryRawFile(_filePath); + break; + case gaia::FileReaderOption::BinaryOctree: + // Octree already constructed and stored as a binary file. + nReadStars = readBinaryOctreeFile(_filePath); + break; + case gaia::FileReaderOption::StreamOctree: + // Read Octree structure from file, without data. + nReadStars = readBinaryOctreeStructureFile(_filePath); + break; + default: + LERROR("Wrong FileReaderOption - no data file loaded!"); + break; + } + + //_octreeManager->printStarsPerNode(); + _nRenderedStars.setMaxValue(nReadStars); + LINFO("Dataset contains a total of " + std::to_string(nReadStars) + " stars."); + _totalDatasetSizeInBytes = nReadStars * (PositionSize + ColorSize + VelocitySize) * 4; + + return nReadStars > 0; +} + +int RenderableGaiaStars::readFitsFile(const std::string& filePath) { + int nReadValuesPerStar = 0; + + FitsFileReader fitsFileReader(false); + std::vector fullData = fitsFileReader.readFitsFile( + filePath, + nReadValuesPerStar, + _firstRow, + _lastRow, + _columnNames + ); + + // Insert stars into octree. + for (size_t i = 0; i < fullData.size(); i += nReadValuesPerStar) { + auto first = fullData.begin() + i; + auto last = fullData.begin() + i + nReadValuesPerStar; + std::vector starValues(first, last); + + _octreeManager.insert(starValues); + } + _octreeManager.sliceLodData(); + return static_cast(fullData.size() / nReadValuesPerStar); +} + +int RenderableGaiaStars::readSpeckFile(const std::string& filePath) { + int nReadValuesPerStar = 0; + + FitsFileReader fileReader(false); + std::vector fullData = fileReader.readSpeckFile(filePath, nReadValuesPerStar); + + // Insert stars into octree. + for (size_t i = 0; i < fullData.size(); i += nReadValuesPerStar) { + auto first = fullData.begin() + i; + auto last = fullData.begin() + i + nReadValuesPerStar; + std::vector starValues(first, last); + + _octreeManager.insert(starValues); + } + _octreeManager.sliceLodData(); + return static_cast(fullData.size() / nReadValuesPerStar); +} + +int RenderableGaiaStars::readBinaryRawFile(const std::string& filePath) { + std::vector fullData; + int nReadStars = 0; + + std::ifstream fileStream(filePath, std::ifstream::binary); + if (fileStream.good()) { + int32_t nValues = 0; + int32_t nReadValuesPerStar = 0; + int renderValues = 8; + fileStream.read(reinterpret_cast(&nValues), sizeof(int32_t)); + fileStream.read(reinterpret_cast(&nReadValuesPerStar), sizeof(int32_t)); + + fullData.resize(nValues); + fileStream.read( + reinterpret_cast(fullData.data()), + nValues * sizeof(fullData[0]) + ); + + // Insert stars into octree. + for (size_t i = 0; i < fullData.size(); i += nReadValuesPerStar) { + auto first = fullData.begin() + i; + auto last = fullData.begin() + i + renderValues; + std::vector starValues(first, last); + + _octreeManager.insert(starValues); + } + _octreeManager.sliceLodData(); + + nReadStars = nValues / nReadValuesPerStar; + fileStream.close(); + } + else { + LERROR(fmt::format( + "Error opening file '{}' for loading raw binary file!", filePath + )); + return nReadStars; + } + return nReadStars; +} + +int RenderableGaiaStars::readBinaryOctreeFile(const std::string& filePath) { + int nReadStars = 0; + + std::ifstream fileStream(filePath, std::ifstream::binary); + if (fileStream.good()) { + nReadStars = _octreeManager.readFromFile(fileStream, true); + + fileStream.close(); + } + else { + LERROR(fmt::format( + "Error opening file '{}' for loading binary Octree file!", filePath + )); + return nReadStars; + } + return nReadStars; +} + +int RenderableGaiaStars::readBinaryOctreeStructureFile(const std::string& folderPath) { + int nReadStars = 0; + std::string indexFile = folderPath + "index.bin"; + + std::ifstream fileStream(indexFile, std::ifstream::binary); + if (fileStream.good()) { + nReadStars = _octreeManager.readFromFile(fileStream, false, folderPath); + + fileStream.close(); + } + else { + LERROR(fmt::format( + "Error opening file '{}' for loading binary Octree file!", indexFile + )); + return nReadStars; + } + return nReadStars; +} + +} // namespace openspace diff --git a/modules/gaia/rendering/renderablegaiastars.h b/modules/gaia/rendering/renderablegaiastars.h new file mode 100644 index 0000000000..6b9179bc07 --- /dev/null +++ b/modules/gaia/rendering/renderablegaiastars.h @@ -0,0 +1,216 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_GAIA___RENDERABLEGAIASTARS___H__ +#define __OPENSPACE_MODULE_GAIA___RENDERABLEGAIASTARS___H__ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ghoul::filesystem { class File; } +namespace ghoul::opengl { + class ProgramObject; + class Texture; +} // namespace ghoul::opengl + +namespace openspace { + +namespace documentation { struct Documentation; } + +class RenderableGaiaStars : public Renderable { +public: + explicit RenderableGaiaStars(const ghoul::Dictionary& dictionary); + virtual ~RenderableGaiaStars(); + + void initializeGL() override; + void deinitializeGL() override; + + bool isReady() const override; + + void render(const RenderData& data, RendererTasks& rendererTask) override; + void update(const UpdateData& data) override; + + static documentation::Documentation Documentation(); + +private: + /** + * Reads data file in format defined by FileReaderOption. + * + * \return true if data was successfully read. + */ + bool readDataFile(); + + /** + * Reads a FITS file by using FitsFileReader.readFitsFile() and constructs an octree. + * + * \return the number of stars read. + */ + int readFitsFile(const std::string& filePath); + + /** + * Read a SPECK file by using FitsFileReader.readSpeckFile() and constructs an octree. + * + * \return the number of stars read. + */ + int readSpeckFile(const std::string& filePath); + + /** + * Reads a preprocessed binary file and constructs an octree. + * + * \return the number of stars read. + */ + int readBinaryRawFile(const std::string& filePath); + + /** + * Reads a pre-constructed octree, with all data, from a binary file. + * + * \return the number of stars read. + */ + int readBinaryOctreeFile(const std::string& filePath); + + /** + * Reads the structure of a pre-constructed octree from a binary file, without any + * data. + * + * \return the number of stars read. + */ + int readBinaryOctreeStructureFile(const std::string& folderPath); + + /** + * Checks for any OpenGL errors and reports these to the log if _reportGlErrors is + * set to true. + */ + void checkGlErrors(const std::string& identifier) const; + + properties::StringProperty _filePath; + std::unique_ptr _dataFile; + bool _dataIsDirty = true; + bool _buffersAreDirty = true; + bool _shadersAreDirty = false; + + properties::StringProperty _pointSpreadFunctionTexturePath; + std::unique_ptr _pointSpreadFunctionTexture; + std::unique_ptr _pointSpreadFunctionFile; + bool _pointSpreadFunctionTextureIsDirty = true; + + properties::StringProperty _colorTexturePath; + std::unique_ptr _colorTexture; + std::unique_ptr _colorTextureFile; + bool _colorTextureIsDirty = true; + + properties::FloatProperty _luminosityMultiplier; + properties::FloatProperty _magnitudeBoost; + properties::FloatProperty _cutOffThreshold; + properties::FloatProperty _sharpness; + properties::FloatProperty _billboardSize; + properties::FloatProperty _closeUpBoostDist; + properties::IntProperty _tmPointFilterSize; + properties::FloatProperty _tmPointSigma; + properties::IVec2Property _additionalNodes; + properties::FloatProperty _tmPointPixelWeightThreshold; + properties::FloatProperty _lodPixelThreshold; + + properties::Vec2Property _posXThreshold; + properties::Vec2Property _posYThreshold; + properties::Vec2Property _posZThreshold; + properties::Vec2Property _gMagThreshold; + properties::Vec2Property _bpRpThreshold; + properties::Vec2Property _distThreshold; + + properties::IntProperty _firstRow; + properties::IntProperty _lastRow; + properties::StringListProperty _columnNamesList; + std::vector _columnNames; + properties::OptionProperty _fileReaderOption; + properties::OptionProperty _renderOption; + properties::OptionProperty _shaderOption; + properties::IntProperty _nRenderedStars; + // LongLongProperty doesn't show up in menu, use FloatProperty instead. + properties::FloatProperty _cpuRamBudgetProperty; + properties::FloatProperty _gpuStreamBudgetProperty; + properties::FloatProperty _maxGpuMemoryPercent; + properties::FloatProperty _maxCpuMemoryPercent; + + properties::BoolProperty _reportGlErrors; + + std::unique_ptr _program; + UniformCache(model, view, cameraPos, cameraLookUp, viewScaling, projection, + renderOption, luminosityMultiplier, magnitudeBoost, cutOffThreshold, + sharpness, billboardSize, closeUpBoostDist, screenSize, psfTexture, + time, colorTexture, nChunksToRender, valuesPerStar, maxStarsPerNode) + _uniformCache; + + UniformCache(posXThreshold, posYThreshold, posZThreshold, gMagThreshold, + bpRpThreshold, distThreshold) _uniformFilterCache; + + std::unique_ptr _programTM; + UniformCache(renderedTexture, screenSize, filterSize, sigma, pixelWeightThreshold, + projection) _uniformCacheTM; + std::unique_ptr _fboTexture; + + OctreeManager _octreeManager; + std::unique_ptr> _ssboIdxBinding; + std::unique_ptr> _ssboDataBinding; + + std::vector _accumulatedIndices; + size_t _nRenderValuesPerStar = 0; + int _nStarsToRender = 0; + bool _firstDrawCalls = true; + glm::dquat _previousCameraRotation; + bool _useVBO = false; + long long _cpuRamBudgetInBytes = 0; + long long _totalDatasetSizeInBytes = 0; + long long _gpuMemoryBudgetInBytes = 0; + long long _maxStreamingBudgetInBytes = 0; + size_t _chunkSize = 0; + + GLuint _vao = 0; + GLuint _vaoEmpty = 0; + GLuint _vboPos = 0; + GLuint _vboCol = 0; + GLuint _vboVel = 0; + GLuint _ssboIdx = 0; + GLuint _ssboData = 0; + GLuint _vaoQuad = 0; + GLuint _vboQuad = 0; + GLuint _fbo = 0; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_GAIA___RENDERABLEGAIASTARS___H__ diff --git a/modules/gaia/scripts/filtering.lua b/modules/gaia/scripts/filtering.lua new file mode 100644 index 0000000000..70cc99ef6c --- /dev/null +++ b/modules/gaia/scripts/filtering.lua @@ -0,0 +1,102 @@ +openspace.gaia.documentation = { + { + Name = "addClippingBox", + Arguments = "string, vec3, vec3", + Documentation = "Creates a clipping box for the Gaia renderable in the first argument" + }, + { + Name = "removeClippingBox", + Arguments = "", + Documentation = "" + }, + { + Name = "addClippingSphere", + Arguments = "string, float", + Documentation = "Creates a clipping sphere for the Gaia renderable in the first argument" + }, + { + Name = "removeClippingBox", + Arguments = "", + Documentation = "" + } +} + +openspace.gaia.addClippingBox = function (name, size, position) + local grid_identifier = "Filtering_Box" + local kilo_parsec_in_meter = 30856775814913700000 + + if openspace.hasSceneGraphNode(grid_identifier) then + openspace.removeSceneGraphNode(grid_identifier) + end + + local grid = { + Identifier = grid_identifier, + Transform = { + Translation = { + Type = "StaticTranslation", + Position = { position[1] * kilo_parsec_in_meter, position[2] * kilo_parsec_in_meter, position[3] * kilo_parsec_in_meter } + } + }, + Renderable = { + Type = "RenderableBoxGrid", + GridColor = { 0.6, 0.5, 0.7, 1.0 }, + LineWidth = 2.0, + Size = { size[1] * kilo_parsec_in_meter, size[2] * kilo_parsec_in_meter, size[3] * kilo_parsec_in_meter} + }, + GUI = { + Name = "Filtering Grid", + Path = "/Other/Grids" + } + } + + openspace.addSceneGraphNode(grid) + + openspace.setPropertyValue('Scene.' .. name .. '.renderable.FilterPosX', { (position[1] - size[1] / 2) * kilo_parsec_in_meter, (position[1] + size[1] / 2) * kilo_parsec_in_meter }) + openspace.setPropertyValue('Scene.' .. name .. '.renderable.FilterPosY', { (position[2] - size[2] / 2) * kilo_parsec_in_meter, (position[2] + size[2] / 2) * kilo_parsec_in_meter }) + openspace.setPropertyValue('Scene.' .. name .. '.renderable.FilterPosZ', { (position[3] - size[3] / 2) * kilo_parsec_in_meter, (position[3] + size[3] / 2) * kilo_parsec_in_meter }) +end + +openspace.gaia.removeClippingBox = function() + local grid_identifier = "Filtering_Box" + + if openspace.hasSceneGraphNode(grid_identifier) then + openspace.removeSceneGraphNode(grid_identifier) + end +end + + +openspace.gaia.addClippingSphere = function (name, radius) + local grid_identifier = "Filtering_Sphere" + local kilo_parsec_in_meter = 30856775814913700000 + + + if openspace.hasSceneGraphNode(grid_identifier) then + openspace.removeSceneGraphNode(grid_identifier) + end + + local grid = { + Identifier = grid_identifier, + Renderable = { + Type = "RenderableSphericalGrid", + GridColor = { 0.6, 0.5, 0.7, 1.0 }, + LineWidth = 1.0, + Radius = radius * kilo_parsec_in_meter + }, + GUI = { + Name = "Filtering Sphere", + Path = "/Other/Grids" + } + } + + openspace.addSceneGraphNode(grid) + + openspace.setPropertyValue('Scene.' .. name .. '.renderable.FilterDist', radius * kilo_parsec_in_meter) +end + +openspace.gaia.removeClippingSphere = function() + local grid_identifier = "Filtering_Sphere" + + if openspace.hasSceneGraphNode(grid_identifier) then + openspace.removeSceneGraphNode(grid_identifier) + end +end \ No newline at end of file diff --git a/modules/gaia/shaders/gaia_billboard_fs.glsl b/modules/gaia/shaders/gaia_billboard_fs.glsl new file mode 100644 index 0000000000..d97a7cb99a --- /dev/null +++ b/modules/gaia/shaders/gaia_billboard_fs.glsl @@ -0,0 +1,114 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +#include "floatoperations.glsl" + +layout (location = 0) out vec4 outColor; + +// Keep in sync with gaiaoptions.h:RenderOption enum +const int RENDEROPTION_STATIC = 0; +const int RENDEROPTION_COLOR = 1; +const int RENDEROPTION_MOTION = 2; +const float ONE_PARSEC = 3.08567758e16; // 1 Parsec +const float FLT_MAX = 3.402823466e38; // Max float constant in GLSL +const float LUM_LOWER_CAP = 0.01; + +in vec2 ge_brightness; +in vec4 ge_gPosition; +in vec2 texCoord; +in float ge_starDistFromSun; +in float ge_cameraDistFromSun; +in float ge_observedDist; + +uniform sampler2D psfTexture; +uniform sampler1D colorTexture; +uniform float luminosityMultiplier; +uniform float sharpness; +uniform int renderOption; + +vec3 color2rgb(float color) { + // BV is [-0.4, 2.0] + float st = (color + 0.4) / (2.0 + 0.4); + + // Bp-Rp[-2.0, 6.5], Bp-G[-2.1, 5.0], G-Rp[-1.0, 3.0] + //float st = (color + 1.0) / (5.0 + 1.0); + + return texture(colorTexture, st).rgb; +} + +void main() { + + // Assume all stars has equal luminosity as the Sun when no magnitude is loaded. + float luminosity = 0.05; + vec3 color = vec3(luminosity); + float ratioMultiplier = 0.05; + + vec4 textureColor = texture(psfTexture, texCoord); + textureColor.a = pow(textureColor.a, sharpness); + if (textureColor.a < 0.001) { + discard; + } + + // Calculate the color and luminosity if we have the magnitude and B-V color. + if ( renderOption != RENDEROPTION_STATIC ) { + color = color2rgb(ge_brightness.y); + ratioMultiplier = 0.5; + + // Absolute magnitude is brightness a star would have at 10 pc away. + float absoluteMagnitude = ge_brightness.x; + + // From formula: MagSun - MagStar = 2.5*log(LumStar / LumSun), it gives that: + // LumStar = 10^(1.89 - 0.4*Magstar) , if LumSun = 1 and MagSun = 4.72 + luminosity = pow(10.0, 1.89 - 0.4 * absoluteMagnitude); + + // If luminosity is really really small then set it to a static low number. + if (luminosity < LUM_LOWER_CAP) { + luminosity = LUM_LOWER_CAP; + } + } + + // Luminosity decrease by {squared} distance [measured in Pc]. + float observedDistance = ge_observedDist / ONE_PARSEC; + luminosity /= pow(observedDistance, 2.0); + + // Multiply our color with the luminosity as well as a user-controlled property. + color *= luminosity * pow(luminosityMultiplier, 3.0); + + // Decrease contributing brightness for stars in central cluster. + if ( ge_cameraDistFromSun > ge_starDistFromSun ) { + float ratio = ge_starDistFromSun / ge_cameraDistFromSun; + //color *= ratio * ratioMultiplier; + } + + // Use truncating tonemapping here so we don't overexposure individual stars. + //color = 1.0 - 1.0 * exp(-5.0 * color.rgb); + float maxVal = max(max(color.r, color.g), color.b); + if (maxVal > 1.0) { + color /= maxVal; + } + + outColor = vec4(color, textureColor.a); +} diff --git a/modules/gaia/shaders/gaia_billboard_ge.glsl b/modules/gaia/shaders/gaia_billboard_ge.glsl new file mode 100644 index 0000000000..08a2b4cc72 --- /dev/null +++ b/modules/gaia/shaders/gaia_billboard_ge.glsl @@ -0,0 +1,132 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +#include "floatoperations.glsl" + +// Keep in sync with gaiaoptions.h:RenderOption enum +const int RENDEROPTION_STATIC = 0; +const int RENDEROPTION_COLOR = 1; +const int RENDEROPTION_MOTION = 2; +const float EPS = 1e-5; + +layout(points) in; +in vec2 vs_brightness[]; +in vec4 vs_gPosition[]; +in float vs_starDistFromSun[]; +in float vs_cameraDistFromSun[]; + +layout(triangle_strip, max_vertices = 4) out; +out vec2 ge_brightness; +out vec4 ge_gPosition; +out vec2 texCoord; +out float ge_starDistFromSun; +out float ge_cameraDistFromSun; +out float ge_observedDist; + +uniform dmat4 view; +uniform dmat4 projection; + +uniform dvec3 cameraPos; +uniform dvec3 cameraLookUp; +uniform float viewScaling; +uniform float cutOffThreshold; +uniform float closeUpBoostDist; +uniform float billboardSize; +uniform int renderOption; +uniform float magnitudeBoost; + +const vec2 corners[4] = vec2[4]( + vec2(0.0, 1.0), + vec2(0.0, 0.0), + vec2(1.0, 1.0), + vec2(1.0, 0.0) +); + + +void main() { + + ge_brightness = vs_brightness[0]; + ge_starDistFromSun = vs_starDistFromSun[0]; + ge_cameraDistFromSun = vs_cameraDistFromSun[0]; + + vec4 viewPosition = vec4(view * vs_gPosition[0]); + + // Make closer stars look a bit bigger. + ge_observedDist = safeLength(viewPosition / viewScaling); + float closeUpBoost = closeUpBoostDist / ge_observedDist; + float initStarSize = billboardSize; + + // Use magnitude for size boost as well. + if ( renderOption != RENDEROPTION_STATIC ) { + // DR1 magnitudes are [4, 20], but could be [-15, 20] according to this chart: + // https://qph.fs.quoracdn.net/main-qimg-317a18e3b228efc7d7f67a1632a55961 + // Negative magnitude => Giants + // Big positive magnitude => Dwarfs + float absoluteMagnitude = vs_brightness[0].x; + float normalizedMagnitude = (absoluteMagnitude - 20) / -1; // (-15 - 20); + + // TODO: A linear scale is prabably not the best! + initStarSize += normalizedMagnitude * (magnitudeBoost / 50); + } + + vec4 position = gl_in[0].gl_Position; + vec2 starSize = vec2(initStarSize + closeUpBoost) * position.w / 1000.0; + + float distThreshold = cutOffThreshold - log(ge_observedDist) / log(4.0); + + // Discard geometry if star has no position (but wasn't a nullArray). + // Or if observed distance is above threshold set by cutOffThreshold. + // By discarding in gs instead of fs we save computations for when nothing is visible. + if( length(position) < EPS || distThreshold <= 0){ + return; + } + + vec4 centerWorldPos = vs_gPosition[0]; + + dvec3 cameraNormal = normalize(cameraPos - dvec3(centerWorldPos.xyz)); + dvec3 newRight = normalize(cross(cameraLookUp, cameraNormal)); + dvec3 newUp = cross(cameraNormal, newRight); + vec4 wCameraRight = vec4(newRight, 0.0); + vec4 wCameraUp = vec4(newUp, 0.0); + + float multiplier = float(length(cameraPos)); + starSize *= float(multiplier/1E1); + + for (int i = 0; i < 4; i++) { + // Always turn the billboard towards the camera (needed for warped screen). + vec4 cornerPoint = centerWorldPos + + wCameraRight * starSize.x * (corners[i].x - 0.5) + + wCameraUp * starSize.y * (corners[i].y - 0.5); + gl_Position = vec4(projection * view * cornerPoint); + gl_Position.z = 0.0; + texCoord = corners[i]; + ge_gPosition = viewPosition; + + EmitVertex(); + } + + EndPrimitive(); +} diff --git a/modules/gaia/shaders/gaia_billboard_nofbo_fs.glsl b/modules/gaia/shaders/gaia_billboard_nofbo_fs.glsl new file mode 100644 index 0000000000..caeff99ac1 --- /dev/null +++ b/modules/gaia/shaders/gaia_billboard_nofbo_fs.glsl @@ -0,0 +1,123 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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 "fragment.glsl" +#include "floatoperations.glsl" + +// Keep in sync with gaiaoptions.h:RenderOption enum +const int RENDEROPTION_STATIC = 0; +const int RENDEROPTION_COLOR = 1; +const int RENDEROPTION_MOTION = 2; +const float ONE_PARSEC = 3.08567758e16; // 1 Parsec +const float DEFAULT_DEPTH = 3.08567758e19; // 1000 Pc +const float LUM_LOWER_CAP = 0.01; + +in vec2 ge_brightness; +in vec4 ge_gPosition; +in vec2 texCoord; +in float ge_starDistFromSun; +in float ge_cameraDistFromSun; +in float ge_observedDist; + +uniform sampler2D psfTexture; +uniform sampler1D colorTexture; +uniform float luminosityMultiplier; +uniform float sharpness; +uniform int renderOption; + +vec3 color2rgb(float color) { + // BV is [-0.4, 2.0] + float st = (color + 0.4) / (2.0 + 0.4); + + // Bp-Rp[-2.0, 6.5], Bp-G[-2.1, 5.0], G-Rp[-1.0, 3.0] + //float st = (color + 1.0) / (5.0 + 1.0); + + return texture(colorTexture, st).rgb; +} + +Fragment getFragment() { + + // Assume all stars has equal luminosity as the Sun when no magnitude is loaded. + float luminosity = 1.0; + vec3 color = vec3(luminosity); + float ratioMultiplier = 0.03; + + vec4 textureColor = texture(psfTexture, texCoord); + textureColor.a = pow(textureColor.a, sharpness); + if (textureColor.a < 0.001) { + discard; + } + + // Calculate the color and luminosity if we have the magnitude and B-V color. + if ( renderOption != RENDEROPTION_STATIC ) { + color = color2rgb(ge_brightness.y); + ratioMultiplier = 0.5; + + // Absolute magnitude is brightness a star would have at 10 pc away. + float absoluteMagnitude = ge_brightness.x; + + // From formula: MagSun - MagStar = 2.5*log(LumStar / LumSun), it gives that: + // LumStar = 10^(1.89 - 0.4*Magstar) , if LumSun = 1 and MagSun = 4.72 + luminosity = pow(10.0, 1.89 - 0.4 * absoluteMagnitude); + + // If luminosity is really really small then set it to a static low number. + if (luminosity < LUM_LOWER_CAP) { + luminosity = LUM_LOWER_CAP; + } + } + + // Luminosity decrease by {squared} distance [measured in Pc]. + float observedDistance = ge_observedDist / ONE_PARSEC; + luminosity /= pow(observedDistance, 2.0); + + // Multiply our color with the luminosity as well as a user-controlled property. + color *= luminosity * pow(luminosityMultiplier, 3.0); + + // Decrease contributing brightness for stars in central cluster. + if ( ge_cameraDistFromSun > ge_starDistFromSun ) { + float ratio = ge_starDistFromSun / ge_cameraDistFromSun; + //color *= ratio * ratioMultiplier; + } + + // Use truncating tonemapping here so we don't overexposure individual stars. + //color = 1.0 - 1.0 * exp(-5.0 * color.rgb); + float maxVal = max(max(color.r, color.g), color.b); + if (maxVal > 1.0) { + color /= maxVal; + } + + if (length(color) < 0.01) { + discard; + } + + Fragment frag; + frag.color = vec4(color, textureColor.a);; + // Place stars at back to begin with. + frag.depth = DEFAULT_DEPTH; + frag.gNormal = vec4(0.0, 0.0, 0.0, 1.0); + frag.blend = BLEND_MODE_NORMAL; + + return frag; +} + diff --git a/modules/gaia/shaders/gaia_point_fs.glsl b/modules/gaia/shaders/gaia_point_fs.glsl new file mode 100644 index 0000000000..e32071e956 --- /dev/null +++ b/modules/gaia/shaders/gaia_point_fs.glsl @@ -0,0 +1,98 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +#include "floatoperations.glsl" + +layout (location = 0) out vec4 outColor; + +// Keep in sync with gaiaoptions.h:RenderOption enum +const int RENDEROPTION_STATIC = 0; +const int RENDEROPTION_COLOR = 1; +const int RENDEROPTION_MOTION = 2; +const float ONE_PARSEC = 3.08567758e16; // 1 Parsec +const float LUM_LOWER_CAP = 0.01; + +in vec2 ge_brightness; +in vec4 ge_gPosition; +in float ge_starDistFromSun; +in float ge_cameraDistFromSun; +in float ge_observedDist; + +uniform sampler1D colorTexture; +uniform float luminosityMultiplier; +uniform int renderOption; +uniform float viewScaling; + +vec3 color2rgb(float color) { + // BV is [-0.4, 2.0] + float st = (color + 0.4) / (2.0 + 0.4); + + // Bp-Rp[-2.0, 6.5], Bp-G[-2.1, 5.0], G-Rp[-1.0, 3.0] + //float st = (color + 1.0) / (5.0 + 1.0); + + return texture(colorTexture, st).rgb; +} + +void main() { + + // Assume all stars has equal luminosity as the Sun when no magnitude is loaded. + float luminosity = 0.05; + vec3 color = vec3(luminosity); + float ratioMultiplier = 1.0; + + // Calculate the color and luminosity if we have the magnitude and B-V color. + if ( renderOption != RENDEROPTION_STATIC ) { + color = color2rgb(ge_brightness.y); + ratioMultiplier = 0.01; + + // Absolute magnitude is brightness a star would have at 10 pc away. + float absoluteMagnitude = ge_brightness.x; + + // From formula: MagSun - MagStar = 2.5*log(LumStar / LumSun), it gives that: + // LumStar = 10^(1.89 - 0.4*Magstar) , if LumSun = 1 and MagSun = 4.72 + luminosity = pow(10.0, 1.89 - 0.4 * absoluteMagnitude); + + // If luminosity is really really small then set it to a static low number. + if (luminosity < LUM_LOWER_CAP) { + luminosity = LUM_LOWER_CAP; + } + } + + // Luminosity decrease by {squared} distance [measured in Pc]. + float observedDistance = ge_observedDist / ONE_PARSEC; + luminosity /= pow(observedDistance, 2.0); + + // Multiply our color with the luminosity as well as a user-controlled property. + color *= luminosity * pow(luminosityMultiplier, 3.0); + + // Decrease contributing brightness for stars in central cluster. + if ( ge_cameraDistFromSun > ge_starDistFromSun ) { + float ratio = ge_starDistFromSun / ge_cameraDistFromSun; + //color *= ratio * ratioMultiplier; + } + + outColor = vec4(color, 1.0f); +} diff --git a/modules/gaia/shaders/gaia_point_ge.glsl b/modules/gaia/shaders/gaia_point_ge.glsl new file mode 100644 index 0000000000..59c253ed9c --- /dev/null +++ b/modules/gaia/shaders/gaia_point_ge.glsl @@ -0,0 +1,76 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +#include "floatoperations.glsl" + +const float EPS = 1e-5; + +layout(points) in; +in vec2 vs_brightness[]; +in vec4 vs_gPosition[]; +in float vs_starDistFromSun[]; +in float vs_cameraDistFromSun[]; + +layout(points, max_vertices = 1) out; +out vec2 ge_brightness; +out vec4 ge_gPosition; +out float ge_starDistFromSun; +out float ge_cameraDistFromSun; +out float ge_observedDist; + +uniform dmat4 view; +uniform float viewScaling; +uniform float cutOffThreshold; + +void main() { + + ge_brightness = vs_brightness[0]; + ge_starDistFromSun = vs_starDistFromSun[0]; + ge_cameraDistFromSun = vs_cameraDistFromSun[0]; + + vec4 viewPosition = vec4(view * vs_gPosition[0]); + + ge_observedDist = safeLength(viewPosition / viewScaling); + float distThreshold = cutOffThreshold - log(ge_observedDist) / log(4.0); + + vec4 position = gl_in[0].gl_Position; + + // Discard geometry if star has no position (but wasn't a nullArray). + // Or if observed distance is above threshold set by cutOffThreshold. + // By discarding in gs instead of fs we save computations for when nothing is visible. + if( length(position) < EPS || distThreshold <= 0){ + return; + } + + //gl_PointSize = 1.0; + gl_Position = position; + gl_Position.z = 0.0; + ge_gPosition = viewPosition; + + EmitVertex(); + + EndPrimitive(); +} diff --git a/modules/gaia/shaders/gaia_ssbo_vs.glsl b/modules/gaia/shaders/gaia_ssbo_vs.glsl new file mode 100644 index 0000000000..9977566b04 --- /dev/null +++ b/modules/gaia/shaders/gaia_ssbo_vs.glsl @@ -0,0 +1,186 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +#include "floatoperations.glsl" + +// Keep in sync with gaiaoptions.h:RenderOption enum +const int RENDEROPTION_STATIC = 0; +const int RENDEROPTION_COLOR = 1; +const int RENDEROPTION_MOTION = 2; +const float EPS = 1e-5; +const float Parsec = 3.0856776e16; + +layout (std430) buffer ssbo_idx_data { + int starsPerChunk[]; +}; + +layout (std430) buffer ssbo_comb_data { + float allData[]; +}; + +in int gl_VertexID; + +out vec2 vs_brightness; +out vec4 vs_gPosition; +out float vs_starDistFromSun; +out float vs_cameraDistFromSun; + +uniform dmat4 model; +uniform dmat4 view; +uniform dmat4 projection; +uniform float time; +uniform int renderOption; + +uniform int maxStarsPerNode; +uniform int valuesPerStar; +uniform int nChunksToRender; + +uniform vec2 posXThreshold; +uniform vec2 posYThreshold; +uniform vec2 posZThreshold; +uniform vec2 gMagThreshold; +uniform vec2 bpRpThreshold; +uniform vec2 distThreshold; + +// Use binary search to find the chunk containing our star ID. +int findChunkId(int left, int right, int id) { + + while ( left <= right ) { + int middle = (left + right) / 2; + int firstStarInChunk = starsPerChunk[middle]; + if (left == right || (firstStarInChunk <= id && id < starsPerChunk[middle+1])) { + return middle; + } + else if (id < firstStarInChunk) { + // Go smaller + right = middle - 1; + } + else { + // Go bigger + left = middle + 1; + } + } + return -1; +} + +void main() { + // Fetch our data. + int chunkId = findChunkId(0, nChunksToRender - 1, gl_VertexID); + // Fail safe - this should never happen! + if (chunkId == -1) { + vs_gPosition = vec4(0.0); + gl_Position = vec4(0.0); + return; + } + int placeInChunk = gl_VertexID - starsPerChunk[chunkId]; + int firstStarInChunk = valuesPerStar * maxStarsPerNode * chunkId; // Chunk offset + int nStarsInChunk = starsPerChunk[chunkId + 1] - starsPerChunk[chunkId]; // Stars in current chunk. + // Remove possible duplicates. + if (nStarsInChunk <= 0) { + vs_gPosition = vec4(0.0); + gl_Position = vec4(0.0); + return; + } + + int startOfPos = firstStarInChunk + placeInChunk * 3; + vec3 in_position = vec3(allData[startOfPos], allData[startOfPos + 1], allData[startOfPos + 2]); + vec2 in_brightness = vec2(0.0); + vec3 in_velocity = vec3(0.0); + + // Check if we should filter this star by position. + if ( (abs(posXThreshold.x) > EPS && in_position.x < posXThreshold.x) || + (abs(posXThreshold.y) > EPS && in_position.x > posXThreshold.y) || + (abs(posYThreshold.x) > EPS && in_position.y < posYThreshold.x) || + (abs(posYThreshold.y) > EPS && in_position.y > posYThreshold.y) || + (abs(posZThreshold.x) > EPS && in_position.z < posZThreshold.x) || + (abs(posZThreshold.y) > EPS && in_position.z > posZThreshold.y) || + (abs(distThreshold.x - distThreshold.y) < EPS + && abs(length(in_position) - distThreshold.y) < EPS) ) { + // Discard star in geometry shader. + vs_gPosition = vec4(0.0); + gl_Position = vec4(0.0); + return; + } + + + if ( renderOption != RENDEROPTION_STATIC ) { + int startOfCol = firstStarInChunk + nStarsInChunk * 3 + placeInChunk * 2; + in_brightness = vec2(allData[startOfCol], allData[startOfCol + 1]); + + // Check if we should filter this star by magnitude or color. + if ( (abs(gMagThreshold.x - gMagThreshold.y) < EPS && abs(gMagThreshold.x - in_brightness.x) < EPS) || + (abs(gMagThreshold.x - 20.0f) > EPS && in_brightness.x < gMagThreshold.x) || + (abs(gMagThreshold.y - 20.0f) > EPS && in_brightness.x > gMagThreshold.y) || + (abs(bpRpThreshold.x - bpRpThreshold.y) < EPS && abs(bpRpThreshold.x - in_brightness.y) < EPS) || + (abs(bpRpThreshold.x) > EPS && in_brightness.y < bpRpThreshold.x) || + (abs(bpRpThreshold.y) > EPS && in_brightness.y > bpRpThreshold.y) ) { + // Discard star in geometry shader. + vs_gPosition = vec4(0.0); + gl_Position = vec4(0.0); + return; + } + + if ( renderOption == RENDEROPTION_MOTION ) { + int startOfVel = firstStarInChunk + nStarsInChunk * 5 + placeInChunk * 3; + in_velocity = vec3(allData[startOfVel], allData[startOfVel + 1], allData[startOfVel + 2]); + } + } + vs_brightness = in_brightness; + + // Convert kiloParsec to meter. + vec4 objectPosition = vec4(in_position * 1000 * Parsec, 1.0); + + // Add velocity [m/s] if we've read any. + objectPosition.xyz += time * in_velocity; + + // Thres moving stars by their new position. + float distPosition = length(objectPosition.xyz / (1000.0 * Parsec) ); + if ( (abs(distThreshold.x - distThreshold.y) > EPS && + ((abs(distThreshold.x) > EPS && distPosition < distThreshold.x) || + (abs(distThreshold.y) > EPS && distPosition > distThreshold.y))) ) { + // Discard star in geometry shader. + vs_gPosition = vec4(0.0); + gl_Position = vec4(0.0); + return; + } + + // Apply camera transforms. + dvec4 viewPosition = view * model * objectPosition; + vec4 sunPosition = vec4(view * model * dvec4(0.0f, 0.0f, 0.0f, 1.0f)); + + vs_starDistFromSun = safeLength(objectPosition); + vs_cameraDistFromSun = safeLength(sunPosition); + + // Remove stars without position, happens when VBO chunk is stuffed with zeros. + // Has to be done in Geometry shader because Vertices cannot be discarded here. + if ( length(in_position) > EPS ){ + vs_gPosition = vec4(model * objectPosition); + gl_Position = vec4(projection * viewPosition); + } else { + vs_gPosition = vec4(0.0); + gl_Position = vec4(0.0); + } +} diff --git a/modules/gaia/shaders/gaia_tonemapping_billboard_fs.glsl b/modules/gaia/shaders/gaia_tonemapping_billboard_fs.glsl new file mode 100644 index 0000000000..cdfe6c3cd7 --- /dev/null +++ b/modules/gaia/shaders/gaia_tonemapping_billboard_fs.glsl @@ -0,0 +1,53 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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 "fragment.glsl" + +in vec2 uv; + +uniform sampler2D renderedTexture; + +const float DEFAULT_DEPTH = 3.08567758e19; // 1000 Pc + + +Fragment getFragment() { + + vec4 color = vec4(0.0); + + // BILLBOARDS + // Sample color. Tonemapping done in first shader pass. + vec4 textureColor = texture( renderedTexture, uv ); + + // Use the following to check for any intensity at all. + //color = (length(intensity.rgb) > 0.001) ? vec4(1.0) : vec4(0.0); + + Fragment frag; + frag.color = textureColor; + // Place stars at back to begin with. + frag.depth = DEFAULT_DEPTH; + frag.gNormal = vec4(0.0, 0.0, 0.0, 1.0); + frag.blend = BLEND_MODE_NORMAL; + + return frag; +} diff --git a/modules/gaia/shaders/gaia_tonemapping_point_fs.glsl b/modules/gaia/shaders/gaia_tonemapping_point_fs.glsl new file mode 100644 index 0000000000..89d091c223 --- /dev/null +++ b/modules/gaia/shaders/gaia_tonemapping_point_fs.glsl @@ -0,0 +1,178 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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 "fragment.glsl" + +in vec2 uv; + +uniform sampler2D renderedTexture; +uniform dmat4 projection; +uniform vec2 screenSize; +uniform int filterSize; +uniform float sigma; +uniform float pixelWeightThreshold; + +const float M_PI = 3.141592653589793238462; +const float DEFAULT_DEPTH = 3.08567758e19; // 1000 Pc + +Fragment getFragment() { + + vec4 color = vec4(0.0); + + // GL_POINTS + + // Use frustum params to be able to compensate for a skewed frustum (in a dome). + float near = float(projection[3][2] / (projection[2][2] - 1.0)); + float left = float(near * (projection[2][0] - 1.0) / projection[0][0]); + float right = float(near * (projection[2][0] + 1.0) / projection[0][0]); + float top = float(near * (projection[2][1] + 1.0) / projection[1][1]); + float bottom = float(near * (projection[2][1] - 1.0) / projection[1][1]); + + float xFactor = float(projection[0][0]); + float yFactor = float(projection[1][1]); + + float planeAspect = yFactor / xFactor; // Equals: (right - left) / (top - bottom) + float screenAspect = screenSize.x / screenSize.y; + float fullAspect = planeAspect / screenAspect; + + // Find screenPos in skewed frustum. uv is [0, 1] + vec2 screenPos = uv * vec2(right - left, top - bottom) + vec2(left, bottom); + + // Find our elliptic scale factors by trigonometric approximation. + float beta = atan(length(screenPos) / near); + vec2 sigmaScaleFactor = vec2( 1.0 / cos(beta), 1.0 / pow(cos(beta), 2.0)); + + float defaultScreen = 1200.0; + float scaling = screenSize.y / defaultScreen * yFactor; + + // Scale filter size depending on screen pos. + vec2 filterScaleFactor = vec2( + pow(screenPos.x / near, 2.0) * fullAspect, + pow(screenPos.y / near, 2.0) + ); + + // Use to ignore scaling. + //filterScaleFactor = vec2(0.0); + //scaling = 1.0; + //sigmaScaleFactor = vec2(1.0); + + // Use the following to find the origo in a skewed frustum. + //Fragment origoFrag; + //vec2 screenOrigo = vec2(-left, -bottom) / vec2(right - left, top - bottom); + //if (abs(screenOrigo.x - uv.x) > 0.0005 && abs(screenOrigo.y - uv.y) > 0.0005) { + // origoFrag.color = vec4(0.0); + //} else { + // origoFrag.color = vec4(1.0); + //} + //return origoFrag; + + // Uncomment to compare to original filterSize (assumes origo in center of screen). + //screenPos = (uv - 0.5) * 2.0; // [-1, 1] + //filterScaleFactor = vec2( + // pow(screenPos.x, 2.0), + // pow(screenPos.y, 2.0) + //); + + // Make use of the following flag this to toggle betweeen circular and elliptic distribution. + bool useCircleDist = false; + + // Apply scaling on bloom filter. + vec2 newFilterSize = vec2(filterSize) * (1.0 + length(filterScaleFactor)) * scaling; + + // Calculate params for a rotated Elliptic Gaussian distribution. + float sigmaMajor; + float sigmaMinor; + float a; + float b; + float c; + if (!useCircleDist) { + float alpha = atan(screenPos.y, screenPos.x); + // Apply scaling on sigma. + sigmaMajor = sigma * sigmaScaleFactor.y * scaling; + sigmaMinor = sigma * sigmaScaleFactor.x * scaling; + + a = pow(cos(alpha), 2.0) / (2 * pow(sigmaMajor, 2.0)) + + pow(sin(alpha), 2.0) / (2 * pow(sigmaMinor, 2.0)) ; + b = sin(2 * alpha) / (4 * pow(sigmaMajor, 2.0)) + - sin(2 * alpha) / (4 * pow(sigmaMinor, 2.0)) ; + c = pow(sin(alpha), 2.0) / (2 * pow(sigmaMajor, 2.0)) + + pow(cos(alpha), 2.0) / (2 * pow(sigmaMinor, 2.0)) ; + } + + // Get a [newFilterSize x newFilterSize] filter around our pixel. UV is [0, 1] + vec3 intensity = vec3(0.0); + vec2 pixelSize = 1.0 / screenSize; + ivec2 halfFilterSize = ivec2((newFilterSize - 1.0) / 2.0); + for (int y = -halfFilterSize.y; y <= halfFilterSize.y; y += 1) { + for (int x = -halfFilterSize.x; x <= halfFilterSize.x; x += 1) { + vec2 sPoint = uv + (pixelSize * ivec2(x, y)); + + // Calculate the contribution of this pixel (elliptic gaussian distribution). + float pixelWeight = exp(-( + a * pow(x * fullAspect, 2.0) + + 2 * b * x * y * fullAspect + + c * pow(y, 2.0) + )); + + // Only sample inside FBO texture and if the pixel will contribute to final color. + if (all(greaterThan(sPoint, vec2(0.0))) && all(lessThan(sPoint, vec2(1.0))) + && pixelWeight > pixelWeightThreshold) { + vec4 sIntensity = texture( renderedTexture, sPoint ); + + // Use normal distribution function for halo/bloom effect. + if (useCircleDist) { + float circleDist = sqrt(pow(x / (1 + length(filterScaleFactor)), 2.0) + + pow(y / (1 + length(filterScaleFactor)), 2.0)); + intensity += sIntensity.rgb * (1.0 / (sigma * sqrt(2.0 * M_PI))) * + exp(-(pow(circleDist, 2.0) / (2.0 * pow(sigma, 2.0)))) / filterSize; + } + else { + // Divide contribution by area of ellipse. + intensity += sIntensity.rgb * pixelWeight * fullAspect; + } + } + } + } + // Tonemap intensity to color! + //intensity = 1.0 - 1.0 * exp(-25.0 * intensity); + intensity = pow(intensity, vec3(0.8)); + + if (length(intensity) < 0.01) { + discard; + } + + color = vec4(intensity, 1.0f); + + // Use the following to check for any intensity at all. + //color = (length(intensity.rgb) > 0.001) ? vec4(1.0) : vec4(0.0); + + Fragment frag; + frag.color = color; + // Place stars at back to begin with. + frag.depth = DEFAULT_DEPTH; + frag.gNormal = vec4(0.0, 0.0, 0.0, 1.0); + frag.blend = BLEND_MODE_NORMAL; + + return frag; +} \ No newline at end of file diff --git a/modules/gaia/shaders/gaia_tonemapping_vs.glsl b/modules/gaia/shaders/gaia_tonemapping_vs.glsl new file mode 100644 index 0000000000..97e3a86a3a --- /dev/null +++ b/modules/gaia/shaders/gaia_tonemapping_vs.glsl @@ -0,0 +1,34 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +in vec3 in_position; + +out vec2 uv; + +void main() { + uv = (in_position.xy + 1.0) / 2.0; + gl_Position = vec4(in_position, 1.0); +} diff --git a/modules/gaia/shaders/gaia_vbo_vs.glsl b/modules/gaia/shaders/gaia_vbo_vs.glsl new file mode 100644 index 0000000000..fcb0abe114 --- /dev/null +++ b/modules/gaia/shaders/gaia_vbo_vs.glsl @@ -0,0 +1,119 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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. * + ****************************************************************************************/ + +#version __CONTEXT__ + +#include "floatoperations.glsl" + +// Keep in sync with gaiaoptions.h:RenderOption enum +const int RENDEROPTION_STATIC = 0; +const int RENDEROPTION_COLOR = 1; +const int RENDEROPTION_MOTION = 2; +const float EPS = 1e-5; +const float Parsec = 3.0856776e16; + +in vec3 in_position; +in vec2 in_brightness; +in vec3 in_velocity; + +out vec2 vs_brightness; +out vec4 vs_gPosition; +out float vs_starDistFromSun; +out float vs_cameraDistFromSun; + +uniform dmat4 model; +uniform dmat4 view; +uniform dmat4 projection; +uniform float time; +uniform int renderOption; + +uniform vec2 posXThreshold; +uniform vec2 posYThreshold; +uniform vec2 posZThreshold; +uniform vec2 gMagThreshold; +uniform vec2 bpRpThreshold; +uniform vec2 distThreshold; + +void main() { + vs_brightness = in_brightness; + + // Check if we should filter this star by position. Thres depending on original values. + if ( (abs(posXThreshold.x) > EPS && in_position.x < posXThreshold.x) || + (abs(posXThreshold.y) > EPS && in_position.x > posXThreshold.y) || + (abs(posYThreshold.x) > EPS && in_position.y < posYThreshold.x) || + (abs(posYThreshold.y) > EPS && in_position.y > posYThreshold.y) || + (abs(posZThreshold.x) > EPS && in_position.z < posZThreshold.x) || + (abs(posZThreshold.y) > EPS && in_position.z > posZThreshold.y) || + (abs(distThreshold.x - distThreshold.y) < EPS + && abs(length(in_position) - distThreshold.y) < EPS) || + ( renderOption != RENDEROPTION_STATIC && ( + (abs(gMagThreshold.x - gMagThreshold.y) < EPS && abs(gMagThreshold.x - in_brightness.x) < EPS) || + (abs(gMagThreshold.x - 20.0f) > EPS && in_brightness.x < gMagThreshold.x) || + (abs(gMagThreshold.y - 20.0f) > EPS && in_brightness.x > gMagThreshold.y) || + (abs(bpRpThreshold.x - bpRpThreshold.y) < EPS && abs(bpRpThreshold.x - in_brightness.y) < EPS) || + (abs(bpRpThreshold.x) > EPS && in_brightness.y < bpRpThreshold.x) || + (abs(bpRpThreshold.y) > EPS && in_brightness.y > bpRpThreshold.y))) ) { + // Discard star in geometry shader. + vs_gPosition = vec4(0.0); + gl_Position = vec4(0.0); + return; + } + + // Convert kiloParsec to meter. + vec4 objectPosition = vec4(in_position * 1000 * Parsec, 1.0); + + // Add velocity if we've read any. + if ( renderOption == RENDEROPTION_MOTION ) { + // Velocity is already in [m/s]. + objectPosition.xyz += time * in_velocity; + } + + // Thres moving stars by their new position. + float distPosition = length(objectPosition.xyz / (1000.0 * Parsec) ); + if ( (abs(distThreshold.x - distThreshold.y) > EPS && + ((abs(distThreshold.x) > EPS && distPosition< distThreshold.x) || + (abs(distThreshold.y) > EPS && distPosition > distThreshold.y))) ) { + // Discard star in geometry shader. + vs_gPosition = vec4(0.0); + gl_Position = vec4(0.0); + return; + } + + // Apply camera transforms. + dvec4 viewPosition = view * model * objectPosition; + vec4 sunPosition = vec4(view * model * vec4(0.0f, 0.0f, 0.0f, 1.0f)); + + vs_starDistFromSun = safeLength(objectPosition); + vs_cameraDistFromSun = safeLength(sunPosition); + + // Remove stars without position, happens when VBO chunk is stuffed with zeros. + // Has to be done in Geometry shader because Vertices cannot be discarded here. + if ( length(in_position) > EPS ){ + vs_gPosition = vec4(model * objectPosition); + gl_Position = vec4(projection * viewPosition); + } else { + vs_gPosition = vec4(0.0); + gl_Position = vec4(0.0); + } +} diff --git a/modules/gaia/tasks/constructoctreetask.cpp b/modules/gaia/tasks/constructoctreetask.cpp new file mode 100644 index 0000000000..095d6bd750 --- /dev/null +++ b/modules/gaia/tasks/constructoctreetask.cpp @@ -0,0 +1,817 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr const char* KeyInFileOrFolderPath = "InFileOrFolderPath"; + constexpr const char* KeyOutFileOrFolderPath = "OutFileOrFolderPath"; + constexpr const char* KeyMaxDist = "MaxDist"; + constexpr const char* KeyMaxStarsPerNode = "MaxStarsPerNode"; + constexpr const char* KeySingleFileInput = "SingleFileInput"; + + constexpr const char* KeyFilterPosX = "FilterPosX"; + constexpr const char* KeyFilterPosY = "FilterPosY"; + constexpr const char* KeyFilterPosZ = "FilterPosZ"; + constexpr const char* KeyFilterGMag = "FilterGMag"; + constexpr const char* KeyFilterBpRp = "FilterBpRp"; + constexpr const char* KeyFilterVelX = "FilterVelX"; + constexpr const char* KeyFilterVelY = "FilterVelY"; + constexpr const char* KeyFilterVelZ = "FilterVelZ"; + constexpr const char* KeyFilterBpMag = "FilterBpMag"; + constexpr const char* KeyFilterRpMag = "FilterRpMag"; + constexpr const char* KeyFilterBpG = "FilterBpG"; + constexpr const char* KeyFilterGRp = "FilterGRp"; + constexpr const char* KeyFilterRa = "FilterRa"; + constexpr const char* KeyFilterRaError = "FilterRaError"; + constexpr const char* KeyFilterDec = "FilterDec"; + constexpr const char* KeyFilterDecError = "FilterDecError"; + constexpr const char* KeyFilterParallax = "FilterParallax"; + constexpr const char* KeyFilterParallaxError = "FilterParallaxError"; + constexpr const char* KeyFilterPmra = "FilterPmra"; + constexpr const char* KeyFilterPmraError = "FilterPmraError"; + constexpr const char* KeyFilterPmdec = "FilterPmdec"; + constexpr const char* KeyFilterPmdecError = "FilterPmdecError"; + constexpr const char* KeyFilterRv = "FilterRv"; + constexpr const char* KeyFilterRvError = "FilterRvError"; + + constexpr const char* _loggerCat = "ConstructOctreeTask"; +} // namespace + +namespace openspace { + +ConstructOctreeTask::ConstructOctreeTask(const ghoul::Dictionary& dictionary) { + openspace::documentation::testSpecificationAndThrow( + documentation(), + dictionary, + "ConstructOctreeTask" + ); + + _inFileOrFolderPath = absPath(dictionary.value(KeyInFileOrFolderPath)); + _outFileOrFolderPath = absPath(dictionary.value(KeyOutFileOrFolderPath)); + + if (dictionary.hasKey(KeyMaxDist)) { + _maxDist = static_cast(dictionary.value(KeyMaxDist)); + } + + if (dictionary.hasKey(KeyMaxStarsPerNode)) { + _maxStarsPerNode = static_cast(dictionary.value(KeyMaxStarsPerNode)); + } + + if (dictionary.hasKey(KeySingleFileInput)) { + _singleFileInput = dictionary.value(KeySingleFileInput); + } + + _octreeManager = std::make_shared(); + _indexOctreeManager = std::make_shared(); + + // Check for filter params. + if (dictionary.hasKey(KeyFilterPosX)) { + _posX = dictionary.value(KeyFilterPosX); + _filterPosX = true; + } + if (dictionary.hasKey(KeyFilterPosY)) { + _posY = dictionary.value(KeyFilterPosY); + _filterPosY = true; + } + if (dictionary.hasKey(KeyFilterPosZ)) { + _posZ = dictionary.value(KeyFilterPosZ); + _filterPosZ = true; + } + if (dictionary.hasKey(KeyFilterGMag)) { + _gMag = dictionary.value(KeyFilterGMag); + _filterGMag = true; + } + if (dictionary.hasKey(KeyFilterBpRp)) { + _bpRp = dictionary.value(KeyFilterBpRp); + _filterBpRp = true; + } + if (dictionary.hasKey(KeyFilterVelX)) { + _velX = dictionary.value(KeyFilterVelX); + _filterVelX = true; + } + if (dictionary.hasKey(KeyFilterVelY)) { + _velY = dictionary.value(KeyFilterVelY); + _filterVelY = true; + } + if (dictionary.hasKey(KeyFilterVelZ)) { + _velZ = dictionary.value(KeyFilterVelZ); + _filterVelZ = true; + } + if (dictionary.hasKey(KeyFilterBpMag)) { + _bpMag = dictionary.value(KeyFilterBpMag); + _filterBpMag = true; + } + if (dictionary.hasKey(KeyFilterRpMag)) { + _rpMag = dictionary.value(KeyFilterRpMag); + _filterRpMag = true; + } + if (dictionary.hasKey(KeyFilterBpG)) { + _bpG = dictionary.value(KeyFilterBpG); + _filterBpG = true; + } + if (dictionary.hasKey(KeyFilterGRp)) { + _gRp = dictionary.value(KeyFilterGRp); + _filterGRp = true; + } + if (dictionary.hasKey(KeyFilterRa)) { + _ra = dictionary.value(KeyFilterRa); + _filterRa = true; + } + if (dictionary.hasKey(KeyFilterRaError)) { + _raError = dictionary.value(KeyFilterRaError); + _filterRaError = true; + } + if (dictionary.hasKey(KeyFilterDec)) { + _dec = dictionary.value(KeyFilterDec); + _filterDec = true; + } + if (dictionary.hasKey(KeyFilterDecError)) { + _decError = dictionary.value(KeyFilterDecError); + _filterDecError = true; + } + if (dictionary.hasKey(KeyFilterParallax)) { + _parallax = dictionary.value(KeyFilterParallax); + _filterParallax = true; + } + if (dictionary.hasKey(KeyFilterParallaxError)) { + _parallaxError = dictionary.value(KeyFilterParallaxError); + _filterParallaxError = true; + } + if (dictionary.hasKey(KeyFilterPmra)) { + _pmra = dictionary.value(KeyFilterPmra); + _filterPmra = true; + } + if (dictionary.hasKey(KeyFilterPmraError)) { + _pmraError = dictionary.value(KeyFilterPmraError); + _filterPmraError = true; + } + if (dictionary.hasKey(KeyFilterPmdec)) { + _pmdec = dictionary.value(KeyFilterPmdec); + _filterPmdec = true; + } + if (dictionary.hasKey(KeyFilterPmdecError)) { + _pmdecError = dictionary.value(KeyFilterPmdecError); + _filterPmdecError = true; + } + if (dictionary.hasKey(KeyFilterRv)) { + _rv = dictionary.value(KeyFilterRv); + _filterRv = true; + } + if (dictionary.hasKey(KeyFilterRvError)) { + _rvError = dictionary.value(KeyFilterRvError); + _filterRvError = true; + } +} + +ConstructOctreeTask::~ConstructOctreeTask() {} + +std::string ConstructOctreeTask::description() { + return "Read bin file (or files in folder): " + _inFileOrFolderPath + "\n " + "and write octree data file (or files) into: " + _outFileOrFolderPath + "\n"; +} + +void ConstructOctreeTask::perform(const Task::ProgressCallback& progressCallback) { + progressCallback(0.0f); + + if (_singleFileInput) { + constructOctreeFromSingleFile(progressCallback); + } + else { + constructOctreeFromFolder(progressCallback); + } + + progressCallback(1.0f); +} + +void ConstructOctreeTask::constructOctreeFromSingleFile( + const Task::ProgressCallback& progressCallback) { + std::vector fullData; + int32_t nValues = 0; + int32_t nValuesPerStar = 0; + size_t nFilteredStars = 0; + int nTotalStars = 0; + + _octreeManager->initOctree(0, _maxDist, _maxStarsPerNode); + + LINFO("Reading data file: " + _inFileOrFolderPath); + + LINFO(fmt::format( + "MAX DIST: {} - MAX STARS PER NODE: {}", + _octreeManager->maxDist(), _octreeManager->maxStarsPerNode() + )); + + // Use to generate a synthetic dataset + /*for (float z = -1.0; z < 1.0; z += 0.05) { + for (float y = -1.0; y < 1.0; y += 0.05) { + for (float x = -1.0; x < 1.0; x += 0.05) { + float r = static_cast (rand()) / static_cast (RAND_MAX); + std::vector renderValues(8); + renderValues[0] = x; // + x * r; + renderValues[2] = z; // + y * r; + renderValues[1] = y; // + z * r; + renderValues[3] = 5.0; // + 10 * r; + renderValues[4] = 2.0; // + 10 * r; + renderValues[5] = r; + renderValues[6] = r; + renderValues[7] = r; + _octreeManager->insert(renderValues); + nTotalStars++; + nValues+=8; + } + } + } + + for (float phi = -180.0; phi < 180.0; phi += 10.0) { + for (float theta = -90.0; theta <= 90.0; theta += 10.0) { + float r = 1.0; + std::vector renderValues(8); + renderValues[0] = r * sin(glm::radians(theta)) * cos(glm::radians(phi)); + renderValues[2] = r * sin(glm::radians(theta)) * sin(glm::radians(phi)); + renderValues[1] = r * cos(glm::radians(theta)); + renderValues[3] = 5.0; + renderValues[4] = 2.0; + renderValues[5] = r; + renderValues[6] = r; + renderValues[7] = r; + _octreeManager->insert(renderValues); + nTotalStars++; + nValues += 8; + } + }*/ + + std::ifstream inFileStream(_inFileOrFolderPath, std::ifstream::binary); + if (inFileStream.good()) { + inFileStream.read(reinterpret_cast(&nValues), sizeof(int32_t)); + inFileStream.read(reinterpret_cast(&nValuesPerStar), sizeof(int32_t)); + + fullData.resize(nValues); + inFileStream.read( + reinterpret_cast(fullData.data()), + nValues * sizeof(fullData[0]) + ); + nTotalStars = nValues / nValuesPerStar; + + progressCallback(0.3f); + LINFO("Constructing Octree."); + + // Insert star into octree. We assume the data already is in correct order. + for (size_t i = 0; i < fullData.size(); i += nValuesPerStar) { + auto first = fullData.begin() + i; + auto last = fullData.begin() + i + nValuesPerStar; + std::vector filterValues(first, last); + std::vector renderValues(first, first + RENDER_VALUES); + + // Filter data by parameters. + if (checkAllFilters(filterValues)) { + nFilteredStars++; + continue; + } + + // If all filters passed then insert render values into Octree. + _octreeManager->insert(renderValues); + } + inFileStream.close(); + } + else { + LERROR(fmt::format( + "Error opening file '{}' for loading preprocessed file!", + _inFileOrFolderPath + )); + } + LINFO(fmt::format("{} of {} read stars were filtered", nFilteredStars, nTotalStars)); + + // Slice LOD data before writing to files. + _octreeManager->sliceLodData(); + + LINFO("Writing octree to: " + _outFileOrFolderPath); + std::ofstream outFileStream(_outFileOrFolderPath, std::ofstream::binary); + if (outFileStream.good()) { + if (nValues == 0) { + LERROR("Error writing file - No values were read from file."); + } + _octreeManager->writeToFile(outFileStream, true); + + outFileStream.close(); + } + else { + LERROR(fmt::format( + "Error opening file: {} as output data file.", _outFileOrFolderPath + )); + } +} + +void ConstructOctreeTask::constructOctreeFromFolder( + const Task::ProgressCallback& progressCallback) +{ + int32_t nStars = 0; + int32_t nValuesPerStar = 0; + size_t nFilteredStars = 0; + //float maxRadius = 0.0; + //int starsOutside10 = 0; + //int starsOutside25 = 0; + //int starsOutside50 = 0; + //int starsOutside75 = 0; + //int starsOutside100 = 0; + //int starsOutside200 = 0; + //int starsOutside300 = 0; + //int starsOutside400 = 0; + //int starsOutside500 = 0; + //int starsOutside750 = 0; + //int starsOutside1000 = 0; + //int starsOutside1500 = 0; + //int starsOutside2000 = 0; + //int starsOutside5000 = 0; + + ghoul::filesystem::Directory currentDir(_inFileOrFolderPath); + std::vector allInputFiles = currentDir.readFiles(); + std::vector filterValues; + auto writeThreads = std::vector(8); + + _indexOctreeManager->initOctree(0, _maxDist, _maxStarsPerNode); + + float processOneFile = 1.f / allInputFiles.size(); + + LINFO(fmt::format( + "MAX DIST: {} - MAX STARS PER NODE: {}", + _indexOctreeManager->maxDist(), _indexOctreeManager->maxStarsPerNode() + )); + + for (size_t idx = 0; idx < allInputFiles.size(); ++idx) { + std::string inFilePath = allInputFiles[idx]; + int nStarsInfile = 0; + + LINFO("Reading data file: " + inFilePath); + + std::ifstream inFileStream(inFilePath, std::ifstream::binary); + if (inFileStream.good()) { + inFileStream.read(reinterpret_cast(&nValuesPerStar), sizeof(int32_t)); + filterValues.resize(nValuesPerStar, 0.f); + + while (inFileStream.read( + reinterpret_cast(filterValues.data()), + nValuesPerStar * sizeof(filterValues[0]) + )) + { + // Filter data by parameters. + if (checkAllFilters(filterValues)) { + nFilteredStars++; + continue; + } + // Generate a 50/12,5 dataset (gMag <=13/>13). + //if ((filterStar(glm::vec2(20.0), filterValues[3], 20.f)) || + // (filterStar(glm::vec2(0.0), filterValues[16])) || + // (filterValues[3] > 13.0 && filterValues[17] > 0.125) || + // (filterValues[3] <= 13.0 && filterValues[17] > 0.5)) { + // nFilteredStars++; + // continue; + //} + + // If all filters passed then insert render values into Octree. + std::vector renderValues( + filterValues.begin(), + filterValues.begin() + RENDER_VALUES + ); + + _indexOctreeManager->insert(renderValues); + nStarsInfile++; + + //float maxVal = fmax(fmax(fabs(renderValues[0]), fabs(renderValues[1])), + // fabs(renderValues[2])); + //if (maxVal > maxRadius) maxRadius = maxVal; + //// Calculate how many stars are outside of different thresholds. + //if (maxVal > 10) starsOutside10++; + //if (maxVal > 25) starsOutside25++; + //if (maxVal > 50) starsOutside50++; + //if (maxVal > 75) starsOutside75++; + //if (maxVal > 100) starsOutside100++; + //if (maxVal > 200) starsOutside200++; + //if (maxVal > 300) starsOutside300++; + //if (maxVal > 400) starsOutside400++; + //if (maxVal > 500) starsOutside500++; + //if (maxVal > 750) starsOutside750++; + //if (maxVal > 1000) starsOutside1000++; + //if (maxVal > 1500) starsOutside1500++; + //if (maxVal > 2000) starsOutside2000++; + //if (maxVal > 5000) starsOutside5000++; + } + inFileStream.close(); + } + else { + LERROR(fmt::format( + "Error opening file '{}' for loading preprocessed file!", inFilePath + )); + } + + // Slice LOD data. + LINFO("Slicing LOD data!"); + _indexOctreeManager->sliceLodData(idx); + + progressCallback((idx + 1) * processOneFile); + nStars += nStarsInfile; + + LINFO(fmt::format("Writing {} stars to octree files!", nStarsInfile)); + LINFO(fmt::format( + "Number leaf nodes: {}\n Number inner nodes: {}\n Total depth of tree: {}", + _indexOctreeManager->numLeafNodes(), + _indexOctreeManager->numInnerNodes(), + _indexOctreeManager->totalDepth() + )); + + // Write to 8 separate files in a separate thread. Data will be cleared after it + // has been written. Store joinable thread for later sync. + std::thread t( + &OctreeManager::writeToMultipleFiles, + _indexOctreeManager, + _outFileOrFolderPath, + idx + ); + writeThreads[idx] = std::move(t); + } + + LINFO(fmt::format( + "A total of {} stars were read from files and distributed into {} total nodes", + nStars, _indexOctreeManager->totalNodes() + )); + LINFO(std::to_string(nFilteredStars) + " stars were filtered"); + + //LINFO("Max radius of dataset is: " + std::to_string(maxRadius) + + // "\n Number of stars outside of:" + + // " - 10kPc is " + std::to_string(starsOutside10) + "\n" + + // " - 25kPc is " + std::to_string(starsOutside25) + "\n" + + // " - 50kPc is " + std::to_string(starsOutside50) + "\n" + + // " - 75kPc is " + std::to_string(starsOutside75) + "\n" + + // " - 100kPc is " + std::to_string(starsOutside100) + "\n" + + // " - 200kPc is " + std::to_string(starsOutside200) + "\n" + + // " - 300kPc is " + std::to_string(starsOutside300) + "\n" + + // " - 400kPc is " + std::to_string(starsOutside400) + "\n" + + // " - 500kPc is " + std::to_string(starsOutside500) + "\n" + + // " - 750kPc is " + std::to_string(starsOutside750) + "\n" + + // " - 1000kPc is " + std::to_string(starsOutside1000) + "\n" + + // " - 1500kPc is " + std::to_string(starsOutside1500) + "\n" + + // " - 2000kPc is " + std::to_string(starsOutside2000) + "\n" + + // " - 5000kPc is " + std::to_string(starsOutside5000)); + + // Write index file of Octree structure. + std::string indexFileOutPath = _outFileOrFolderPath + "index.bin"; + std::ofstream outFileStream(indexFileOutPath, std::ofstream::binary); + if (outFileStream.good()) { + LINFO("Writing index file!"); + _indexOctreeManager->writeToFile(outFileStream, false); + + outFileStream.close(); + } + else { + LERROR(fmt::format( + "Error opening file: {} as index output file.", indexFileOutPath + )); + } + + // Make sure all threads are done. + for (int i = 0; i < 8; ++i) { + writeThreads[i].join(); + } +} + +bool ConstructOctreeTask::checkAllFilters(const std::vector& filterValues) { + // Return true if star is caught in any filter. + return (_filterPosX && filterStar(_posX, filterValues[0])) || + (_filterPosY && filterStar(_posY, filterValues[1])) || + (_filterPosZ && filterStar(_posZ, filterValues[2])) || + (_filterGMag && filterStar(_gMag, filterValues[3], 20.f)) || + (_filterBpRp && filterStar(_bpRp, filterValues[4])) || + (_filterVelX && filterStar(_velX, filterValues[5])) || + (_filterVelY && filterStar(_velY, filterValues[6])) || + (_filterVelZ && filterStar(_velZ, filterValues[7])) || + (_filterBpMag && filterStar(_bpMag, filterValues[8], 20.f)) || + (_filterRpMag && filterStar(_rpMag, filterValues[9], 20.f)) || + (_filterBpG && filterStar(_bpG, filterValues[10])) || + (_filterGRp && filterStar(_gRp, filterValues[11])) || + (_filterRa && filterStar(_ra, filterValues[12])) || + (_filterRaError && filterStar(_raError, filterValues[13])) || + (_filterDec && filterStar(_dec, filterValues[14])) || + (_filterDecError && filterStar(_decError, filterValues[15])) || + (_filterParallax && filterStar(_parallax, filterValues[16])) || + (_filterParallaxError && filterStar(_parallaxError, filterValues[17])) || + (_filterPmra && filterStar(_pmra, filterValues[18])) || + (_filterPmraError && filterStar(_pmraError, filterValues[19])) || + (_filterPmdec && filterStar(_pmdec, filterValues[20])) || + (_filterPmdecError && filterStar(_pmdecError, filterValues[21])) || + (_filterRv && filterStar(_rv, filterValues[22])) || + (_filterRvError && filterStar(_rvError, filterValues[23])); +} + +bool ConstructOctreeTask::filterStar(const glm::vec2& range, float filterValue, + float normValue) +{ + // Return true if star should be filtered away, i.e. if min = max = filterValue or + // if filterValue < min (when min != 0.0) or filterValue > max (when max != 0.0). + return (fabs(range.x - range.y) < FLT_EPSILON && + fabs(range.x - filterValue) < FLT_EPSILON) || + (fabs(range.x - normValue) > FLT_EPSILON && filterValue < range.x) || + (fabs(range.y - normValue) > FLT_EPSILON && filterValue > range.y); +} + +documentation::Documentation ConstructOctreeTask::Documentation() { + using namespace documentation; + return { + "ConstructOctreeTask", + "gaiamission_constructoctreefrombin", + { + { + "Type", + new StringEqualVerifier("ConstructOctreeTask"), + Optional::No + }, + { + KeyInFileOrFolderPath, + new StringVerifier, + Optional::No, + "If SingleFileInput is set to true then this specifies the path to a " + "single BIN file containing a full dataset. Otherwise this specifies the " + "path to a folder with multiple BIN files containing subsets of sorted " + "star data.", + }, + { + KeyOutFileOrFolderPath, + new StringVerifier, + Optional::No, + "If SingleFileInput is set to true then this specifies the output file " + "name (including full path). Otherwise this specifies the path to the " + "folder which to save all files.", + }, + { + KeyMaxDist, + new IntVerifier, + Optional::Yes, + "If set it determines what MAX_DIST to use when creating Octree." + }, + { + KeyMaxStarsPerNode, + new IntVerifier, + Optional::Yes, + "If set it determines what MAX_STAR_PER_NODE to use when creating Octree." + }, + { + KeySingleFileInput, + new BoolVerifier, + Optional::Yes, + "If true then task will read from a single file and output a single " + "binary file with the full Octree. If false then task will read all " + "files in specified folder and output multiple files for the Octree." + }, + { + KeyFilterPosX, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Position X values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterPosY, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Position Y values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterPosZ, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Position Z values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterGMag, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with G mean magnitude values between " + "[min, max] will be inserted into Octree (if min is set to 20.0 it is " + "read as -Inf, if max is set to 20.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away. Default " + "GMag = 20.0 if no value existed." + }, + { + KeyFilterBpRp, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Bp-Rp color values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterVelX, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Velocity X values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterVelY, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Velocity Y values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterVelZ, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Velocity Z values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterBpMag, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Bp mean magnitude values between " + "[min, max] will be inserted into Octree (if min is set to 20.0 it is " + "read as -Inf, if max is set to 20.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away. Default " + "BpMag = 20.0 if no value existed." + }, + { + KeyFilterRpMag, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Rp mean magnitude values between " + "[min, max] will be inserted into Octree (if min is set to 20.0 it is " + "read as -Inf, if max is set to 20.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away. Default RpMag = " + "20.0 if no value existed." + }, + { + KeyFilterBpG, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Bp-G color values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterGRp, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with G-Rp color values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterRa, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with RA values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterRaError, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with RA Error values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterDec, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with DEC values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterDecError, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with DEC Error values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterParallax, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Parallax values between [min, max] " + "will be inserted into Octree (if min is set to 0.0 it is read as -Inf, " + "if max is set to 0.0 it is read as +Inf). If min = max then all values " + "equal min|max will be filtered away." + }, + { + KeyFilterParallaxError, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Parallax Error values between " + "[min, max] will be inserted into Octree (if min is set to 0.0 it is " + "read as -Inf, if max is set to 0.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away." + }, + { + KeyFilterPmra, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Proper Motion RA values between " + "[min, max] will be inserted into Octree (if min is set to 0.0 it is " + "read as -Inf, if max is set to 0.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away." + }, + { + KeyFilterPmraError, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Proper Motion RA Error values between " + "[min, max] will be inserted into Octree (if min is set to 0.0 it is " + "read as -Inf, if max is set to 0.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away." + }, + { + KeyFilterPmdec, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Proper Motion DEC values between " + "[min, max] will be inserted into Octree (if min is set to 0.0 it is " + "read as -Inf, if max is set to 0.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away." + }, + { + KeyFilterPmdecError, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Proper Motion DEC Error values between " + "[min, max] will be inserted into Octree (if min is set to 0.0 it is " + "read as -Inf, if max is set to 0.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away." + }, + { + KeyFilterRv, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Radial Velocity values between " + "[min, max] will be inserted into Octree (if min is set to 0.0 it is " + "read as -Inf, if max is set to 0.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away." + }, + { + KeyFilterRvError, + new Vector2Verifier, + Optional::Yes, + "If defined then only stars with Radial Velocity Error values between " + "[min, max] will be inserted into Octree (if min is set to 0.0 it is " + "read as -Inf, if max is set to 0.0 it is read as +Inf). If min = max " + "then all values equal min|max will be filtered away." + }, + } + }; +} + +} // namespace openspace diff --git a/modules/gaia/tasks/constructoctreetask.h b/modules/gaia/tasks/constructoctreetask.h new file mode 100644 index 0000000000..12d50d5c34 --- /dev/null +++ b/modules/gaia/tasks/constructoctreetask.h @@ -0,0 +1,143 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_GAIA___CONSTRUCTOCTREETASK___H__ +#define __OPENSPACE_MODULE_GAIA___CONSTRUCTOCTREETASK___H__ + +#include + +#include +#include + +namespace openspace { + +namespace documentation { struct Documentation; } + +class ConstructOctreeTask : public Task { +public: + ConstructOctreeTask(const ghoul::Dictionary& dictionary); + virtual ~ConstructOctreeTask(); + + std::string description() override; + void perform(const Task::ProgressCallback& onProgress) override; + static documentation::Documentation Documentation(); + +private: + const int RENDER_VALUES = 8; + + /** + * Reads a single binary file with preprocessed star data and insert the render values + * into an octree structure (if star data passed all defined filters). + * Stores the entire octree in one binary file. + */ + void constructOctreeFromSingleFile(const Task::ProgressCallback& progressCallback); + + /** + * Reads binary star data from 8 preprocessed files (one per branch) in specified + * folder, prepared by ReadFitsTask, and inserts star render data into an octree + * (if star data passed all defined filters). + * Stores octree structure in a binary index file and stores all render data + * separate files, one file per node in the octree. + */ + void constructOctreeFromFolder(const Task::ProgressCallback& progressCallback); + + /** + * Checks all defined filter ranges and \returns true if any of the corresponding + * filterValues are outside of the defined range. + * \returns false if value should be inserted into Octree. + * \param filterValues are all read filter values in binary file. + */ + bool checkAllFilters(const std::vector& filterValues); + + /** + * \returns true if star should be filtered away and false if all filters passed. + * \param range contains ]min, max[ and \param filterValue corresponding value in + * star. Star is filtered either if min = max = filterValue or if filterValue < min + * (when min != 0.0) or filterValue > max (when max != 0.0). + */ + bool filterStar(const glm::vec2& range, float filterValue, float normValue = 0.f); + + std::string _inFileOrFolderPath; + std::string _outFileOrFolderPath; + int _maxDist = 0; + int _maxStarsPerNode = 0; + bool _singleFileInput = false; + + std::shared_ptr _octreeManager; + std::shared_ptr _indexOctreeManager; + + // Filter params + glm::vec2 _posX = glm::vec2(0.f); + bool _filterPosX = false; + glm::vec2 _posY = glm::vec2(0.f); + bool _filterPosY = false; + glm::vec2 _posZ = glm::vec2(0.f); + bool _filterPosZ = false; + glm::vec2 _gMag = glm::vec2(0.f); + bool _filterGMag = false; + glm::vec2 _bpRp = glm::vec2(0.f); + bool _filterBpRp = false; + glm::vec2 _velX = glm::vec2(0.f); + bool _filterVelX = false; + glm::vec2 _velY = glm::vec2(0.f); + bool _filterVelY = false; + glm::vec2 _velZ = glm::vec2(0.f); + bool _filterVelZ = false; + glm::vec2 _bpMag = glm::vec2(0.f); + bool _filterBpMag = false; + glm::vec2 _rpMag = glm::vec2(0.f); + bool _filterRpMag = false; + glm::vec2 _bpG = glm::vec2(0.f); + bool _filterBpG = false; + glm::vec2 _gRp = glm::vec2(0.f); + bool _filterGRp = false; + glm::vec2 _ra = glm::vec2(0.f); + bool _filterRa = false; + glm::vec2 _raError = glm::vec2(0.f); + bool _filterRaError = false; + glm::vec2 _dec = glm::vec2(0.f); + bool _filterDec = false; + glm::vec2 _decError = glm::vec2(0.f); + bool _filterDecError = false; + glm::vec2 _parallax = glm::vec2(0.f); + bool _filterParallax = false; + glm::vec2 _parallaxError = glm::vec2(0.f); + bool _filterParallaxError = false; + glm::vec2 _pmra = glm::vec2(0.f); + bool _filterPmra = false; + glm::vec2 _pmraError = glm::vec2(0.f); + bool _filterPmraError = false; + glm::vec2 _pmdec = glm::vec2(0.f); + bool _filterPmdec = false; + glm::vec2 _pmdecError = glm::vec2(0.f); + bool _filterPmdecError = false; + glm::vec2 _rv = glm::vec2(0.f); + bool _filterRv = false; + glm::vec2 _rvError = glm::vec2(0.f); + bool _filterRvError = false; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_GAIA___CONSTRUCTOCTREETASK___H__ diff --git a/modules/gaia/tasks/readfilejob.cpp b/modules/gaia/tasks/readfilejob.cpp new file mode 100644 index 0000000000..d85bb48e4b --- /dev/null +++ b/modules/gaia/tasks/readfilejob.cpp @@ -0,0 +1,265 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include + +namespace { + constexpr const char* _loggerCat = "ReadFileJob"; +} + +namespace openspace::gaia { + +ReadFileJob::ReadFileJob(const std::string& filePath, + const std::vector& allColumns, int firstRow, + int lastRow, size_t nDefaultCols, int nValuesPerStar, + std::shared_ptr fitsReader) + : _inFilePath(filePath) + , _allColumns(allColumns) + , _firstRow(firstRow) + , _lastRow(lastRow) + , _nDefaultCols(nDefaultCols) + , _nValuesPerStar(nValuesPerStar) + , _fitsFileReader(fitsReader) + , _octants(8) +{} + +void ReadFileJob::execute() { + // Read columns from FITS file. If rows aren't specified then full table will be read. + std::shared_ptr> table = _fitsFileReader->readTable( + _inFilePath, + _allColumns, + _firstRow, + _lastRow + ); + + if (!table) { + throw ghoul::RuntimeError( + fmt::format("Failed to open Fits file '{}'", _inFilePath + )); + } + + int nStars = table->readRows - _firstRow + 1; + + int nNullArr = 0; + size_t nColumnsRead = _allColumns.size(); + if (nColumnsRead != _nDefaultCols) { + LINFO("Additional columns will be read! Consider add column in code for " + "significant speedup!"); + } + + // Copy columns to local variables. + std::unordered_map>& tableContent = table->contents; + + // Default columns parameters. + //std::vector l_longitude = std::move(tableContent[_allColumns[0]]); + //std::vector b_latitude = std::move(tableContent[_allColumns[1]]); + std::vector ra = std::move(tableContent[_allColumns[0]]); + std::vector ra_err = std::move(tableContent[_allColumns[1]]); + std::vector dec = std::move(tableContent[_allColumns[2]]); + std::vector dec_err = std::move(tableContent[_allColumns[3]]); + std::vector parallax = std::move(tableContent[_allColumns[4]]); + std::vector parallax_err = std::move(tableContent[_allColumns[5]]); + std::vector pmra = std::move(tableContent[_allColumns[6]]); + std::vector pmra_err = std::move(tableContent[_allColumns[7]]); + std::vector pmdec = std::move(tableContent[_allColumns[8]]); + std::vector pmdec_err = std::move(tableContent[_allColumns[9]]); + std::vector meanMagG = std::move(tableContent[_allColumns[10]]); + std::vector meanMagBp = std::move(tableContent[_allColumns[11]]); + std::vector meanMagRp = std::move(tableContent[_allColumns[12]]); + std::vector bp_rp = std::move(tableContent[_allColumns[13]]); + std::vector bp_g = std::move(tableContent[_allColumns[14]]); + std::vector g_rp = std::move(tableContent[_allColumns[15]]); + std::vector radial_vel = std::move(tableContent[_allColumns[16]]); + std::vector radial_vel_err = std::move(tableContent[_allColumns[17]]); + + + // Construct data array. OBS: ORDERING IS IMPORTANT! This is where slicing happens. + for (int i = 0; i < nStars; ++i) { + std::vector values(_nValuesPerStar); + size_t idx = 0; + + // Default order for rendering: + // Position [X, Y, Z] + // Mean G-band Magnitude + // -- Mean Bp-band Magnitude + // -- Mean Rp-band Magnitude + // Bp-Rp Color + // -- Bp-G Color + // -- G-Rp Color + // Velocity [X, Y, Z] + + // Return early if star doesn't have a measured position. + if (std::isnan(ra[i]) || std::isnan(dec[i])) { + nNullArr++; + continue; + } + + // Store positions. Set to a default distance if parallax doesn't exist. + float radiusInKiloParsec = 9.0; + if (!std::isnan(parallax[i])) { + // Parallax is in milliArcseconds -> distance in kiloParsecs + // https://gea.esac.esa.int/archive/documentation/GDR2/Gaia_archive/ + // chap_datamodel/sec_dm_main_tables/ssec_dm_gaia_source.html + //LINFO("Parallax: " + std::to_string(parallax[i])); + radiusInKiloParsec = 1.0 / parallax[i]; + } + /*// Convert to Galactic Coordinates from Galactic Lon & Lat. + // https://gea.esac.esa.int/archive/documentation/GDR2/Data_processing/ + // chap_cu3ast/sec_cu3ast_intro/ssec_cu3ast_intro_tansforms.html#SSS1 + values[idx++] = radiusInKiloParsec * cos(glm::radians(b_latitude[i])) * + cos(glm::radians(l_longitude[i])); // Pos X + values[idx++] = radiusInKiloParsec * cos(glm::radians(b_latitude[i])) * + sin(glm::radians(l_longitude[i])); // Pos Y + values[idx++] = radiusInKiloParsec * sin(glm::radians(b_latitude[i])); // Pos Z + */ + + // Convert ICRS Equatorial Ra and Dec to Galactic latitude and longitude. + glm::mat3 aPrimG = glm::mat3( + // Col 0 + glm::vec3(-0.0548755604162154, 0.4941094278755837, -0.8676661490190047), + // Col 1 + glm::vec3(-0.8734370902348850, -0.4448296299600112, -0.1980763734312015), + // Col 2 + glm::vec3(-0.4838350155487132, 0.7469822444972189, 0.4559837761750669) + ); + glm::vec3 rICRS = glm::vec3( + cos(glm::radians(ra[i])) * cos(glm::radians(dec[i])), + sin(glm::radians(ra[i])) * cos(glm::radians(dec[i])), + sin(glm::radians(dec[i])) + ); + glm::vec3 rGal = aPrimG * rICRS; + values[idx++] = radiusInKiloParsec * rGal.x; // Pos X + values[idx++] = radiusInKiloParsec * rGal.y; // Pos Y + values[idx++] = radiusInKiloParsec * rGal.z; // Pos Z + + /*if (abs(rGal.x - values[0]) > 1e-5 || abs(rGal.y - values[1]) > 1e-5 || + abs(rGal.z - values[2]) > 1e-5) { + LINFO("rGal: " + std::to_string(rGal) + + " - LB: [" + std::to_string(values[0]) + ", " + std::to_string(values[1]) + + ", " + std::to_string(values[2]) + "]"); + }*/ + + // Store magnitude render value. (Set default to high mag = low brightness) + values[idx++] = std::isnan(meanMagG[i]) ? 20.f : meanMagG[i]; // Mean G-band Mag + + // Store color render value. (Default value is bluish stars) + values[idx++] = std::isnan(bp_rp[i]) ? 0.f : bp_rp[i]; // Bp-Rp Color + + + // Store velocity. + if (std::isnan(pmra[i])) { + pmra[i] = 0.f; + } + if (std::isnan(pmdec[i])) { + pmdec[i] = 0.f; + } + + // Convert Proper Motion from ICRS [Ra,Dec] to Galactic Tanget Vector [l,b]. + glm::vec3 uICRS = glm::vec3( + -sin(glm::radians(ra[i])) * pmra[i] - + cos(glm::radians(ra[i])) * sin(glm::radians(dec[i])) * pmdec[i], + cos(glm::radians(ra[i])) * pmra[i] - + sin(glm::radians(ra[i])) * sin(glm::radians(dec[i])) * pmdec[i], + cos(glm::radians(dec[i])) * pmdec[i] + ); + glm::vec3 pmVecGal = aPrimG * uICRS; + + // Convert to Tangential vector [m/s] from Proper Motion vector [mas/yr] + float tanVelX = 1000.0 * 4.74 * radiusInKiloParsec * pmVecGal.x; + float tanVelY = 1000.0 * 4.74 * radiusInKiloParsec * pmVecGal.y; + float tanVelZ = 1000.0 * 4.74 * radiusInKiloParsec * pmVecGal.z; + + // Calculate True Space Velocity [m/s] if we have the radial velocity + if (!std::isnan(radial_vel[i])) { + // Calculate Radial Velocity in the direction of the star. + // radial_vel is given in [km/s] -> convert to [m/s]. + float radVelX = 1000.0 * radial_vel[i] * rGal.x; + float radVelY = 1000.0 * radial_vel[i] * rGal.y; + float radVelZ = 1000.0 * radial_vel[i] * rGal.z; + + // Use Pythagoras theorem for the final Space Velocity [m/s]. + values[idx++] = sqrt(pow(radVelX, 2) + pow(tanVelX, 2)); // Vel X [U] + values[idx++] = sqrt(pow(radVelY, 2) + pow(tanVelY, 2)); // Vel Y [V] + values[idx++] = sqrt(pow(radVelZ, 2) + pow(tanVelZ, 2)); // Vel Z [W] + } + // Otherwise use the vector [m/s] we got from proper motion. + else { + radial_vel[i] = 0.f; + values[idx++] = tanVelX; // Vel X [U] + values[idx++] = tanVelY; // Vel Y [V] + values[idx++] = tanVelZ; // Vel Z [W] + } + + // Store additional parameters to filter by. + values[idx++] = std::isnan(meanMagBp[i]) ? 20.f : meanMagBp[i]; + values[idx++] = std::isnan(meanMagRp[i]) ? 20.f : meanMagRp[i]; + values[idx++] = std::isnan(bp_g[i]) ? 0.f : bp_g[i]; + values[idx++] = std::isnan(g_rp[i]) ? 0.f : g_rp[i]; + values[idx++] = ra[i]; + values[idx++] = std::isnan(ra_err[i]) ? 0.f : ra_err[i]; + values[idx++] = dec[i]; + values[idx++] = std::isnan(dec_err[i]) ? 0.f : dec_err[i]; + values[idx++] = std::isnan(parallax[i]) ? 0.f : parallax[i]; + values[idx++] = std::isnan(parallax_err[i]) ? 0.f : parallax_err[i]; + values[idx++] = pmra[i]; + values[idx++] = std::isnan(pmra_err[i]) ? 0.f : pmra_err[i]; + values[idx++] = pmdec[i]; + values[idx++] = std::isnan(pmdec_err[i]) ? 0.f : pmdec_err[i]; + values[idx++] = radial_vel[i]; + values[idx++] = std::isnan(radial_vel_err[i]) ? 0.f : radial_vel_err[i]; + + // Read extra columns, if any. This will slow down the sorting tremendously! + for (size_t col = _nDefaultCols; col < nColumnsRead; ++col) { + std::vector vecData = std::move(tableContent[_allColumns[col]]); + values[idx++] = std::isnan(vecData[col]) ? 0.f : vecData[col]; + } + + size_t index = 0; + if (values[0] < 0.0) { + index += 1; + } + if (values[1] < 0.0) { + index += 2; + } + if (values[2] < 0.0) { + index += 4; + } + + _octants[index].insert(_octants[index].end(), values.begin(), values.end()); + } + + /*LINFO(std::to_string(nNullArr) + " out of " + + std::to_string(nStars) + " read stars were nullArrays.");*/ +} + +std::vector> ReadFileJob::product() { + return _octants; +} + +} // namespace openspace::gaiamission diff --git a/modules/gaia/tasks/readfilejob.h b/modules/gaia/tasks/readfilejob.h new file mode 100644 index 0000000000..6433f7c930 --- /dev/null +++ b/modules/gaia/tasks/readfilejob.h @@ -0,0 +1,71 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_GAIA___READFILEJOB___H__ +#define __OPENSPACE_MODULE_GAIA___READFILEJOB___H__ + +#include + +#include + +namespace openspace::gaia { + +struct ReadFileJob : public Job>> { + /** + * Constructs a Job that will read a single FITS file in a concurrent thread and + * divide the star data into 8 octants depending on position. + * \param allColumns define which columns that will be read, it should correspond + * to the pre-defined order in the job. If additional columns are defined they will + * be read but slow down the process. + * Proper conversions of positions and velocities will take place and all values + * will be checked for NaNs. + * If \param firstRow is < 1 then reading will begin at first row in table. + * If \param lastRow < firstRow then entire table will be read. + * \param nValuesPerStar defines how many values that will be stored per star. + */ + ReadFileJob(const std::string& filePath, const std::vector& allColumns, + int firstRow, int lastRow, size_t nDefaultCols, int nValuesPerStar, + std::shared_ptr fitsReader); + + ~ReadFileJob() = default; + + void execute() override; + + std::vector> product() override; + +private: + std::string _inFilePath; + int _firstRow; + int _lastRow; + size_t _nDefaultCols; + int _nValuesPerStar; + std::vector _allColumns; + + std::shared_ptr _fitsFileReader; + std::vector> _octants; +}; + +} // namespace openspace::gaiamission + +#endif // __OPENSPACE_MODULE_GAIA___READFILEJOB___H__ diff --git a/modules/gaia/tasks/readfitstask.cpp b/modules/gaia/tasks/readfitstask.cpp new file mode 100644 index 0000000000..2c5985699c --- /dev/null +++ b/modules/gaia/tasks/readfitstask.cpp @@ -0,0 +1,392 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace { + constexpr const char* KeyInFileOrFolderPath = "InFileOrFolderPath"; + constexpr const char* KeyOutFileOrFolderPath = "OutFileOrFolderPath"; + constexpr const char* KeySingleFileProcess = "SingleFileProcess"; + constexpr const char* KeyThreadsToUse = "ThreadsToUse"; + constexpr const char* KeyFirstRow = "FirstRow"; + constexpr const char* KeyLastRow = "LastRow"; + constexpr const char* KeyFilterColumnNames = "FilterColumnNames"; + + constexpr const char* _loggerCat = "ReadFitsTask"; +} // namespace + +namespace openspace { + +ReadFitsTask::ReadFitsTask(const ghoul::Dictionary& dictionary) { + openspace::documentation::testSpecificationAndThrow( + documentation(), + dictionary, + "ReadFitsTask" + ); + + _inFileOrFolderPath = absPath(dictionary.value(KeyInFileOrFolderPath)); + _outFileOrFolderPath = absPath(dictionary.value(KeyOutFileOrFolderPath)); + + if (dictionary.hasKey(KeySingleFileProcess)) { + _singleFileProcess = dictionary.value(KeySingleFileProcess); + } + + if (dictionary.hasKey(KeyThreadsToUse)) { + _threadsToUse = static_cast(dictionary.value(KeyThreadsToUse)); + if (_threadsToUse < 1) { + LINFO(fmt::format( + "User defined ThreadsToUse was: {}. Will be set to 1", _threadsToUse + )); + _threadsToUse = 1; + } + } + + if (dictionary.hasKey(KeyFirstRow)) { + _firstRow = static_cast(dictionary.value(KeyFirstRow)); + } + + if (dictionary.hasKey(KeyLastRow)) { + _lastRow = static_cast(dictionary.value(KeyLastRow)); + } + + + if (dictionary.hasKey(KeyFilterColumnNames)) { + ghoul::Dictionary d = dictionary.value(KeyFilterColumnNames); + + // Ugly fix for ASCII sorting when there are more columns read than 10. + std::set intKeys; + for (const std::string& key : d.keys()) { + intKeys.insert(std::stoi(key)); + } + + for (int key : intKeys) { + _filterColumnNames.push_back(d.value(std::to_string(key))); + } + } +} + +std::string ReadFitsTask::description() { + return fmt::format( + "Read the specified fits file (or all fits files in specified folder): {}\n and " + "write raw star data into: {}\nAll columns required for default rendering and " + "filtering parameters will always be read but user can define additional filter " + "columns to read.", _inFileOrFolderPath, _outFileOrFolderPath + ); +} + +void ReadFitsTask::perform(const Task::ProgressCallback& progressCallback) { + progressCallback(0.f); + + if (_singleFileProcess) { + readSingleFitsFile(progressCallback); + } + else { + readAllFitsFilesFromFolder(progressCallback); + } + + progressCallback(1.f); +} + +void ReadFitsTask::readSingleFitsFile(const Task::ProgressCallback& progressCallback) { + int32_t nValuesPerStar = 0; + + FitsFileReader fileReader(false); + std::vector fullData = fileReader.readFitsFile( + _inFileOrFolderPath, + nValuesPerStar, + _firstRow, + _lastRow, + _filterColumnNames + ); + + progressCallback(0.8f); + + std::ofstream outFileStream(_outFileOrFolderPath, std::ofstream::binary); + if (outFileStream.good()) { + int32_t nValues = static_cast(fullData.size()); + LINFO(fmt::format("Writing {} values to file {}", nValues, _outFileOrFolderPath)); + LINFO("Number of values per star: " + std::to_string(nValuesPerStar)); + + if (nValues == 0) { + LERROR("Error writing file - No values were read from file."); + } + outFileStream.write( + reinterpret_cast(&nValues), + sizeof(int32_t) + ); + outFileStream.write( + reinterpret_cast(&nValuesPerStar), + sizeof(int32_t) + ); + + size_t nBytes = nValues * sizeof(fullData[0]); + outFileStream.write(reinterpret_cast(fullData.data()), nBytes); + + outFileStream.close(); + } + else { + LERROR(fmt::format( + "Error opening file: {} as output data file.", _outFileOrFolderPath + )); + } +} + +void ReadFitsTask::readAllFitsFilesFromFolder(const Task::ProgressCallback&) { + std::vector> octants(8); + std::vector isFirstWrite(8, true); + size_t finishedJobs = 0; + int totalStars = 0; + + _firstRow = std::max(_firstRow, 1); + + // Create Threadpool and JobManager. + LINFO("Threads in pool: " + std::to_string(_threadsToUse)); + ThreadPool threadPool(_threadsToUse); + ConcurrentJobManager>> jobManager(threadPool); + + // Get all files in specified folder. + ghoul::filesystem::Directory currentDir(_inFileOrFolderPath); + std::vector allInputFiles = currentDir.readFiles(); + size_t nInputFiles = allInputFiles.size(); + LINFO("Files to read: " + std::to_string(nInputFiles)); + + // Define what columns to read. + _allColumnNames.clear(); + // Read in the order of table in file. + std::vector defaultColumnNames = { + "ra", + "ra_error", + "dec", + "dec_error", + "parallax", + "parallax_error", + "pmra", + "pmra_error", + "pmdec", + "pmdec_error", + "phot_g_mean_mag", + "phot_bp_mean_mag", + "phot_rp_mean_mag", + "bp_rp", + "bp_g", + "g_rp", + "radial_velocity", + "radial_velocity_error", + }; + _allColumnNames.insert( + _allColumnNames.end(), + defaultColumnNames.begin(), + defaultColumnNames.end() + ); + // Append additional filter parameters to default rendering parameters. + _allColumnNames.insert( + _allColumnNames.end(), + _filterColumnNames.begin(), + _filterColumnNames.end() + ); + + std::string allNames = "Columns to read: \n"; + for (const std::string& colName : _allColumnNames) { + allNames += colName + "\n"; + } + LINFO(allNames); + + // Declare how many values to save for each star. + int32_t nValuesPerStar = 24; + size_t nDefaultColumns = defaultColumnNames.size(); + auto fitsFileReader = std::make_shared(false); + + // Divide all files into ReadFilejobs and then delegate them onto several threads! + while (!allInputFiles.empty()) { + std::string fileToRead = allInputFiles.back(); + allInputFiles.erase(allInputFiles.end() - 1); + + // Add reading of file to jobmanager, which will distribute it to our threadpool. + auto readFileJob = std::make_shared( + fileToRead, + _allColumnNames, + _firstRow, + _lastRow, + nDefaultColumns, + nValuesPerStar, + fitsFileReader + ); + jobManager.enqueueJob(readFileJob); + } + + LINFO("All files added to queue!"); + + // Check for finished jobs. + while (finishedJobs < nInputFiles) { + if (jobManager.numFinishedJobs() > 0) { + std::vector> newOctant = + jobManager.popFinishedJob()->product(); + + finishedJobs++; + + for (int i = 0; i < 8; ++i) { + // Add read values to global octant and check if it's time to write! + octants[i].insert( + octants[i].end(), + newOctant[i].begin(), + newOctant[i].end() + ); + if ((octants[i].size() > MAX_SIZE_BEFORE_WRITE) || + (finishedJobs == nInputFiles)) + { + // Write to file! + totalStars += writeOctantToFile( + octants[i], + i, + isFirstWrite, + nValuesPerStar + ); + + octants[i].clear(); + octants[i].shrink_to_fit(); + } + } + } + } + LINFO(fmt::format("A total of {} stars were written to binary files.", totalStars)); +} + +int ReadFitsTask::writeOctantToFile(const std::vector& octantData, int index, + std::vector& isFirstWrite, int nValuesPerStar) +{ + std::string outPath = fmt::format("{}octant_{}.bin", _outFileOrFolderPath, index); + std::ofstream fileStream(outPath, std::ofstream::binary | std::ofstream::app); + if (fileStream.good()) { + int32_t nValues = static_cast(octantData.size()); + LINFO("Write " + std::to_string(nValues) + " values to " + outPath); + + if (nValues == 0) { + LERROR("Error writing file - No values were read from file."); + } + // If this is the first write then write number of values per star! + if (isFirstWrite[index]) { + LINFO("First write for Octant_" + std::to_string(index)); + fileStream.write( + reinterpret_cast(&nValuesPerStar), + sizeof(int32_t) + ); + isFirstWrite[index] = false; + } + + size_t nBytes = nValues * sizeof(octantData[0]); + fileStream.write(reinterpret_cast(octantData.data()), nBytes); + + fileStream.close(); + + // Return number of stars written. + return nValues / nValuesPerStar; + } + else { + LERROR(fmt::format("Error opening file: {} as output data file.", outPath)); + return 0; + } +} + +documentation::Documentation ReadFitsTask::Documentation() { + using namespace documentation; + return { + "ReadFitsFile", + "gaiamission_fitsfiletorawdata", + { + { + "Type", + new StringEqualVerifier("ReadFitsTask"), + Optional::No + }, + { + KeyInFileOrFolderPath, + new StringVerifier, + Optional::No, + "If SingleFileProcess is set to true then this specifies the path to a " + "single FITS file that will be read. Otherwise it specifies the path to " + "a folder with multiple FITS files that are to be read.", + }, + { + KeyOutFileOrFolderPath, + new StringVerifier, + Optional::No, + "If SingleFileProcess is set to true then this specifies the name " + "(including entire path) to the output file. Otherwise it specifies the " + "path to the output folder which to export binary star data to.", + }, + { + KeySingleFileProcess, + new BoolVerifier, + Optional::Yes, + "If true then task will read from a single FITS file and output a single " + "binary file. If false then task will read all files in specified folder " + "and output multiple files sorted by location." + }, + { + KeyThreadsToUse, + new IntVerifier, + Optional::Yes, + "Defines how many threads to use when reading from multiple files." + }, + { + KeyFirstRow, + new IntVerifier, + Optional::Yes, + "Defines the first row that will be read from the specified FITS " + "file(s). If not defined then reading will start at first row.", + }, + { + KeyLastRow, + new IntVerifier, + Optional::Yes, + "Defines the last row that will be read from the specified FITS file(s). " + "If not defined (or less than FirstRow) then full file(s) will be read.", + }, + { + KeyFilterColumnNames, + new StringListVerifier, + Optional::Yes, + "A list of strings with the names of all the additional columns that are " + "to be read from the specified FITS file(s). These columns can be used " + "for filtering while constructing Octree later.", + }, + + } + }; +} + +} // namespace openspace diff --git a/modules/gaia/tasks/readfitstask.h b/modules/gaia/tasks/readfitstask.h new file mode 100644 index 0000000000..1a917633b9 --- /dev/null +++ b/modules/gaia/tasks/readfitstask.h @@ -0,0 +1,82 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_GAIA___READFITSTASK___H__ +#define __OPENSPACE_MODULE_GAIA___READFITSTASK___H__ + +#include +#include +#include +#include + +namespace openspace { + +namespace documentation { struct Documentation; } + +class ReadFitsTask : public Task { +public: + ReadFitsTask(const ghoul::Dictionary& dictionary); + virtual ~ReadFitsTask() = default; + + std::string description() override; + void perform(const Task::ProgressCallback& onProgress) override; + static documentation::Documentation Documentation(); + +private: + const size_t MAX_SIZE_BEFORE_WRITE = 48000000; // ~183MB -> 2M stars with 24 values + //const size_t MAX_SIZE_BEFORE_WRITE = 9000000; // ~34MB -> 0,5 stars with 18 values + + /** + * Reads a single FITS file and stores ordered star data in one binary file. + */ + void readSingleFitsFile(const Task::ProgressCallback& progressCallback); + + /** + * Reads all FITS files in a folder with multiple threads and stores ordered star + * data into 8 binary files. + */ + void readAllFitsFilesFromFolder(const Task::ProgressCallback& progressCallback); + + /** + * Writes \param data to octant [\param index] file. + * \param isFirstWrite defines if this is the first write to specified octant, if so + * the file is created, otherwise the accumulated data is appended to the end of the + * file. + */ + int writeOctantToFile(const std::vector& data, int index, + std::vector& isFirstWrite, int nValuesPerStar); + + std::string _inFileOrFolderPath; + std::string _outFileOrFolderPath; + bool _singleFileProcess = false; + size_t _threadsToUse = 1; + int _firstRow = 0; + int _lastRow = 0; + std::vector _allColumnNames; + std::vector _filterColumnNames; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_GAIA___READFITSTASK___H__ diff --git a/modules/gaia/tasks/readspecktask.cpp b/modules/gaia/tasks/readspecktask.cpp new file mode 100644 index 0000000000..409028c78d --- /dev/null +++ b/modules/gaia/tasks/readspecktask.cpp @@ -0,0 +1,122 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr const char* KeyInFilePath = "InFilePath"; + constexpr const char* KeyOutFilePath = "OutFilePath"; + + constexpr const char* _loggerCat = "ReadSpeckTask"; +} // namespace + +namespace openspace { + +ReadSpeckTask::ReadSpeckTask(const ghoul::Dictionary& dictionary) { + openspace::documentation::testSpecificationAndThrow( + documentation(), + dictionary, + "ReadSpeckTask" + ); + + _inFilePath = absPath(dictionary.value(KeyInFilePath)); + _outFilePath = absPath(dictionary.value(KeyOutFilePath)); +} + +std::string ReadSpeckTask::description() { + return fmt::format( + "Read speck file {} and write raw star data into {}", _inFilePath, _outFilePath + ); +} + +void ReadSpeckTask::perform(const Task::ProgressCallback& progressCallback) { + progressCallback(0.f); + + int32_t nRenderValues = 0; + + FitsFileReader fileReader(false); + std::vector fullData = fileReader.readSpeckFile(_inFilePath, nRenderValues); + + progressCallback(0.9f); + + std::ofstream fileStream(_outFilePath, std::ofstream::binary); + if (fileStream.good()) { + int32_t nValues = static_cast(fullData.size()); + LINFO("nValues: " + std::to_string(nValues)); + + if (nValues == 0) { + LERROR("Error writing file - No values were read from file."); + } + fileStream.write(reinterpret_cast(&nValues), sizeof(int32_t)); + fileStream.write(reinterpret_cast(&nRenderValues), sizeof(int32_t)); + + size_t nBytes = nValues * sizeof(fullData[0]); + fileStream.write(reinterpret_cast(fullData.data()), nBytes); + + fileStream.close(); + } + else { + LERROR(fmt::format("Error opening file: {} as output data file.", _outFilePath)); + } + + progressCallback(1.f); +} + +documentation::Documentation ReadSpeckTask::Documentation() { + using namespace documentation; + return { + "ReadSpeckTask", + "gaiamission_speckfiletorawdata", + { + { + "Type", + new StringEqualVerifier("ReadSpeckTask"), + Optional::No + }, + { + KeyInFilePath, + new StringVerifier, + Optional::No, + "The path to the SPECK file that are to be read.", + }, + { + KeyOutFilePath, + new StringVerifier, + Optional::No, + "The path to the file to export raw VBO data to.", + }, + } + }; +} + +} // namespace openspace diff --git a/modules/gaia/tasks/readspecktask.h b/modules/gaia/tasks/readspecktask.h new file mode 100644 index 0000000000..303006fa98 --- /dev/null +++ b/modules/gaia/tasks/readspecktask.h @@ -0,0 +1,50 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_GAIA___READSPECKTASK___H__ +#define __OPENSPACE_MODULE_GAIA___READSPECKTASK___H__ + +#include + +namespace openspace { + +namespace documentation { struct Documentation; } + +class ReadSpeckTask : public Task { +public: + ReadSpeckTask(const ghoul::Dictionary& dictionary); + virtual ~ReadSpeckTask() = default; + + std::string description() override; + void perform(const Task::ProgressCallback& onProgress) override; + static documentation::Documentation Documentation(); + +private: + std::string _inFilePath; + std::string _outFilePath; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_GAIA___READSPECKTASK___H__ diff --git a/modules/server/src/jsonconverters.cpp b/modules/server/src/jsonconverters.cpp index ac17df54fe..20e3ad2012 100644 --- a/modules/server/src/jsonconverters.cpp +++ b/modules/server/src/jsonconverters.cpp @@ -33,12 +33,6 @@ using json = nlohmann::json; namespace openspace::properties { -namespace { - - - -} // namespace - void to_json(json& j, const Property& p) { std::string description = p.generateBaseJsonDescription(); json desc = json::parse(description); diff --git a/modules/space/CMakeLists.txt b/modules/space/CMakeLists.txt index 764761c42d..d17204258a 100644 --- a/modules/space/CMakeLists.txt +++ b/modules/space/CMakeLists.txt @@ -34,6 +34,7 @@ set(HEADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/translation/keplertranslation.h ${CMAKE_CURRENT_SOURCE_DIR}/translation/spicetranslation.h ${CMAKE_CURRENT_SOURCE_DIR}/translation/tletranslation.h + ${CMAKE_CURRENT_SOURCE_DIR}/translation/horizonstranslation.h ${CMAKE_CURRENT_SOURCE_DIR}/rotation/spicerotation.h ) source_group("Header Files" FILES ${HEADER_FILES}) @@ -48,6 +49,7 @@ set(SOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/translation/keplertranslation.cpp ${CMAKE_CURRENT_SOURCE_DIR}/translation/spicetranslation.cpp ${CMAKE_CURRENT_SOURCE_DIR}/translation/tletranslation.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/translation/horizonstranslation.cpp ${CMAKE_CURRENT_SOURCE_DIR}/rotation/spicerotation.cpp ) source_group("Source Files" FILES ${SOURCE_FILES}) diff --git a/modules/space/shaders/star_fs.glsl b/modules/space/shaders/star_fs.glsl index d6ef38ec0c..8e8c5e67d3 100644 --- a/modules/space/shaders/star_fs.glsl +++ b/modules/space/shaders/star_fs.glsl @@ -120,4 +120,4 @@ Fragment getFragment() { } return frag; -} \ No newline at end of file +} diff --git a/modules/space/shaders/star_ge.glsl b/modules/space/shaders/star_ge.glsl index 4dd4ad6378..b6fc81b941 100644 --- a/modules/space/shaders/star_ge.glsl +++ b/modules/space/shaders/star_ge.glsl @@ -105,4 +105,4 @@ void main() { } EndPrimitive(); -} \ No newline at end of file +} diff --git a/modules/space/shaders/star_vs.glsl b/modules/space/shaders/star_vs.glsl index f1dac63c31..37040bd3c4 100644 --- a/modules/space/shaders/star_vs.glsl +++ b/modules/space/shaders/star_vs.glsl @@ -56,4 +56,4 @@ void main() { position = vec4(1E-19, 1E-19, 1E-19, 1.0) * vs_gPosition; gl_Position = position; -} \ No newline at end of file +} diff --git a/modules/space/spacemodule.cpp b/modules/space/spacemodule.cpp index 265f3aa0b1..4293b44e8e 100644 --- a/modules/space/spacemodule.cpp +++ b/modules/space/spacemodule.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -86,6 +87,7 @@ void SpaceModule::internalInitialize(const ghoul::Dictionary&) { fTranslation->registerClass("KeplerTranslation"); fTranslation->registerClass("SpiceTranslation"); fTranslation->registerClass("TLETranslation"); + fTranslation->registerClass("HorizonsTranslation"); auto fRotation = FactoryManager::ref().factory(); ghoul_assert(fRotation, "Rotation factory was not created"); @@ -111,6 +113,7 @@ std::vector SpaceModule::documentations() const { SpiceTranslation::Documentation(), KeplerTranslation::Documentation(), TLETranslation::Documentation(), + HorizonsTranslation::Documentation(), planetgeometry::PlanetGeometry::Documentation(), planetgeometry::SimpleSphereGeometry::Documentation() }; diff --git a/modules/space/translation/horizonstranslation.cpp b/modules/space/translation/horizonstranslation.cpp new file mode 100644 index 0000000000..2d54dce171 --- /dev/null +++ b/modules/space/translation/horizonstranslation.cpp @@ -0,0 +1,189 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this * + * software and associated documentation files (the "Software"), to deal in the Software * + * without restriction, including without limitation the rights to use, copy, modify, * + * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * + * permit persons to whom the Software is furnished to do so, subject to the following * + * conditions: * + * * + * The above copyright notice and this permission notice shall be included in all copies * + * or substantial portions of the Software. * + * * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * + ****************************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr const char* _loggerCat = "HorizonsTranslation"; +} // namespace + +namespace { + constexpr openspace::properties::Property::PropertyInfo HorizonsTextFileInfo = { + "HorizonsTextFile", + "Horizons Text File", + "This value is the path to the text file generated by Horizons with observer " + "range and Galactiv longitude and latitude for different timestamps." + }; +} // namespace + +namespace openspace { + +documentation::Documentation HorizonsTranslation::Documentation() { + using namespace documentation; + return { + "Horizons Translation", + "base_transform_translation_horizons", + { + { + "Type", + new StringEqualVerifier("HorizonsTranslation"), + Optional::No + }, + { + HorizonsTextFileInfo.identifier, + new StringVerifier, + Optional::No, + HorizonsTextFileInfo.description + } + } + }; +} + + +HorizonsTranslation::HorizonsTranslation() + : _horizonsTextFile(HorizonsTextFileInfo) +{ + addProperty(_horizonsTextFile); + + _horizonsTextFile.onChange([&](){ + requireUpdate(); + _fileHandle = std::make_unique(_horizonsTextFile); + _fileHandle->setCallback([&](const ghoul::filesystem::File&) { + requireUpdate(); + notifyObservers(); + }); + readHorizonsTextFile(_horizonsTextFile); + }); +} + +HorizonsTranslation::HorizonsTranslation(const ghoul::Dictionary& dictionary) + : HorizonsTranslation() +{ + documentation::testSpecificationAndThrow( + Documentation(), + dictionary, + "HorizonsTranslation" + ); + + _horizonsTextFile = absPath( + dictionary.value(HorizonsTextFileInfo.identifier) + ); + + // Read specified file and store it in memory. + readHorizonsTextFile(_horizonsTextFile); +} + +glm::dvec3 HorizonsTranslation::position(const UpdateData& data) const { + glm::dvec3 interpolatedPos = glm::dvec3(0.0); + + auto lastBefore = _timeline.lastKeyframeBefore(data.time.j2000Seconds(), true); + auto firstAfter = _timeline.firstKeyframeAfter(data.time.j2000Seconds(), false); + if (lastBefore && firstAfter) { + // We're inbetween first and last value. + double timelineDiff = firstAfter->timestamp - lastBefore->timestamp; + double timeDiff = data.time.j2000Seconds() - lastBefore->timestamp; + double diff = (timelineDiff > DBL_EPSILON) ? timeDiff / timelineDiff : 0.0; + + glm::dvec3 dir = firstAfter->data - lastBefore->data; + interpolatedPos = lastBefore->data + dir * diff; + } + else if (lastBefore) { + // Requesting a time after last value. Return last known position. + interpolatedPos = lastBefore->data; + } + else if (firstAfter) { + // Requesting a time before first value. Return last known position. + interpolatedPos = firstAfter->data; + } + + return interpolatedPos; +} + +void HorizonsTranslation::readHorizonsTextFile(const std::string& horizonsTextFilePath) { + std::ifstream fileStream(horizonsTextFilePath); + + if (!fileStream.good()) { + LERROR(fmt::format( + "Failed to open Horizons text file '{}'", horizonsTextFilePath + )); + return; + } + + // The beginning of a Horizons file has a header with a lot of information about the + // query that we do not care about. Ignore everything until data starts, including + // the row marked by $$SOE (i.e. Start Of Ephemerides). + std::string line = ""; + while (line[0] != '$') { + std::getline(fileStream, line); + } + + // Read data line by line until $$EOE (i.e. End Of Ephemerides). + // Skip the rest of the file. + std::getline(fileStream, line); + while (line[0] != '$') { + std::stringstream str(line); + std::string date; + std::string time; + float range = 0; + float gLon = 0; + float gLat = 0; + + // File is structured by: + // YYYY-MM-DD + // HH:MM:SS + // Range-to-observer (km) + // Range-delta (km/s) -- suppressed! + // Galactic Longitude (degrees) + // Galactic Latitude (degrees) + str >> date >> time >> range >> gLon >> gLat; + + // Convert date and time to seconds after 2000 + // and pos to Galactic positions in meter from Observer. + std::string timeString = date + " " + time; + double timeInJ2000 = Time::convertTime(timeString); + glm::dvec3 gPos = glm::dvec3( + 1000 * range * cos(glm::radians(gLat)) * cos(glm::radians(gLon)), + 1000 * range * cos(glm::radians(gLat)) * sin(glm::radians(gLon)), + 1000 * range * sin(glm::radians(gLat)) + ); + + // Add position to stored timeline. + _timeline.addKeyframe(timeInJ2000, gPos); + + std::getline(fileStream, line); + } + fileStream.close(); +} + +} // namespace openspace diff --git a/modules/space/translation/horizonstranslation.h b/modules/space/translation/horizonstranslation.h new file mode 100644 index 0000000000..6523deeeec --- /dev/null +++ b/modules/space/translation/horizonstranslation.h @@ -0,0 +1,72 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2018 * + * * + * 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_SPACE___HORIZONSTRANSLATION___H__ +#define __OPENSPACE_MODULE_SPACE___HORIZONSTRANSLATION___H__ + +#include + +#include +#include +#include +#include +#include + +namespace openspace { + +namespace documentation { struct Documentation; } + + +/** + * The HorizonsTranslation is based on text files generated from NASA JPL HORIZONS Website + * (https://ssd.jpl.nasa.gov/horizons.cgi). The implementation expects a file with format: + * TIME(YYYY-MM-DD HH:MM:SS) Range(km) GalLon(degrees) GalLat(degrees) + * Range - The distance from target to observer. Chosen as "Observer range & range-rate" + * in "Table Setting". This also generates a delta that can be suppressed under "Optional + * observer-table settings" to limit file size. User must set output settings to + * kilometers. + * GalLon - Galactic Longitude. User must set output to Degrees in "Table Settings". + * GalLat - Galactic Latitude. User must set output to Degrees in "Table Settings". + */ +class HorizonsTranslation : public Translation { +public: + HorizonsTranslation(); + HorizonsTranslation(const ghoul::Dictionary& dictionary); + + glm::dvec3 position(const UpdateData& time) const override; + + static documentation::Documentation Documentation(); + +private: + void readHorizonsTextFile(const std::string& _horizonsTextFilePath); + + properties::StringProperty _horizonsTextFile; + std::unique_ptr _fileHandle; + ghoul::lua::LuaState _state; + Timeline _timeline; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_SPACE___HORIZONSTRANSLATION___H__ diff --git a/modules/sync/tasks/syncassettask.cpp b/modules/sync/tasks/syncassettask.cpp index 658e3309ab..83899d8c22 100644 --- a/modules/sync/tasks/syncassettask.cpp +++ b/modules/sync/tasks/syncassettask.cpp @@ -36,6 +36,12 @@ #include #include #include +#include + +#include +#include +#include + #include namespace { diff --git a/modules/webbrowser/src/webrenderhandler.cpp b/modules/webbrowser/src/webrenderhandler.cpp index 9dc606c2b4..8b70a4ccc4 100644 --- a/modules/webbrowser/src/webrenderhandler.cpp +++ b/modules/webbrowser/src/webrenderhandler.cpp @@ -92,7 +92,7 @@ void WebRenderHandler::OnPaint(CefRefPtr browser, // Add the dirty rect bounds to the GPU texture dirty rect. _lowerDirtyRectBound = glm::min(lowerUpdatingRectBound, _lowerDirtyRectBound); _upperDirtyRectBound = glm::max(upperUpdatingRectBound, _upperDirtyRectBound); - _needsRepaint = false; + _needsRepaint = false; } void WebRenderHandler::updateTexture() { diff --git a/openspace.cfg b/openspace.cfg index 93fe7d8c12..88b96514c1 100644 --- a/openspace.cfg +++ b/openspace.cfg @@ -32,6 +32,8 @@ SGCTConfig = sgct.config.single{} -- SGCTConfig = "${CONFIG}/openvr_oculusRiftCv1.xml" -- SGCTConfig = "${CONFIG}/openvr_htcVive.xml" + + -- Sets the scene that is to be loaded by OpenSpace. A scene file is a description -- of all entities that will be visible during an instance of OpenSpace @@ -41,6 +43,7 @@ Asset = "default" -- Asset = "rosetta" -- Asset = "osirisrex" -- Asset = "voyager" +-- Asset = "gaia" -- Asset = "juno" -- Asset = "messenger" diff --git a/src/properties/optionproperty.cpp b/src/properties/optionproperty.cpp index c7a0200b11..7690f36c6f 100644 --- a/src/properties/optionproperty.cpp +++ b/src/properties/optionproperty.cpp @@ -97,6 +97,9 @@ void OptionProperty::setValue(int value) { const Option& o = _options[i]; if (o.value == value) { // If it does, set it by calling the superclasses setValue method + // @TODO(abock): This should be setValue(value) instead or otherwise the + // stored indices and option values start to drift if the + // operator T of the OptionProperty is used NumericalProperty::setValue(static_cast(i)); return; } diff --git a/src/properties/propertyowner.cpp b/src/properties/propertyowner.cpp index 92a1853ace..5a2579ee1a 100644 --- a/src/properties/propertyowner.cpp +++ b/src/properties/propertyowner.cpp @@ -130,6 +130,16 @@ bool PropertyOwner::hasProperty(const std::string& uri) const { return property(uri) != nullptr; } +bool PropertyOwner::hasProperty(const Property* prop) const { + ghoul_precondition(prop != nullptr, "prop must not be nullptr"); + + std::vector::const_iterator it = std::find( + _properties.begin(), _properties.end(), prop + ); + + return it != _properties.end(); +} + const std::vector& PropertyOwner::propertySubOwners() const { return _subOwners; } diff --git a/src/util/distanceconversion.cpp b/src/util/distanceconversion.cpp index 3cdb3b27d9..349764cc8f 100644 --- a/src/util/distanceconversion.cpp +++ b/src/util/distanceconversion.cpp @@ -24,6 +24,9 @@ #include +#include +#include + #include namespace openspace { @@ -93,4 +96,13 @@ std::pair simplifyDistance(double meters, bool forceSingula } } +float convertMasPerYearToMeterPerSecond(float masPerYear, float parallax) { + double degreeFromMas = 1.0 / 3600000.0; + double radiusInMeter = (distanceconstants::Parsec * 1000.0) / parallax; + double perYearToPerSecond = 1.0 / SecondsPerYear; + double meterPerSecond = masPerYear * degreeFromMas * radiusInMeter * + perYearToPerSecond; + return static_cast(meterPerSecond); +} + } // namespace openspace diff --git a/src/util/transformationmanager.cpp b/src/util/transformationmanager.cpp index 08a7b3975a..b002c54941 100644 --- a/src/util/transformationmanager.cpp +++ b/src/util/transformationmanager.cpp @@ -41,6 +41,10 @@ #endif // WIN32 #endif +namespace { + constexpr const char* _loggerCat = "TransformationManager"; +} + namespace openspace { TransformationManager* TransformationManager::_instance = nullptr; diff --git a/tests/test_optionproperty.inl b/tests/test_optionproperty.inl index 341d351bcf..5c87671a28 100644 --- a/tests/test_optionproperty.inl +++ b/tests/test_optionproperty.inl @@ -68,9 +68,7 @@ TEST_F(OptionPropertyTest, SingleOptionSinglePositive) { TEST_F(OptionPropertyTest, SingleOptionMultipleZero) { openspace::properties::OptionProperty p({ "id", "gui", "desc" }); - p.addOptions({ - { 0, "a" } - }); + p.addOptions({ "a" }); p = 0; ASSERT_EQ(0, p.option().value);