diff --git a/data/assets/examples/pointclouds/data/dummydata.csv b/data/assets/examples/pointclouds/data/dummydata.csv index e8b698833f..0df526dbac 100644 --- a/data/assets/examples/pointclouds/data/dummydata.csv +++ b/data/assets/examples/pointclouds/data/dummydata.csv @@ -1,3 +1,6 @@ +# A dummy dataset with an xyz position and some random data columns. +# The last two columns has data values with either missing values or +# Nan values (which can be handled seprately when color mapping) x,y,z,a,b,normaldist_withMissing,number_withNan 13428000,26239000,45870000,-3.226548224,33.95773276,-0.357778948,29 14727000,45282000,10832000,45.05941924,-106.0395917,,29 diff --git a/data/assets/examples/pointclouds/data/interpolation_expand.csv b/data/assets/examples/pointclouds/data/interpolation_expand.csv index 3bd9aede78..b0f3e25a9a 100644 --- a/data/assets/examples/pointclouds/data/interpolation_expand.csv +++ b/data/assets/examples/pointclouds/data/interpolation_expand.csv @@ -1,3 +1,12 @@ +# A test dataset for interpolation, where the xyz positions expand outward in each +# time step. There are 10 points per timestep, as illustrated by the time column. +# There are also two columns that may be used for color mapping: +# +# static_value has values that are the same for the correpsonding point in each +# time step +# dynamic_value has values that change for every timestep. The change will be +# reflected in the interpolation +# time,x,y,z,dynamic_value,static_value 0.0,675.0297905065192,1672.6820684730765,-124.14442820502654,1,1 0.0,9.0852354697237,1080.363474597831,266.4506394528842,3,3 diff --git a/data/assets/examples/pointclouds/data/interpolation_randomwalk.csv b/data/assets/examples/pointclouds/data/interpolation_randomwalk.csv index f8681b6d42..09ed368662 100644 --- a/data/assets/examples/pointclouds/data/interpolation_randomwalk.csv +++ b/data/assets/examples/pointclouds/data/interpolation_randomwalk.csv @@ -1,3 +1,5 @@ +# A test dataset for interpolation, where the xyz vary in a random walk patterin +# time step. There are 10 points per timestep, as illustrated by the time column time,x,y,z 0.0,675.0297905065192,1672.6820684730765,-124.14442820502654 0.0,9.0852354697237,1080.363474597831,266.4506394528842 diff --git a/data/assets/examples/pointclouds/data/textured_csv/textured_points.csv b/data/assets/examples/pointclouds/data/textured_csv/textured_points.csv new file mode 100644 index 0000000000..0bf863e4e6 --- /dev/null +++ b/data/assets/examples/pointclouds/data/textured_csv/textured_points.csv @@ -0,0 +1,9 @@ +# A dummy dataset with an xyz position, some random values and an integer value to +# use for texturing the points +# +# The texture mapping from index to file is handled in another file +x,y,z,a,b,texture +13428000,26239000,45870000,-3.226548224,33.95773276,1 +14727000,45282000,10832000,45.05941924,-106.0395917,0 +24999000,28370000,19911000,-70.58906931,154.1851656,2 +26539000,36165000,39582000,-13.3663358,71.79484733,3 diff --git a/data/assets/examples/pointclouds/data/textured_csv/texturemap.tmap b/data/assets/examples/pointclouds/data/textured_csv/texturemap.tmap new file mode 100644 index 0000000000..0a475f8d61 --- /dev/null +++ b/data/assets/examples/pointclouds/data/textured_csv/texturemap.tmap @@ -0,0 +1,8 @@ +# The texture map is a mapping between an index and the name of an image file. +# All the images should be located in the same folder, or the name need to be specified as a path relative +# to a specific folder + +0 test3.jpg +1 test2.jpg +2 test.jpg +3 openspace-horiz-logo.png diff --git a/data/assets/examples/pointclouds/data/textured_speck/textures_points.speck b/data/assets/examples/pointclouds/data/textured_speck/textures_points.speck new file mode 100644 index 0000000000..2e8b763c2c --- /dev/null +++ b/data/assets/examples/pointclouds/data/textured_speck/textures_points.speck @@ -0,0 +1,16 @@ +# A dummy dataset with an xyz position, some random values and an integer value to +# use for texturing the points + +datavar 0 a +datavar 1 b +datavar 2 texture +texturevar 2 # The index of the data column that has the texture data +texture 0 test3.jpg +texture 1 test.jpg +texture 2 test.jpg +texture 3 openspace-horiz-logo.png + +13428000 26239000 45870000 -3.226548224 33.95773276 0 +14727000 45282000 10832000 45.05941924 -106.0395917 2 +24999000 28370000 19911000 -70.58906931 154.1851656 3 +26539000 36165000 39582000 -13.3663358 71.79484733 1 diff --git a/data/assets/examples/pointclouds/multitextured_points.asset b/data/assets/examples/pointclouds/multitextured_points.asset new file mode 100644 index 0000000000..e236893562 --- /dev/null +++ b/data/assets/examples/pointclouds/multitextured_points.asset @@ -0,0 +1,107 @@ +-- CSV +local Test = { + Identifier = "TexturedPointCloudExample_CSV", + Renderable = { + Type = "RenderablePointCloud", + File = asset.resource("data/textured_csv/textured_points.csv"), + DataMapping = { + -- The name of the column in the CSV file that corresponds to the texture (should + -- be an integer) + TextureColumn = "texture", + -- A Texture mapping file that provides information about which value/index + -- corresponds to which texture file + TextureMapFile = asset.resource("data/textured_csv/texturemap.tmap") + }, + Texture = { + -- Where to find the texture files (in this case, in the OpenSpace data folder) + Folder = openspace.absPath("${DATA}") + }, + UseAdditiveBlending = false + }, + GUI = { + Name = "Multi-Textured Points", + Path = "/Example/Point Clouds/Multi-Textured" + } +} + + +-- Interpolated +-- Multi-texturing works also for interpolated point clouds. Here we let the same +-- dataset as used above be interpreted as representing only two points, with a different +-- texture. Note that the textures will be set based on the first two data items and will +-- not be changed during interpolation +local Test_Interpolated = { + Identifier = "TexturedPointCloudExample_Interpolated", + Renderable = { + Type = "RenderableInterpolatedPoints", + File = asset.resource("data/textured_csv/textured_points.csv"), + NumberOfObjects = 2, + DataMapping = { + TextureColumn = "texture", + TextureMapFile = asset.resource("data/textured_csv/texturemap.tmap") + }, + Texture = { + Folder = openspace.absPath("${DATA}") + }, + UseAdditiveBlending = false + }, + GUI = { + Name = "Multi-Textured Points (Interpolation)", + Path = "/Example/Point Clouds/Multi-Textured" + } +} + +-- Speck file (allows storing all data in one single file, including the texture mapping) +-- Note that we disable this scene graph node per default here, as it shows the same +-- information as the CSV version +local Test_Speck = { + Identifier = "TexturedPointCloudExample_Speck", + Renderable = { + Type = "RenderablePointCloud", + Enabled = false, + -- When loading multi-texture information from a speck file, we do not need a + -- DataMapping entry - all information is in the file + File = asset.resource("data/textured_speck/textures_points.speck"), + Texture = { + -- However, we do still need to specify where the textures are located + Folder = openspace.absPath("${DATA}") + }, + UseAdditiveBlending = false + }, + GUI = { + Name = "Multi-Textured Points (Speck file)", + Path = "/Example/Point Clouds/Multi-Textured" + } +} + + +asset.onInitialize(function() + openspace.addSceneGraphNode(Test) + openspace.addSceneGraphNode(Test_Interpolated) + openspace.addSceneGraphNode(Test_Speck) +end) + +asset.onDeinitialize(function() + openspace.removeSceneGraphNode(Test_Speck) + openspace.removeSceneGraphNode(Test_Interpolated) + openspace.removeSceneGraphNode(Test) +end) + +asset.export(Test) +asset.export(Test_Interpolated) +asset.export(Test_Speck) + + +asset.meta = { + Name = "Multi-textured Points", + Version = "1.0", + Description = [[Example of point clouds where multiple textures are used for the points, + based on information in the dataset. The dataset may be either CSV or Speck format. + If CSV is used, additional information must be provided through the DataMapping: 1) + Which column in the dataset that corresponds to the texture, and a separate file that + maps that value to a texture file + ]], + Author = "OpenSpace Team", + URL = "http://openspaceproject.com", + License = "MIT license" +} diff --git a/data/assets/examples/pointclouds/points.asset b/data/assets/examples/pointclouds/points.asset index b14f00906e..9129b14046 100644 --- a/data/assets/examples/pointclouds/points.asset +++ b/data/assets/examples/pointclouds/points.asset @@ -100,13 +100,19 @@ local FixedColor_ScaleBasedOnData = { FixedColor = { 0.5, 0.5, 0.0 } }, SizeSettings = { - -- The options for the columns that the points can be scaled by. The first - -- alternative is chosen per default - SizeMapping = { "number_withNan", "a" }, + SizeMapping = { + -- The options for the columns that the points can be scaled by. The first + -- alternative in the list is chosen per default + ParameterOptions = { "a", "b" }, + -- Specify which option we want to use for size mapping at start up. Here we + -- use the last of the provided options rather than the first one, which is + -- otherwise used by default + Parameter = "b" + }, -- Use a slightly smaller scale than above for the base size of the points -- (will decide the size of the smallest point). That way, the points don't -- become too big when scaled by the data parameter - ScaleExponent = 5 + ScaleExponent = 5.0 } }, GUI = { @@ -131,11 +137,13 @@ local Textured = { Renderable = { Type = "RenderablePointCloud", File = asset.resource("data/dummydata.csv"), - -- The path to the texture file. Here we use openspace.absPath so that we can use - -- the ${DATA} token to get the path to a texture in the "OpenSpace/data" folder, - -- but for a file at a relative location it would also work to use asset.resource, - -- like for the data file above - Texture = openspace.absPath("${DATA}/test3.jpg"), + Texture = { + -- The path to the texture file. Here we use openspace.absPath so that we can use + -- the ${DATA} token to get the path to a texture in the "OpenSpace/data" folder, + -- but for a file at a relative location it would also work to use asset.resource, + -- like for the data file above + File = openspace.absPath("${DATA}/test3.jpg"), + }, -- Disable additive blending, so that points will be rendered with their actual color -- and overlapping points will be sorted by depth. This works best when the points -- have an opacity of 1 diff --git a/data/assets/scene/digitaluniverse/2dF.asset b/data/assets/scene/digitaluniverse/2dF.asset index 48441443d8..ff2b40703c 100644 --- a/data/assets/scene/digitaluniverse/2dF.asset +++ b/data/assets/scene/digitaluniverse/2dF.asset @@ -21,7 +21,9 @@ local Object = { Opacity = 1.0, File = speck .. "2dF.speck", Unit = "Mpc", - Texture = textures .. "point3A.png", + Texture = { + File = textures .. "point3A.png", + }, Coloring = { ColorMapping = { File = speck .. "2dF.cmap", diff --git a/data/assets/scene/digitaluniverse/2mass.asset b/data/assets/scene/digitaluniverse/2mass.asset index daf8dfe6d9..2dc58b4cfe 100644 --- a/data/assets/scene/digitaluniverse/2mass.asset +++ b/data/assets/scene/digitaluniverse/2mass.asset @@ -21,7 +21,9 @@ local Object = { Opacity = 1.0, File = speck .. "2MASS.speck", Unit = "Mpc", - Texture = textures .. "point3A.png", + Texture = { + File = textures .. "point3A.png", + }, Coloring = { FixedColor = { 1.0, 0.4, 0.2 }, ColorMapping = { diff --git a/data/assets/scene/digitaluniverse/6dF.asset b/data/assets/scene/digitaluniverse/6dF.asset index f3d5528369..248971a60d 100644 --- a/data/assets/scene/digitaluniverse/6dF.asset +++ b/data/assets/scene/digitaluniverse/6dF.asset @@ -21,7 +21,9 @@ local Object = { Opacity = 1.0, File = speck .. "6dF.speck", Unit = "Mpc", - Texture = textures .. "point3A.png", + Texture = { + File = textures .. "point3A.png", + }, Coloring = { FixedColor = { 1.0, 1.0, 0.0 }, ColorMapping = { diff --git a/data/assets/scene/digitaluniverse/abell.asset b/data/assets/scene/digitaluniverse/abell.asset index 43ef6c6796..793ff0fcb6 100644 --- a/data/assets/scene/digitaluniverse/abell.asset +++ b/data/assets/scene/digitaluniverse/abell.asset @@ -25,6 +25,7 @@ local Object = { Renderable = { Type = "RenderablePointCloud", Enabled = false, + File = speck .. "abell.speck", Labels = { File = speck .. "abell.label", Opacity = 1.0, @@ -39,8 +40,9 @@ local Object = { FixedColor = { 1.0, 0.4, 0.2 }, --ColorMap = speck .. "abell.cmap", -- TODO: Decide whether to add }, - File = speck .. "abell.speck", - Texture = textures .. "point3A.png", + Texture = { + File = textures .. "point3A.png", + }, Unit = "Mpc", TransformationMatrix = TransformMatrix, SizeSettings = { diff --git a/data/assets/scene/digitaluniverse/deepsky.asset b/data/assets/scene/digitaluniverse/deepsky.asset index 5aa89ea88b..c17f0d6c1f 100644 --- a/data/assets/scene/digitaluniverse/deepsky.asset +++ b/data/assets/scene/digitaluniverse/deepsky.asset @@ -18,6 +18,7 @@ local DeepSkyObjects = { Renderable = { Type = "RenderablePointCloud", Enabled = false, + File = speck .. "dso.speck", Labels = { File = speck .. "dso.label", Color = { 0.1, 0.4, 0.6 }, @@ -29,8 +30,9 @@ local DeepSkyObjects = { Coloring = { FixedColor = { 1.0, 1.0, 0.0 } }, - File = speck .. "dso.speck", - Texture = textures .. "point3.png", + Texture = { + File = textures .. "point3.png", + }, Unit = "pc", --FadeInDistances = { 0.05, 1.0 }, -- Fade in value in the same unit as "Unit" SizeSettings = { diff --git a/data/assets/scene/digitaluniverse/dwarfs.asset b/data/assets/scene/digitaluniverse/dwarfs.asset index fd13e0e4ec..b45473517d 100644 --- a/data/assets/scene/digitaluniverse/dwarfs.asset +++ b/data/assets/scene/digitaluniverse/dwarfs.asset @@ -18,6 +18,7 @@ local Object = { Renderable = { Type = "RenderablePointCloud", Enabled = false, + File = speck .. "dwarfs.speck", Labels = { File = speck .. "dwarfs.label", Color = { 0.5, 0.1, 0.2 }, @@ -26,8 +27,9 @@ local Object = { Unit = "pc" }, Opacity = 1.0, - File = speck .. "dwarfs.speck", - Texture = textures .. "point3.png", + Texture = { + File = textures .. "point3.png", + }, Unit = "pc", Coloring = { FixedColor = { 0.4, 0.0, 0.1 }, diff --git a/data/assets/scene/digitaluniverse/exoplanets.asset b/data/assets/scene/digitaluniverse/exoplanets.asset index c53f3ae27d..43e604ed61 100644 --- a/data/assets/scene/digitaluniverse/exoplanets.asset +++ b/data/assets/scene/digitaluniverse/exoplanets.asset @@ -18,6 +18,7 @@ local Object = { Renderable = { Type = "RenderablePointCloud", Enabled = false, + File = speck .. "expl.speck", Labels = { File = speck .. "expl.label", Color = { 0.3, 0.3, 0.8 }, @@ -26,8 +27,9 @@ local Object = { Unit = "pc" }, Opacity = 1.0, - Texture = textures .. "target-blue.png", - File = speck .. "expl.speck", + Texture = { + File = textures .. "target-blue.png", + }, Unit = "pc", SizeSettings = { ScaleExponent = 16.9, diff --git a/data/assets/scene/digitaluniverse/exoplanets_candidates.asset b/data/assets/scene/digitaluniverse/exoplanets_candidates.asset index e0bb934486..6c95b2e68d 100644 --- a/data/assets/scene/digitaluniverse/exoplanets_candidates.asset +++ b/data/assets/scene/digitaluniverse/exoplanets_candidates.asset @@ -21,7 +21,9 @@ local Object = { Opacity = 0.99, File = speck .. "exoplanet_candidates.speck", Unit = "pc", - Texture = textures .. "halo.png", + Texture = { + File = textures .. "halo.png", + }, Coloring = { FixedColor = { 1.0, 1.0, 0.0 } }, diff --git a/data/assets/scene/digitaluniverse/hdf.asset b/data/assets/scene/digitaluniverse/hdf.asset index 964144e69c..9598b46ba6 100644 --- a/data/assets/scene/digitaluniverse/hdf.asset +++ b/data/assets/scene/digitaluniverse/hdf.asset @@ -27,7 +27,9 @@ local Object = { Enabled = false, Opacity = 1.0, File = HUDFSpeck .. "hudf.speck", - Texture = circle .. "circle.png", + Texture = { + File = circle .. "circle.png", + }, Coloring = { ColorMapping = { File = ColorMap .. "hudf.cmap", diff --git a/data/assets/scene/digitaluniverse/obassociations.asset b/data/assets/scene/digitaluniverse/obassociations.asset index fd0610c6f1..3c26a82179 100644 --- a/data/assets/scene/digitaluniverse/obassociations.asset +++ b/data/assets/scene/digitaluniverse/obassociations.asset @@ -36,10 +36,11 @@ local Object = { Opacity = 0.7, File = speck .. "ob.speck", Unit = "pc", - Texture = textures .. "point4.png", PolygonSides = 7, SizeSettings = { - SizeMapping = { "diameter" }, + SizeMapping = { + ParameterOptions = { "diameter" } + }, ScaleExponent = 16.9, MaxSize = 17, EnableMaxSizeControl = true diff --git a/data/assets/scene/digitaluniverse/quasars.asset b/data/assets/scene/digitaluniverse/quasars.asset index 93efec3236..0594ad74c4 100644 --- a/data/assets/scene/digitaluniverse/quasars.asset +++ b/data/assets/scene/digitaluniverse/quasars.asset @@ -27,7 +27,9 @@ local Object = { Enabled = true, Opacity = 0.95, File = speck .. "quasars.speck", - Texture = textures .. "point3A.png", + Texture = { + File = textures .. "point3A.png", + }, Unit = "Mpc", Fading = { FadeInDistances = { 1000.0, 10000.0 } -- Fade in value in the same unit as "Unit" diff --git a/data/assets/scene/digitaluniverse/sdss.asset b/data/assets/scene/digitaluniverse/sdss.asset index 0ffa195d31..cb3103e1d1 100644 --- a/data/assets/scene/digitaluniverse/sdss.asset +++ b/data/assets/scene/digitaluniverse/sdss.asset @@ -30,7 +30,9 @@ local Object = { } } }, - Texture = textures .. "point3A.png", + Texture = { + File = textures .. "point3A.png", + }, Unit = "Mpc", Fading = { FadeInDistances = { 220.0, 650.0 } -- Fade in value in the same unit as "Unit" diff --git a/data/assets/scene/digitaluniverse/superclusters.asset b/data/assets/scene/digitaluniverse/superclusters.asset index 77260619f7..4ec85bc0a0 100644 --- a/data/assets/scene/digitaluniverse/superclusters.asset +++ b/data/assets/scene/digitaluniverse/superclusters.asset @@ -18,6 +18,7 @@ local Object = { Renderable = { Type = "RenderablePointCloud", Enabled = false, + File = speck .. "superclust.speck", Labels = { Enabled = true, File = speck .. "superclust.label", @@ -28,8 +29,9 @@ local Object = { }, DrawElements = false, Opacity = 0.65, - File = speck .. "superclust.speck", - Texture = textures .. "point3A.png", + Texture = { + File = textures .. "point3A.png", + }, Unit = "Mpc", SizeSettings = { ScaleExponent = 23.1, diff --git a/data/assets/scene/digitaluniverse/tully.asset b/data/assets/scene/digitaluniverse/tully.asset index b7739c503d..040c386ae2 100644 --- a/data/assets/scene/digitaluniverse/tully.asset +++ b/data/assets/scene/digitaluniverse/tully.asset @@ -35,7 +35,9 @@ local TullyGalaxies = { }, Opacity = 0.99, File = speck .. "tully.speck", - Texture = textures .. "point3A.png", + Texture = { + File = textures .. "point3A.png" + }, Coloring = { FixedColor = { 1.0, 0.4, 0.2 }, ColorMapping = { diff --git a/include/openspace/data/csvloader.h b/include/openspace/data/csvloader.h index a6f3734e61..911e2fe3db 100644 --- a/include/openspace/data/csvloader.h +++ b/include/openspace/data/csvloader.h @@ -34,6 +34,9 @@ namespace openspace::dataloader::csv { Dataset loadCsvFile(std::filesystem::path path, std::optional specs = std::nullopt); +std::vector loadTextureMapFile(std::filesystem::path path, + const std::set& texturesInData); + } // namespace openspace::dataloader #endif // __OPENSPACE_CORE___CSVLOADER___H__ diff --git a/include/openspace/data/datamapping.h b/include/openspace/data/datamapping.h index 5f2b98beb2..9504a78aab 100644 --- a/include/openspace/data/datamapping.h +++ b/include/openspace/data/datamapping.h @@ -41,10 +41,15 @@ struct DataMapping { bool hasExcludeColumns() const; bool isExcludeColumn(std::string_view column) const; + bool checkIfAllProvidedColumnsExist(const std::vector& columns) const; + std::optional xColumnName; std::optional yColumnName; std::optional zColumnName; std::optional nameColumn; + std::optional textureColumn; + + std::optional textureMap; std::optional missingDataValue; @@ -72,6 +77,8 @@ bool isColumnZ(const std::string& c, const std::optional& mapping); bool isNameColumn(const std::string& c, const std::optional& mapping); +bool isTextureColumn(const std::string& c, const std::optional& mapping); + } // namespace openspace::dataloader #endif // __OPENSPACE_CORE___DATAMAPPING___H__ diff --git a/modules/base/CMakeLists.txt b/modules/base/CMakeLists.txt index e3168b3272..53ad712da3 100644 --- a/modules/base/CMakeLists.txt +++ b/modules/base/CMakeLists.txt @@ -47,6 +47,7 @@ set(HEADER_FILES rendering/pointcloud/renderableinterpolatedpoints.h rendering/pointcloud/renderablepointcloud.h rendering/pointcloud/renderablepolygoncloud.h + rendering/pointcloud/sizemappingcomponent.h rendering/renderablecartesianaxes.h rendering/renderabledisc.h rendering/renderablelabel.h @@ -109,6 +110,7 @@ set(SOURCE_FILES rendering/pointcloud/renderableinterpolatedpoints.cpp rendering/pointcloud/renderablepointcloud.cpp rendering/pointcloud/renderablepolygoncloud.cpp + rendering/pointcloud/sizemappingcomponent.cpp rendering/renderablecartesianaxes.cpp rendering/renderabledisc.cpp rendering/renderablelabel.cpp diff --git a/modules/base/basemodule.cpp b/modules/base/basemodule.cpp index 9dc5c9e9b4..011cce0547 100644 --- a/modules/base/basemodule.cpp +++ b/modules/base/basemodule.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -251,6 +252,8 @@ std::vector BaseModule::documentations() const { RenderableTrailOrbit::Documentation(), RenderableTrailTrajectory::Documentation(), + SizeMappingComponent::Documentation(), + ScreenSpaceDashboard::Documentation(), ScreenSpaceFramebuffer::Documentation(), ScreenSpaceImageLocal::Documentation(), diff --git a/modules/base/rendering/pointcloud/renderableinterpolatedpoints.cpp b/modules/base/rendering/pointcloud/renderableinterpolatedpoints.cpp index 81faf785fe..e11dc65d5a 100644 --- a/modules/base/rendering/pointcloud/renderableinterpolatedpoints.cpp +++ b/modules/base/rendering/pointcloud/renderableinterpolatedpoints.cpp @@ -130,6 +130,10 @@ namespace { // the first set of positions for the objects, the next N rows to the second set of // positions, and so on. The number of objects in the dataset must be specified in the // asset. + // + // MultiTexture: + // Note that if using multiple textures for the points based on values in the dataset, + // the used texture will be decided based on the first N set of points. struct [[codegen::Dictionary(RenderableInterpolatedPoints)]] Parameters { // The number of objects to read from the dataset. Every N:th datapoint will // be interpreted as the same point, but at a different step in the interpolation @@ -328,13 +332,12 @@ void RenderableInterpolatedPoints::deinitializeShaders() { _program = nullptr; } -void RenderableInterpolatedPoints::bindDataForPointRendering() { - RenderablePointCloud::bindDataForPointRendering(); - +void RenderableInterpolatedPoints::setExtraUniforms() { float t0 = computeCurrentLowerValue(); float t = glm::clamp(_interpolation.value - t0, 0.f, 1.f); + _program->setUniform("interpolationValue", t); - _program->setUniform("useSpline", _interpolation.useSpline); + _program->setUniform("useSpline", useSplineInterpolation()); } void RenderableInterpolatedPoints::preUpdate() { @@ -346,103 +349,96 @@ void RenderableInterpolatedPoints::preUpdate() { int RenderableInterpolatedPoints::nAttributesPerPoint() const { int n = RenderablePointCloud::nAttributesPerPoint(); - // Need twice as much information as the regular points - n *= 2; - if (_interpolation.useSpline) { + + // Always at least three extra position values (xyz) + n += 3; + if (useSplineInterpolation()) { // Use two more positions (xyz) n += 2 * 3; } + // And potentially some more color and size data + n += _hasColorMapFile ? 1 : 0; + n += _hasDatavarSize ? 1 : 0; + return n; } -std::vector RenderableInterpolatedPoints::createDataSlice() { - ZoneScoped; +bool RenderableInterpolatedPoints::useSplineInterpolation() const { + return _interpolation.useSpline && _interpolation.nSteps > 1; +} - if (_dataset.entries.empty()) { - return std::vector(); +void RenderableInterpolatedPoints::addPositionDataForPoint(unsigned int index, + std::vector& result, + double& maxRadius) const +{ + using namespace dataloader; + auto [firstIndex, secondIndex] = interpolationIndices(index); + + const Dataset::Entry& e0 = _dataset.entries[firstIndex]; + const Dataset::Entry& e1 = _dataset.entries[secondIndex]; + + glm::dvec3 position0 = transformedPosition(e0); + glm::dvec3 position1 = transformedPosition(e1); + + const double r = glm::max(glm::length(position0), glm::length(position1)); + maxRadius = glm::max(maxRadius, r); + + for (int j = 0; j < 3; ++j) { + result.push_back(static_cast(position0[j])); } - std::vector result; - result.reserve(nAttributesPerPoint() * _nDataPoints); + for (int j = 0; j < 3; ++j) { + result.push_back(static_cast(position1[j])); + } - // Find the information we need for the interpolation and to identify the points, - // and make sure these result in valid indices in all cases - float t0 = computeCurrentLowerValue(); - float t1 = t0 + 1.f; - t1 = glm::clamp(t1, 0.f, _interpolation.value.maxValue()); - unsigned int t0Index = static_cast(t0); - unsigned int t1Index = static_cast(t1); + if (useSplineInterpolation()) { + // Compute the extra positions, before and after the other ones. But make sure + // we do not overflow the allowed bound for the current interpolation step + int beforeIndex = glm::max(static_cast(firstIndex - _nDataPoints), 0); + int maxT = static_cast(_interpolation.value.maxValue() - 1.f); + int maxAllowedindex = maxT * _nDataPoints + index; + int afterIndex = glm::min( + static_cast(secondIndex + _nDataPoints), + maxAllowedindex + ); + + const Dataset::Entry& e00 = _dataset.entries[beforeIndex]; + const Dataset::Entry& e11 = _dataset.entries[afterIndex]; + glm::dvec3 positionBefore = transformedPosition(e00); + glm::dvec3 positionAfter = transformedPosition(e11); + + for (int j = 0; j < 3; ++j) { + result.push_back(static_cast(positionBefore[j])); + } + + for (int j = 0; j < 3; ++j) { + result.push_back(static_cast(positionAfter[j])); + } + } +} + +void RenderableInterpolatedPoints::addColorAndSizeDataForPoint(unsigned int index, + std::vector& result) const +{ + using namespace dataloader; + auto [firstIndex, secondIndex] = interpolationIndices(index); + const Dataset::Entry& e0 = _dataset.entries[firstIndex]; + const Dataset::Entry& e1 = _dataset.entries[secondIndex]; - // What datavar is in use for the index color int colorParamIndex = currentColorParameterIndex(); - - // What datavar is in use for the size scaling (if present) - int sizeParamIndex = currentSizeParameterIndex(); - - double maxRadius = 0.0; - - for (unsigned int i = 0; i < _nDataPoints; i++) { - using namespace dataloader; - const Dataset::Entry& e0 = _dataset.entries[t0Index * _nDataPoints + i]; - const Dataset::Entry& e1 = _dataset.entries[t1Index * _nDataPoints + i]; - glm::dvec3 position0 = transformedPosition(e0); - glm::dvec3 position1 = transformedPosition(e1); - - const double r = glm::max(glm::length(position0), glm::length(position1)); - maxRadius = glm::max(maxRadius, r); - - // Positions - for (int j = 0; j < 3; j++) { - result.push_back(static_cast(position0[j])); - } - - for (int j = 0; j < 3; j++) { - result.push_back(static_cast(position1[j])); - } - - if (_interpolation.useSpline && _interpolation.nSteps > 1) { - // Compute the extra positions, before and after the other ones - unsigned int beforeIndex = static_cast( - glm::max(t0 - 1.f, 0.f) - ); - unsigned int afterIndex = static_cast( - glm::min(t1 + 1.f, _interpolation.value.maxValue() - 1.f) - ); - - const Dataset::Entry& e00 = _dataset.entries[beforeIndex * _nDataPoints + i]; - const Dataset::Entry& e11 = _dataset.entries[afterIndex * _nDataPoints + i]; - glm::dvec3 positionBefore = transformedPosition(e00); - glm::dvec3 positionAfter = transformedPosition(e11); - - for (int j = 0; j < 3; j++) { - result.push_back(static_cast(positionBefore[j])); - } - - for (int j = 0; j < 3; j++) { - result.push_back(static_cast(positionAfter[j])); - } - } - - // Colors - if (_hasColorMapFile) { - result.push_back(e0.data[colorParamIndex]); - result.push_back(e1.data[colorParamIndex]); - } - - // Size data - if (_hasDatavarSize) { - // @TODO: Consider more detailed control over the scaling. Currently the value - // is multiplied with the value as is. Should have similar mapping properties - // as the color mapping - result.push_back(e0.data[sizeParamIndex]); - result.push_back(e1.data[sizeParamIndex]); - } - - // @TODO: Also need to update label positions, if we have created labels from the dataset - // And make sure these are created from only the first set of points.. + if (_hasColorMapFile && colorParamIndex >= 0) { + result.push_back(e0.data[colorParamIndex]); + result.push_back(e1.data[colorParamIndex]); + } + + int sizeParamIndex = currentSizeParameterIndex(); + if (_hasDatavarSize && sizeParamIndex >= 0) { + // @TODO: Consider more detailed control over the scaling. Currently the value + // is multiplied with the value as is. Should have similar mapping properties + // as the color mapping + result.push_back(e0.data[sizeParamIndex]); + result.push_back(e1.data[sizeParamIndex]); } - setBoundingSphere(maxRadius); - return result; } void RenderableInterpolatedPoints::initializeBufferData() { @@ -463,40 +459,28 @@ void RenderableInterpolatedPoints::initializeBufferData() { glBindBuffer(GL_ARRAY_BUFFER, _vbo); glBufferData(GL_ARRAY_BUFFER, bufferSize, nullptr, GL_DYNAMIC_DRAW); - int attributeOffset = 0; + int offset = 0; - auto addFloatAttribute = [&](const std::string& name, GLint nValues) { - GLint attrib = _program->attributeLocation(name); - glEnableVertexAttribArray(attrib); - glVertexAttribPointer( - attrib, - nValues, - GL_FLOAT, - GL_FALSE, - attibutesPerPoint * sizeof(float), - (attributeOffset > 0) ? - reinterpret_cast(attributeOffset * sizeof(float)) : - nullptr - ); - attributeOffset += nValues; - }; + offset = bufferVertexAttribute("in_position0", 3, attibutesPerPoint, offset); + offset = bufferVertexAttribute("in_position1", 3, attibutesPerPoint, offset); - addFloatAttribute("in_position0", 3); - addFloatAttribute("in_position1", 3); - - if (_interpolation.useSpline) { - addFloatAttribute("in_position_before", 3); - addFloatAttribute("in_position_after", 3); + if (useSplineInterpolation()) { + offset = bufferVertexAttribute("in_position_before", 3, attibutesPerPoint, offset); + offset = bufferVertexAttribute("in_position_after", 3, attibutesPerPoint, offset); } if (_hasColorMapFile) { - addFloatAttribute("in_colorParameter0", 1); - addFloatAttribute("in_colorParameter1", 1); + offset = bufferVertexAttribute("in_colorParameter0", 1, attibutesPerPoint, offset); + offset = bufferVertexAttribute("in_colorParameter1", 1, attibutesPerPoint, offset); } if (_hasDatavarSize) { - addFloatAttribute("in_scalingParameter0", 1); - addFloatAttribute("in_scalingParameter1", 1); + offset = bufferVertexAttribute("in_scalingParameter0", 1, attibutesPerPoint, offset); + offset = bufferVertexAttribute("in_scalingParameter1", 1, attibutesPerPoint, offset); + } + + if (_hasSpriteTexture) { + offset = bufferVertexAttribute("in_textureLayer", 1, attibutesPerPoint, offset); } glBindVertexArray(0); @@ -541,4 +525,25 @@ float RenderableInterpolatedPoints::computeCurrentLowerValue() const { return t0; } +float RenderableInterpolatedPoints::computeCurrentUpperValue() const { + float t0 = computeCurrentLowerValue(); + float t1 = t0 + 1.f; + t1 = glm::clamp(t1, 0.f, _interpolation.value.maxValue()); + return t1; +} + +std::pair +RenderableInterpolatedPoints::interpolationIndices(unsigned int index) const +{ + float t0 = computeCurrentLowerValue(); + float t1 = computeCurrentUpperValue(); + unsigned int t0Index = static_cast(t0); + unsigned int t1Index = static_cast(t1); + + size_t lower = size_t(t0Index * _nDataPoints + index); + size_t upper = size_t(t1Index * _nDataPoints + index); + + return { lower, upper }; +} + } // namespace openspace diff --git a/modules/base/rendering/pointcloud/renderableinterpolatedpoints.h b/modules/base/rendering/pointcloud/renderableinterpolatedpoints.h index 27b238fa74..8ead53ad92 100644 --- a/modules/base/rendering/pointcloud/renderableinterpolatedpoints.h +++ b/modules/base/rendering/pointcloud/renderableinterpolatedpoints.h @@ -52,20 +52,34 @@ public: protected: void initializeShadersAndGlExtras() override; void deinitializeShaders() override; - void bindDataForPointRendering() override; + void setExtraUniforms() override; void preUpdate() override; int nAttributesPerPoint() const override; + bool useSplineInterpolation() const; + /** - * Create the data slice to use for rendering the points. Compared to the regular - * point cloud, the data slice for an interpolated set of points will have to be - * recreated when the interpolation value changes, and will only include a subset of - * the points in the entire dataset + * Create the rendering data for the positions for the point with the given index + * and append that to the result. Compared to the base class, this class may require + * 2-4 positions, depending on if * spline interpolation is used or not. * - * \return The dataslice to use for rendering the points + * The values are computed based on the current interpolation value. + * + * Also, compute the maxRadius to use for setting the bounding sphere. */ - std::vector createDataSlice() override; + void addPositionDataForPoint(unsigned int index, std::vector& result, + double& maxRadius) const override; + + /** + * Create the rendering data for the color and size data for the point with the given + * index and append that to the result. Compared to the base class, this class require + * 2 values per data value, to use for interpolation. + * + * The values are computed based on the current interpolation value. + */ + void addColorAndSizeDataForPoint(unsigned int index, + std::vector& result) const override; void initializeBufferData(); void updateBufferData() override; @@ -73,6 +87,8 @@ protected: private: bool isAtKnot() const; float computeCurrentLowerValue() const; + float computeCurrentUpperValue() const; + std::pair interpolationIndices(unsigned int index) const; struct Interpolation : public properties::PropertyOwner { Interpolation(); diff --git a/modules/base/rendering/pointcloud/renderablepointcloud.cpp b/modules/base/rendering/pointcloud/renderablepointcloud.cpp index 65c99497e7..3d03a94a56 100644 --- a/modules/base/rendering/pointcloud/renderablepointcloud.cpp +++ b/modules/base/rendering/pointcloud/renderablepointcloud.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -55,14 +56,15 @@ namespace { constexpr std::string_view _loggerCat = "RenderablePointCloud"; - constexpr std::array UniformNames = { + constexpr std::array UniformNames = { "cameraViewMatrix", "projectionMatrix", "modelMatrix", "cameraPosition", "cameraLookUp", "renderOption", "maxAngularSize", "color", "opacity", "scaleExponent", "scaleFactor", "up", "right", "fadeInValue", "hasSpriteTexture", "spriteTexture", "useColorMap", "colorMapTexture", "cmapRangeMin", "cmapRangeMax", "nanColor", "useNanColor", "hideOutsideRange", "enableMaxSizeControl", "aboveRangeColor", "useAboveRangeColor", "belowRangeColor", "useBelowRangeColor", - "hasDvarScaling", "enableOutline", "outlineColor", "outlineWeight" + "hasDvarScaling", "dvarScaleFactor", "enableOutline", "outlineColor", + "outlineWeight", "aspectRatioScale" }; enum RenderOption { @@ -70,18 +72,55 @@ namespace { PositionNormal }; - constexpr openspace::properties::Property::PropertyInfo SpriteTextureInfo = { - "Texture", - "Point Sprite Texture", - "The path to the texture that should be used as the point sprite", + constexpr openspace::properties::Property::PropertyInfo TextureEnabledInfo = { + "Enabled", + "Enabled", + "If true, use a provided sprite texture to render the point. If false, draw " + "the points using the default point shape.", openspace::properties::Property::Visibility::AdvancedUser }; - constexpr openspace::properties::Property::PropertyInfo UseSpriteTextureInfo = { - "UseTexture", - "Use Texture", - "If true, use the provided sprite texture to render the point. If false, draw " - "the points using the default point shape.", + constexpr openspace::properties::Property::PropertyInfo AllowTextureCompressionInfo = + { + "AllowCompression", + "Allow Compression", + "If true, the textures will be compressed to preserve graphics card memory. This " + "is enabled per default, but may lead to visible artefacts for certain images, " + "especially up close. Set this to false to disable any hardware compression of " + "the textures, and represent each color channel with 8 bits.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo UseAlphaInfo = { + "UseAlphaChannel", + "Use Alpha Channel", + "If true, include transparency information in the loaded textures, if there " + "is any. If false, all loaded textures will be converted to RGB format. \n" + "This setting can be used if you have textures with transparency, but do not " + "need the transparency information. This may be the case when using additive " + "blending, for example. Converting the files to RGB on load may then reduce the " + "memory footprint and/or lead to some optimization in terms of rendering speed.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo SpriteTextureInfo = { + "File", + "Point Sprite Texture File", + "The path to the texture that should be used as the point sprite. Note that if " + "multiple textures option is set in the asset, by providing a texture folder, " + "this value will be ignored.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo TextureModeInfo = { + "TextureMode", + "Texture Mode", + "This tells which texture mode is being used for this renderable. There are " + "three different texture modes: 1) One single sprite texture used for all " + "points, 2) Multiple textures, that are mapped to the points based on a column " + "in the dataset, and 3) Other, which is used for specific subtypes where the " + "texture is internally controlled by the renderable and can't be set from a " + "file (such as the RenderablePolygonCloud).", openspace::properties::Property::Visibility::AdvancedUser }; @@ -282,7 +321,7 @@ namespace { struct [[codegen::Dictionary(RenderablePointCloud)]] Parameters { // The path to the data file that contains information about the point to be // rendered. Can be either a CSV or SPECK file - std::optional file; + std::optional file; // If true (default), the loaded dataset will be cached so that it can be loaded // faster at a later time. This does however mean that any updates to the values @@ -296,11 +335,29 @@ namespace { std::optional dataMapping [[codegen::reference("dataloader_datamapping")]]; - // [[codegen::verbatim(SpriteTextureInfo.description)]] - std::optional texture; + struct Texture { + // [[codegen::verbatim(TextureEnabledInfo.description)]] + std::optional enabled; - // [[codegen::verbatim(UseSpriteTextureInfo.description)]] - std::optional useTexture; + // [[codegen::verbatim(SpriteTextureInfo.description)]] + std::optional file; + + // The folder where the textures are located when using multiple different + // textures to render the points. Setting this value means that multiple + // textures shall be used and any single sprite texture file is ignored. + // + // Note that the textures can be any format, but rendering efficiency will + // be best if using textures with the exact same resolution. + std::optional folder [[codegen::directory()]]; + + // [[codegen::verbatim(AllowTextureCompressionInfo.description)]] + std::optional allowCompression; + + // [[codegen::verbatim(UseAlphaInfo.description)]] + std::optional useAlphaChannel; + }; + // Settings related to the texturing of the points + std::optional texture; // [[codegen::verbatim(DrawElementsInfo.description)]] std::optional drawElements; @@ -333,9 +390,9 @@ namespace { [[codegen::reference("labelscomponent")]]; struct SizeSettings { - // A list specifying all parameters that may be used for size mapping, i.e. - // scaling the points based on the provided data columns - std::optional> sizeMapping; + // Settings related to scaling the points based on data + std::optional sizeMapping + [[codegen::reference("base_sizemappingcomponent")]]; // [[codegen::verbatim(ScaleExponentInfo.description)]] std::optional scaleExponent; @@ -420,14 +477,10 @@ RenderablePointCloud::SizeSettings::SizeSettings(const ghoul::Dictionary& dictio maxAngularSize = settings.maxSize.value_or(maxAngularSize); if (settings.sizeMapping.has_value()) { - std::vector opts = *settings.sizeMapping; - for (size_t i = 0; i < opts.size(); i++) { - // Note that options are added in order - sizeMapping.parameterOption.addOption(static_cast(i), opts[i]); - } - sizeMapping.enabled = true; - - addPropertySubOwner(sizeMapping); + sizeMapping = std::make_unique( + *settings.sizeMapping + ); + addPropertySubOwner(sizeMapping.get()); } } @@ -437,18 +490,6 @@ RenderablePointCloud::SizeSettings::SizeSettings(const ghoul::Dictionary& dictio addProperty(maxAngularSize); } -RenderablePointCloud::SizeSettings::SizeMapping::SizeMapping() - : properties::PropertyOwner({ "SizeMapping", "Size Mapping", "" }) - , enabled(SizeMappingEnabledInfo, false) - , parameterOption( - SizeMappingOptionInfo, - properties::OptionProperty::DisplayType::Dropdown - ) -{ - addProperty(enabled); - addProperty(parameterOption); -} - RenderablePointCloud::ColorSettings::ColorSettings(const ghoul::Dictionary& dictionary) : properties::PropertyOwner({ "Coloring", "Coloring", "" }) , pointColor(PointColorInfo, glm::vec3(1.f), glm::vec3(0.f), glm::vec3(1.f)) @@ -485,6 +526,23 @@ RenderablePointCloud::ColorSettings::ColorSettings(const ghoul::Dictionary& dict addProperty(outlineWeight); } +RenderablePointCloud::Texture::Texture() + : properties::PropertyOwner({ "Texture", "Texture", "" }) + , enabled(TextureEnabledInfo, true) + , allowCompression(AllowTextureCompressionInfo, true) + , useAlphaChannel(UseAlphaInfo, true) + , spriteTexturePath(SpriteTextureInfo) + , inputMode(TextureModeInfo) +{ + addProperty(enabled); + addProperty(allowCompression); + addProperty(useAlphaChannel); + addProperty(spriteTexturePath); + + inputMode.setReadOnly(true); + addProperty(inputMode); +} + RenderablePointCloud::Fading::Fading(const ghoul::Dictionary& dictionary) : properties::PropertyOwner({ "Fading", "Fading", "" }) , enabled(EnableDistanceFadeInfo, false) @@ -520,8 +578,6 @@ RenderablePointCloud::Fading::Fading(const ghoul::Dictionary& dictionary) RenderablePointCloud::RenderablePointCloud(const ghoul::Dictionary& dictionary) : Renderable(dictionary) - , _spriteTexturePath(SpriteTextureInfo) - , _useSpriteTexture(UseSpriteTextureInfo, true) , _drawElements(DrawElementsInfo, true) , _useAdditiveBlending(UseAdditiveBlendingInfo, true) , _renderOption(RenderOptionInfo, properties::OptionProperty::DisplayType::Dropdown) @@ -567,22 +623,46 @@ RenderablePointCloud::RenderablePointCloud(const ghoul::Dictionary& dictionary) _unit = DistanceUnit::Meter; } - _spriteTexturePath.onChange([this]() { _spriteTextureIsDirty = true; }); - addProperty(_spriteTexturePath); - - _useSpriteTexture = p.useTexture.value_or(_useSpriteTexture); - addProperty(_useSpriteTexture); + addPropertySubOwner(_texture); if (p.texture.has_value()) { - _spriteTexturePath = absPath(*p.texture).string(); + const Parameters::Texture t = *p.texture; + + // Read texture information. Multi-texture is prioritized over single-texture + if (t.folder.has_value()) { + _textureMode = TextureInputMode::Multi; + _hasSpriteTexture = true; + _texturesDirectory = absPath(*t.folder).string(); + + if (t.file.has_value()) { + LWARNING(fmt::format( + "Both a single texture File and multi-texture Folder was provided. " + "The folder '{}' has priority and the single texture with the " + "following path will be ignored: '{}'", *t.folder, *t.file + )); + } + + _texture.removeProperty(_texture.spriteTexturePath); + } + else if (t.file.has_value()) { + _textureMode = TextureInputMode::Single; + _hasSpriteTexture = true; + _texture.spriteTexturePath = absPath(*t.file).string(); + _texture.spriteTexturePath.onChange([this]() { _spriteTextureIsDirty = true; }); + } + + _texture.enabled = t.enabled.value_or(_texture.enabled); + _texture.allowCompression = t.allowCompression.value_or(_texture.allowCompression); + _texture.useAlphaChannel = t.useAlphaChannel.value_or(_texture.useAlphaChannel); } - _hasSpriteTexture = p.texture.has_value(); + _texture.allowCompression.onChange([this]() { _spriteTextureIsDirty = true; }); + _texture.useAlphaChannel.onChange([this]() { _spriteTextureIsDirty = true; }); _transformationMatrix = p.transformationMatrix.value_or(_transformationMatrix); if (p.sizeSettings.has_value() && p.sizeSettings->sizeMapping.has_value()) { - _sizeSettings.sizeMapping.parameterOption.onChange( + _sizeSettings.sizeMapping->parameterOption.onChange( [this]() { _dataIsDirty = true; } ); _hasDatavarSize = true; @@ -642,7 +722,7 @@ RenderablePointCloud::RenderablePointCloud(const ghoul::Dictionary& dictionary) if (p.labels.has_value()) { if (!p.labels->hasKey("File") && _hasDataFile) { - // Load the labelset from the dataset if no file was included + // Load the labelset from the dataset if no label file was included _labels = std::make_unique(*p.labels, _dataset, _unit); } else { @@ -651,7 +731,7 @@ RenderablePointCloud::RenderablePointCloud(const ghoul::Dictionary& dictionary) _hasLabels = true; addPropertySubOwner(_labels.get()); - // Fading of the labels should also depend on the fading of the renderable + // Fading of the labels should depend on the fading of the renderable _labels->setParentFadeable(this); } @@ -661,8 +741,6 @@ RenderablePointCloud::RenderablePointCloud(const ghoul::Dictionary& dictionary) bool RenderablePointCloud::isReady() const { bool isReady = _program; - - // If we have labels, they also need to be loaded if (_hasLabels) { isReady = isReady && _labels->isReady(); } @@ -672,6 +750,20 @@ bool RenderablePointCloud::isReady() const { void RenderablePointCloud::initialize() { ZoneScoped; + switch (_textureMode) { + case TextureInputMode::Single: + _texture.inputMode = "Single Sprite Texture"; + break; + case TextureInputMode::Multi: + _texture.inputMode = "Multipe Textures / Data-based"; + break; + case TextureInputMode::Other: + _texture.inputMode = "Other"; + break; + default: + break; + } + if (_hasDataFile && _hasColorMapFile) { _colorSettings.colorMapping->initialize(_dataset); } @@ -687,6 +779,22 @@ void RenderablePointCloud::initializeGL() { initializeShadersAndGlExtras(); ghoul::opengl::updateUniformLocations(*_program, _uniformCache, UniformNames); + + if (_hasSpriteTexture) { + switch (_textureMode) { + case TextureInputMode::Single: + initializeSingleTexture(); + break; + case TextureInputMode::Multi: + initializeMultiTextures(); + break; + case TextureInputMode::Other: + initializeCustomTexture(); + break; + default: + break; + } + } } void RenderablePointCloud::deinitializeGL() { @@ -697,8 +805,7 @@ void RenderablePointCloud::deinitializeGL() { deinitializeShaders(); - BaseModule::TextureManager.release(_spriteTexture); - _spriteTexture = nullptr; + clearTextureDataStructures(); } void RenderablePointCloud::initializeShadersAndGlExtras() { @@ -725,9 +832,210 @@ void RenderablePointCloud::deinitializeShaders() { _program = nullptr; } -void RenderablePointCloud::bindTextureForRendering() const { - if (_spriteTexture) { - _spriteTexture->bind(); +void RenderablePointCloud::initializeCustomTexture() {} + +void RenderablePointCloud::initializeSingleTexture() { + if (_texture.spriteTexturePath.value().empty()) { + return; + } + + std::filesystem::path p = absPath(_texture.spriteTexturePath); + + if (!std::filesystem::is_regular_file(p)) { + throw ghoul::RuntimeError(fmt::format( + "Could not find image file '{}'", p + )); + } + + loadTexture(p, 0); + generateArrayTextures(); +} + +void RenderablePointCloud::initializeMultiTextures() { + for (const dataloader::Dataset::Texture& tex : _dataset.textures) { + std::filesystem::path path = _texturesDirectory / tex.file; + + if (!std::filesystem::is_regular_file(path)) { + throw ghoul::RuntimeError(fmt::format( + "Could not find image file '{}'", path + )); + } + loadTexture(path, tex.index); + } + + generateArrayTextures(); +} + +void RenderablePointCloud::clearTextureDataStructures() { + _textures.clear(); + _textureNameToIndex.clear(); + _indexInDataToTextureIndex.clear(); + _textureMapByFormat.clear(); + // Unload texture arrays from GPU memory + for (const TextureArrayInfo& arrayInfo : _textureArrays) { + glDeleteTextures(1, &arrayInfo.renderId); + } + _textureArrays.clear(); + _textureIndexToArrayMap.clear(); +} + +void RenderablePointCloud::loadTexture(const std::filesystem::path& path, int index) { + if (path.empty()) { + return; + } + + std::string filename = path.filename().string(); + auto search = _textureNameToIndex.find(filename); + if (search != _textureNameToIndex.end()) { + // The texture has already been loaded. Find the index + size_t indexInTextureArray = _textureNameToIndex[filename]; + _indexInDataToTextureIndex[index] = indexInTextureArray; + return; + } + + std::unique_ptr t = + ghoul::io::TextureReader::ref().loadTexture(path.string(), 2); + + bool useAlpha = (t->numberOfChannels() > 3) && _texture.useAlphaChannel; + + if (t) { + LINFOC("RenderablePlanesCloud", fmt::format("Loaded texture {}", path)); + // Do not upload the loaded texture to the GPU, we just want it to hold the data. + // However, convert textures make sure they all use the same format + ghoul::opengl::Texture::Format targetFormat = glFormat(useAlpha); + convertTextureFormat(*t, targetFormat); + } + else { + throw ghoul::RuntimeError(fmt::format( + "Could not find image file {}", path + )); + } + + TextureFormat format = { + .resolution = glm::uvec2(t->width(), t->height()), + .useAlpha = useAlpha + }; + + size_t indexInTextureArray = _textures.size(); + _textures.push_back(std::move(t)); + _textureNameToIndex[filename] = indexInTextureArray; + _textureMapByFormat[format].push_back(indexInTextureArray); + _indexInDataToTextureIndex[index] = indexInTextureArray; +} + +void RenderablePointCloud::initAndAllocateTextureArray(unsigned int textureId, + glm::uvec2 resolution, + size_t nLayers, + bool useAlpha) +{ + float w = static_cast(resolution.x); + float h = static_cast(resolution.y); + glm::vec2 aspectScale = w > h ? glm::vec2(1.f, h / w) : glm::vec2(w / h, 1.f); + + _textureArrays.push_back({ + .renderId = textureId, + .aspectRatioScale = aspectScale + }); + + gl::GLenum internalFormat = internalGlFormat(useAlpha); + gl::GLenum format = gl::GLenum(glFormat(useAlpha)); + + // Create storage for the texture + // The nicer way would be to use glTexStorage3D, but that is only available in OpenGl + // 4.2 and above + glTexImage3D( + GL_TEXTURE_2D_ARRAY, + 0, + internalFormat, + resolution.x, + resolution.y, + static_cast(nLayers), + 0, + format, + GL_UNSIGNED_BYTE, + nullptr + ); + + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); +} + +void RenderablePointCloud::fillAndUploadTextureLayer(unsigned int arrayIndex, + unsigned int layer, + size_t textureIndex, + glm::uvec2 resolution, + bool useAlpha, + const void* pixelData) +{ + gl::GLenum format = gl::GLenum(glFormat(useAlpha)); + + glTexSubImage3D( + GL_TEXTURE_2D_ARRAY, + 0, // Mipmap number + 0, // xoffset + 0, // yoffset + gl::GLint(layer), // zoffset + gl::GLsizei(resolution.x), // width + gl::GLsizei(resolution.y), // height + 1, // depth + format, + GL_UNSIGNED_BYTE, // type + pixelData + ); + + // Keep track of which layer in which texture array corresponds to the texture with + // this index, so we can use it when generating vertex data + _textureIndexToArrayMap[textureIndex] = { + .arrayId = arrayIndex, + .layer = layer + }; +} + +void RenderablePointCloud::generateArrayTextures() { + using Entry = std::pair>; + unsigned int arrayIndex = 0; + for (const Entry& e : _textureMapByFormat) { + glm::uvec2 res = e.first.resolution; + bool useAlpha = e.first.useAlpha; + std::vector textureListIndices = e.second; + size_t nLayers = textureListIndices.size(); + + // Generate an array texture storage + unsigned int id = 0; + glGenTextures(1, &id); + glBindTexture(GL_TEXTURE_2D_ARRAY, id); + + initAndAllocateTextureArray(id, res, nLayers, useAlpha); + + // Fill that storage with the data from the individual textures + unsigned int layer = 0; + for (const size_t& i : textureListIndices) { + ghoul::opengl::Texture* texture = _textures[i].get(); + fillAndUploadTextureLayer(arrayIndex, layer, i, res, useAlpha, texture->pixelData()); + layer++; + + // At this point we don't need the keep the texture data around anymore. If + // the textures need updating, we will reload them from file + texture->purgeFromRAM(); + } + + int nMaxTextureLayers = 0; + glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &nMaxTextureLayers); + if (static_cast(layer) > nMaxTextureLayers) { + LERROR(fmt::format( + "Too many layers bound in the same texture array. Found {} textures with " + "resolution {}x{} pixels. Max supported is {}.", + layer, res.x, res.y, nMaxTextureLayers + )); + // @TODO: Should we split the array up? Do we think this will ever become + // a problem? + } + + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + + arrayIndex++; } } @@ -759,7 +1067,54 @@ float RenderablePointCloud::computeDistanceFadeValue(const RenderData& data) con return fadeValue * funcValue; } -void RenderablePointCloud::bindDataForPointRendering() { +void RenderablePointCloud::setExtraUniforms() {} + +void RenderablePointCloud::renderBillboards(const RenderData& data, + const glm::dmat4& modelMatrix, + const glm::dvec3& orthoRight, + const glm::dvec3& orthoUp, + float fadeInVariable) +{ + if (!_hasDataFile || _dataset.entries.empty()) { + return; + } + + glEnablei(GL_BLEND, 0); + + if (_useAdditiveBlending) { + glDepthMask(false); + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + } + else { + // Normal blending, with transparency + glDepthMask(true); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } + + _program->activate(); + + _program->setUniform(_uniformCache.cameraPos, data.camera.positionVec3()); + _program->setUniform( + _uniformCache.cameraLookup, + glm::vec3(data.camera.lookUpVectorWorldSpace()) + ); + _program->setUniform(_uniformCache.renderOption, _renderOption.value()); + _program->setUniform(_uniformCache.modelMatrix, modelMatrix); + + _program->setUniform( + _uniformCache.cameraViewMatrix, + data.camera.combinedViewMatrix() + ); + + _program->setUniform( + _uniformCache.projectionMatrix, + glm::dmat4(data.camera.projectionMatrix()) + ); + + _program->setUniform(_uniformCache.up, glm::vec3(orthoUp)); + _program->setUniform(_uniformCache.right, glm::vec3(orthoRight)); + _program->setUniform(_uniformCache.fadeInValue, fadeInVariable); + _program->setUniform(_uniformCache.renderOption, _renderOption.value()); _program->setUniform(_uniformCache.opacity, opacity()); @@ -770,16 +1125,17 @@ void RenderablePointCloud::bindDataForPointRendering() { _sizeSettings.useMaxSizeControl ); _program->setUniform(_uniformCache.maxAngularSize, _sizeSettings.maxAngularSize); - _program->setUniform(_uniformCache.hasDvarScaling, _sizeSettings.sizeMapping.enabled); - bool useTexture = _hasSpriteTexture && _useSpriteTexture; - _program->setUniform(_uniformCache.hasSpriteTexture, useTexture); + bool useSizeMapping = _hasDatavarSize && _sizeSettings.sizeMapping && + _sizeSettings.sizeMapping->enabled; - ghoul::opengl::TextureUnit spriteTextureUnit; - _program->setUniform(_uniformCache.spriteTexture, spriteTextureUnit); - if (useTexture) { - spriteTextureUnit.activate(); - bindTextureForRendering(); + _program->setUniform(_uniformCache.hasDvarScaling, useSizeMapping); + + if (useSizeMapping) { + _program->setUniform( + _uniformCache.dvarScaleFactor, + _sizeSettings.sizeMapping->scaleFactor + ); } _program->setUniform(_uniformCache.color, _colorSettings.pointColor); @@ -834,58 +1190,38 @@ void RenderablePointCloud::bindDataForPointRendering() { _colorSettings.colorMapping->useBelowRangeColor ); } -} -void RenderablePointCloud::renderBillboards(const RenderData& data, - const glm::dmat4& modelMatrix, - const glm::dvec3& orthoRight, - const glm::dvec3& orthoUp, - float fadeInVariable) -{ - if (!_hasDataFile || _dataset.entries.empty()) { - return; - } + bool useTexture = _hasSpriteTexture && _texture.enabled; + _program->setUniform(_uniformCache.hasSpriteTexture, useTexture); - glEnablei(GL_BLEND, 0); + ghoul::opengl::TextureUnit spriteTextureUnit; + _program->setUniform(_uniformCache.spriteTexture, spriteTextureUnit); - if (_useAdditiveBlending) { - glDepthMask(false); - glBlendFunc(GL_SRC_ALPHA, GL_ONE); - } - else { - // Normal blending, with transparency - glDepthMask(true); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - } - - _program->activate(); - - _program->setUniform(_uniformCache.cameraPos, data.camera.positionVec3()); - _program->setUniform( - _uniformCache.cameraLookup, - glm::vec3(data.camera.lookUpVectorWorldSpace()) - ); - _program->setUniform(_uniformCache.renderOption, _renderOption.value()); - _program->setUniform(_uniformCache.modelMatrix, modelMatrix); - - _program->setUniform( - _uniformCache.cameraViewMatrix, - data.camera.combinedViewMatrix() - ); - - _program->setUniform( - _uniformCache.projectionMatrix, - glm::dmat4(data.camera.projectionMatrix()) - ); - - _program->setUniform(_uniformCache.up, glm::vec3(orthoUp)); - _program->setUniform(_uniformCache.right, glm::vec3(orthoRight)); - _program->setUniform(_uniformCache.fadeInValue, fadeInVariable); - - bindDataForPointRendering(); + setExtraUniforms(); glBindVertexArray(_vao); - glDrawArrays(GL_POINTS, 0, static_cast(_nDataPoints)); + + if (useTexture && !_textureArrays.empty()) { + spriteTextureUnit.activate(); + for (const TextureArrayInfo& arrayInfo : _textureArrays) { + _program->setUniform( + _uniformCache.aspectRatioScale, + arrayInfo.aspectRatioScale + ); + glBindTexture(GL_TEXTURE_2D_ARRAY, arrayInfo.renderId); + glDrawArrays( + GL_POINTS, + arrayInfo.startOffset, + static_cast(arrayInfo.nPoints) + ); + } + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + } + else { + _program->setUniform(_uniformCache.aspectRatioScale, glm::vec2(1.f)); + glDrawArrays(GL_POINTS, 0, static_cast(_nDataPoints)); + } + glBindVertexArray(0); _program->deactivate(); @@ -940,13 +1276,13 @@ void RenderablePointCloud::update(const UpdateData&) { _colorSettings.colorMapping->update(_dataset); } - if (_dataIsDirty) { - updateBufferData(); - } - if (_spriteTextureIsDirty) { updateSpriteTexture(); } + + if (_dataIsDirty) { + updateBufferData(); + } } glm::dvec3 RenderablePointCloud::transformedPosition( @@ -961,9 +1297,27 @@ int RenderablePointCloud::nAttributesPerPoint() const { int n = 3; // position n += _hasColorMapFile ? 1 : 0; n += _hasDatavarSize ? 1 : 0; + n += _hasSpriteTexture ? 1 : 0; // texture id return n; } +int RenderablePointCloud::bufferVertexAttribute(const std::string& name, GLint nValues, + int nAttributesPerPoint, int offset) const +{ + GLint attrib = _program->attributeLocation(name); + glEnableVertexAttribArray(attrib); + glVertexAttribPointer( + attrib, + nValues, + GL_FLOAT, + GL_FALSE, + nAttributesPerPoint * sizeof(float), + reinterpret_cast(offset * sizeof(float)) + ); + + return offset + nValues; +} + void RenderablePointCloud::updateBufferData() { if (!_hasDataFile || _dataset.entries.empty()) { return; @@ -991,45 +1345,20 @@ void RenderablePointCloud::updateBufferData() { glBufferData(GL_ARRAY_BUFFER, size * sizeof(float), slice.data(), GL_STATIC_DRAW); const int attibutesPerPoint = nAttributesPerPoint(); - int attributeOffset = 0; + int offset = 0; - GLint positionAttrib = _program->attributeLocation("in_position"); - glEnableVertexAttribArray(positionAttrib); - glVertexAttribPointer( - positionAttrib, - 3, - GL_FLOAT, - GL_FALSE, - attibutesPerPoint * sizeof(float), - nullptr - ); - attributeOffset += 3; + offset = bufferVertexAttribute("in_position", 3, attibutesPerPoint, offset); if (_hasColorMapFile) { - GLint colorParamAttrib = _program->attributeLocation("in_colorParameter"); - glEnableVertexAttribArray(colorParamAttrib); - glVertexAttribPointer( - colorParamAttrib, - 1, - GL_FLOAT, - GL_FALSE, - attibutesPerPoint * sizeof(float), - reinterpret_cast(attributeOffset * sizeof(float)) - ); - attributeOffset += 1; + offset = bufferVertexAttribute("in_colorParameter", 1, attibutesPerPoint, offset); } if (_hasDatavarSize) { - GLint scalingAttrib = _program->attributeLocation("in_scalingParameter"); - glEnableVertexAttribArray(scalingAttrib); - glVertexAttribPointer( - scalingAttrib, - 1, - GL_FLOAT, - GL_FALSE, - attibutesPerPoint * sizeof(float), - reinterpret_cast(attributeOffset * sizeof(float)) - ); + offset = bufferVertexAttribute("in_scalingParameter", 1, attibutesPerPoint, offset); + } + + if (_hasSpriteTexture) { + offset = bufferVertexAttribute("in_textureLayer", 1, attibutesPerPoint, offset); } glBindVertexArray(0); @@ -1038,8 +1367,7 @@ void RenderablePointCloud::updateBufferData() { } void RenderablePointCloud::updateSpriteTexture() { - bool shouldUpdate = _hasSpriteTexture && _spriteTextureIsDirty && - !_spriteTexturePath.value().empty(); + bool shouldUpdate = _hasSpriteTexture && _spriteTextureIsDirty; if (!shouldUpdate) { return; @@ -1048,26 +1376,34 @@ void RenderablePointCloud::updateSpriteTexture() { ZoneScopedN("Sprite texture"); TracyGpuZone("Sprite texture"); - ghoul::opengl::Texture* texture = _spriteTexture; + clearTextureDataStructures(); - unsigned int hash = ghoul::hashCRC32File(_spriteTexturePath); + // We also have to update the dataset, to update the texture array offsets + _dataIsDirty = true; - _spriteTexture = BaseModule::TextureManager.request( - std::to_string(hash), - [path = _spriteTexturePath]() -> std::unique_ptr { - std::filesystem::path p = absPath(path); - LINFO(fmt::format("Loaded texture from {}", p)); - std::unique_ptr t = - ghoul::io::TextureReader::ref().loadTexture(p.string(), 2); - t->uploadTexture(); - t->setFilter(ghoul::opengl::Texture::FilterMode::AnisotropicMipMap); - t->purgeFromRAM(); - return t; - } - ); - - BaseModule::TextureManager.release(texture); + // Always set the is-dirty flag, even if the loading fails, as to not try to reload + // the texture without the input file being changed _spriteTextureIsDirty = false; + + switch (_textureMode) { + case TextureInputMode::Single: + initializeSingleTexture(); + // Note that these are usually set when the data slice initialized. However, + // we want to avoid reinitializing the data, and here we know that all points + // will be rendered using the same texture array and hence the data can stay fixed + _textureArrays.front().nPoints = _nDataPoints; + _textureArrays.front().startOffset = 0; + _dataIsDirty = false; + break; + case TextureInputMode::Multi: + initializeMultiTextures(); + break; + case TextureInputMode::Other: + initializeCustomTexture(); + break; + default: + break; + } } int RenderablePointCloud::currentColorParameterIndex() const { @@ -1083,7 +1419,7 @@ int RenderablePointCloud::currentColorParameterIndex() const { int RenderablePointCloud::currentSizeParameterIndex() const { const properties::OptionProperty& property = - _sizeSettings.sizeMapping.parameterOption; + _sizeSettings.sizeMapping->parameterOption; if (!_hasDatavarSize || property.options().empty()) { return 0; @@ -1092,6 +1428,41 @@ int RenderablePointCloud::currentSizeParameterIndex() const { return _dataset.index(property.option().description); } +void RenderablePointCloud::addPositionDataForPoint(unsigned int index, + std::vector& result, + double& maxRadius) const +{ + const dataloader::Dataset::Entry& e = _dataset.entries[index]; + glm::dvec3 position = transformedPosition(e); + const double r = glm::length(position); + + // Add values to result + for (int j = 0; j < 3; ++j) { + result.push_back(static_cast(position[j])); + } + + maxRadius = std::max(maxRadius, r); +} + +void RenderablePointCloud::addColorAndSizeDataForPoint(unsigned int index, + std::vector& result) const +{ + const dataloader::Dataset::Entry& e = _dataset.entries[index]; + + int colorParamIndex = currentColorParameterIndex(); + if (_hasColorMapFile && colorParamIndex >= 0) { + result.push_back(e.data[colorParamIndex]); + } + + int sizeParamIndex = currentSizeParameterIndex(); + if (_hasDatavarSize && sizeParamIndex >= 0) { + // @TODO: Consider more detailed control over the scaling. Currently the value + // is multiplied with the value as is. Should have similar mapping properties + // as the color mapping + result.push_back(e.data[sizeParamIndex]); + } +} + std::vector RenderablePointCloud::createDataSlice() { ZoneScoped; @@ -1099,43 +1470,100 @@ std::vector RenderablePointCloud::createDataSlice() { return std::vector(); } - std::vector result; - result.reserve(nAttributesPerPoint() * _dataset.entries.size()); - - // What datavar is in use for the index color - int colorParamIndex = currentColorParameterIndex(); - - // What datavar is in use for the size scaling (if present) - int sizeParamIndex = currentSizeParameterIndex(); + // What datavar is the texture, if any + int textureIdIndex = _dataset.textureDataIndex; double maxRadius = 0.0; - for (const dataloader::Dataset::Entry& e : _dataset.entries) { - glm::dvec3 position = transformedPosition(e); + // One sub-array per texture array, since each of these will correspond to a separate + // draw call. We need at least one sub result array + std::vector> subResults = std::vector>( + !_textureArrays.empty() ? _textureArrays.size() : 1 + ); - const double r = glm::length(position); - maxRadius = std::max(maxRadius, r); + // Reserve enough space for all points in each for now + for (std::vector& subres : subResults) { + subres.reserve(nAttributesPerPoint() * _dataset.entries.size()); + } - // Positions - for (int j = 0; j < 3; j++) { - result.push_back(static_cast(position[j])); + for (unsigned int i = 0; i < _nDataPoints; i++) { + const dataloader::Dataset::Entry& e = _dataset.entries[i]; + + unsigned int subresultIndex = 0; + float textureLayer = 0.f; + + bool useMultiTexture = (_textureMode == TextureInputMode::Multi) && + (textureIdIndex >= 0); + + if (_hasSpriteTexture && useMultiTexture) { + int texId = static_cast(e.data[textureIdIndex]); + size_t texIndex = _indexInDataToTextureIndex[texId]; + textureLayer = static_cast( + _textureIndexToArrayMap[texIndex].layer + ); + subresultIndex = _textureIndexToArrayMap[texIndex].arrayId; } - // Colors - if (_hasColorMapFile && colorParamIndex > -1) { - result.push_back(e.data[colorParamIndex]); - } + std::vector& subArrayToUse = subResults[subresultIndex]; - // Size data - if (_hasDatavarSize && sizeParamIndex > -1) { - // @TODO: Consider more detailed control over the scaling. Currently the value - // is multiplied with the value as is. Should have similar mapping properties - // as the color mapping - result.push_back(e.data[sizeParamIndex]); + // Add position, color and size data (subclasses may compute these differently) + addPositionDataForPoint(i, subArrayToUse, maxRadius); + addColorAndSizeDataForPoint(i, subArrayToUse); + + // Texture layer + if (_hasSpriteTexture) { + subArrayToUse.push_back(static_cast(textureLayer)); } } + + for (std::vector& subres : subResults) { + subres.shrink_to_fit(); + } + + // Combine subresults, which should be in same order as texture arrays + std::vector result; + result.reserve(nAttributesPerPoint() * _dataset.entries.size()); + size_t vertexCount = 0; + for (size_t i = 0; i < subResults.size(); ++i) { + result.insert(result.end(), subResults[i].begin(), subResults[i].end()); + int nVertices = static_cast(subResults[i].size()) / nAttributesPerPoint(); + if (!_textureArrays.empty()) { + _textureArrays[i].nPoints = nVertices; + _textureArrays[i].startOffset = static_cast(vertexCount); + } + vertexCount += nVertices; + } + result.shrink_to_fit(); + setBoundingSphere(maxRadius); return result; } + +gl::GLenum RenderablePointCloud::internalGlFormat(bool useAlpha) const { + if (useAlpha) { + return _texture.allowCompression ? GL_COMPRESSED_RGBA_S3TC_DXT5_EXT : GL_RGBA8; + } + else { + return _texture.allowCompression ? GL_COMPRESSED_RGB_S3TC_DXT1_EXT : GL_RGB8; + } +} + +ghoul::opengl::Texture::Format RenderablePointCloud::glFormat(bool useAlpha) const { + using Texture = ghoul::opengl::Texture; + return useAlpha ? Texture::Format::RGBA : Texture::Format::RGB; +} + +bool operator==(const TextureFormat& l, const TextureFormat& r) { + return (l.resolution == r.resolution) && (l.useAlpha == r.useAlpha); +} + +size_t TextureFormatHash::operator()(const TextureFormat& k) const { + size_t res = 0; + res += static_cast(k.resolution.x) << 32; + res += static_cast(k.resolution.y) << 16; + res += k.useAlpha ? 0 : 1; + return res; +} + } // namespace openspace diff --git a/modules/base/rendering/pointcloud/renderablepointcloud.h b/modules/base/rendering/pointcloud/renderablepointcloud.h index c1ec0b0be5..05e2cf3a65 100644 --- a/modules/base/rendering/pointcloud/renderablepointcloud.h +++ b/modules/base/rendering/pointcloud/renderablepointcloud.h @@ -27,6 +27,7 @@ #include +#include #include #include #include @@ -52,6 +53,16 @@ namespace openspace { namespace documentation { struct Documentation; } +struct TextureFormat { + glm::uvec2 resolution; + bool useAlpha = false; + + friend bool operator==(const TextureFormat& l, const TextureFormat& r); +}; +struct TextureFormatHash { + size_t operator()(const TextureFormat& k) const; +}; + /** * This class describes a point cloud renderable that can be used to draw billboraded * points based on a data file with 3D positions. Alternatively the points can also @@ -74,15 +85,30 @@ public: static documentation::Documentation Documentation(); protected: + enum class TextureInputMode { + Single = 0, + Multi, + Other // For subclasses that need to handle their own texture + }; + virtual void initializeShadersAndGlExtras(); virtual void deinitializeShaders(); - virtual void bindDataForPointRendering(); + virtual void setExtraUniforms(); virtual void preUpdate(); glm::dvec3 transformedPosition(const dataloader::Dataset::Entry& e) const; virtual int nAttributesPerPoint() const; + /** + * Helper function to buffer the vertex attribute with the given name and number + * of values. Assumes that the value is a float value. + * + * Returns the updated offset after this attribute is added + */ + int bufferVertexAttribute(const std::string& name, GLint nValues, + int nAttributesPerPoint, int offset) const; + virtual void updateBufferData(); void updateSpriteTexture(); @@ -91,17 +117,42 @@ protected: /// Find the index of the currently chosen size parameter in the dataset int currentSizeParameterIndex() const; - virtual std::vector createDataSlice(); + virtual void addPositionDataForPoint(unsigned int index, std::vector& result, + double& maxRadius) const; + virtual void addColorAndSizeDataForPoint(unsigned int index, + std::vector& result) const; - virtual void bindTextureForRendering() const; + std::vector createDataSlice(); + + /** + * A function that subclasses could override to initialize their own textures to + * use for rendering, when the `_textureMode` is set to Other + */ + virtual void initializeCustomTexture(); + void initializeSingleTexture(); + void initializeMultiTextures(); + void clearTextureDataStructures(); + + void loadTexture(const std::filesystem::path& path, int index); + + void initAndAllocateTextureArray(unsigned int textureId, + glm::uvec2 resolution, size_t nLayers, bool useAlpha); + + void fillAndUploadTextureLayer(unsigned int arrayindex, unsigned int layer, + size_t textureIndex, glm::uvec2 resolution, bool useAlpha, const void* pixelData); + + void generateArrayTextures(); float computeDistanceFadeValue(const RenderData& data) const; void renderBillboards(const RenderData& data, const glm::dmat4& modelMatrix, const glm::dvec3& orthoRight, const glm::dvec3& orthoUp, float fadeInVariable); + gl::GLenum internalGlFormat(bool useAlpha) const; + ghoul::opengl::Texture::Format glFormat(bool useAlpha) const; + bool _dataIsDirty = true; - bool _spriteTextureIsDirty = true; + bool _spriteTextureIsDirty = false; bool _cmapIsDirty = true; bool _hasSpriteTexture = false; @@ -113,12 +164,7 @@ protected: struct SizeSettings : properties::PropertyOwner { explicit SizeSettings(const ghoul::Dictionary& dictionary); - struct SizeMapping : properties::PropertyOwner { - SizeMapping(); - properties::BoolProperty enabled; - properties::OptionProperty parameterOption; - }; - SizeMapping sizeMapping; + std::unique_ptr sizeMapping; properties::FloatProperty scaleExponent; properties::FloatProperty scaleFactor; @@ -146,9 +192,6 @@ protected: }; Fading _fading; - properties::BoolProperty _useSpriteTexture; - properties::StringProperty _spriteTexturePath; - properties::BoolProperty _useAdditiveBlending; properties::BoolProperty _drawElements; @@ -156,7 +199,18 @@ protected: properties::UIntProperty _nDataPoints; - ghoul::opengl::Texture* _spriteTexture = nullptr; + struct Texture : properties::PropertyOwner { + Texture(); + properties::BoolProperty enabled; + properties::BoolProperty allowCompression; + properties::BoolProperty useAlphaChannel; + properties::StringProperty spriteTexturePath; + properties::StringProperty inputMode; + }; + Texture _texture; + TextureInputMode _textureMode = TextureInputMode::Single; + std::filesystem::path _texturesDirectory; + ghoul::opengl::ProgramObject* _program = nullptr; UniformCache( @@ -165,7 +219,8 @@ protected: right, fadeInValue, hasSpriteTexture, spriteTexture, useColormap, colorMapTexture, cmapRangeMin, cmapRangeMax, nanColor, useNanColor, hideOutsideRange, enableMaxSizeControl, aboveRangeColor, useAboveRangeColor, belowRangeColor, - useBelowRangeColor, hasDvarScaling, enableOutline, outlineColor, outlineWeight + useBelowRangeColor, hasDvarScaling, dvarScaleFactor, enableOutline, outlineColor, + outlineWeight, aspectRatioScale ) _uniformCache; std::string _dataFile; @@ -181,6 +236,33 @@ protected: GLuint _vao = 0; GLuint _vbo = 0; + + // List of (unique) loaded textures. The other maps refer to the index in this vector + std::vector> _textures; + std::unordered_map _textureNameToIndex; + + // Texture index in dataset to index in vector of textures + std::unordered_map _indexInDataToTextureIndex; + + // Resolution/format to index in textures vector (used to generate one texture + // array per unique format) + std::unordered_map, TextureFormatHash> + _textureMapByFormat; + + // One per resolution above + struct TextureArrayInfo { + GLuint renderId; + GLint startOffset = -1; + int nPoints = -1; + glm::vec2 aspectRatioScale = glm::vec2(1.f); + }; + std::vector _textureArrays; + + struct TextureId { + unsigned int arrayId; + unsigned int layer; + }; + std::unordered_map _textureIndexToArrayMap; }; } // namespace openspace diff --git a/modules/base/rendering/pointcloud/renderablepolygoncloud.cpp b/modules/base/rendering/pointcloud/renderablepolygoncloud.cpp index cdfd786524..8b165d9529 100644 --- a/modules/base/rendering/pointcloud/renderablepolygoncloud.cpp +++ b/modules/base/rendering/pointcloud/renderablepolygoncloud.cpp @@ -38,8 +38,9 @@ namespace { // A RenderablePolygonCloud is a RenderablePointCloud where the shape of the points // is a uniform polygon with a given number of sides instead of a texture. For // instance, PolygonSides = 5 results in the points being rendered as pentagons. - // Note that while this renderable inherits the texture property from - // RenderablePointCloud, any added texture value will be ignored in favor of the + // + // Note that while this renderable inherits the texture component from + // RenderablePointCloud, any added texture information will be ignored in favor of the // polygon shape. // // See documentation of RenderablePointCloud for details on the other parts of the @@ -70,15 +71,11 @@ RenderablePolygonCloud::RenderablePolygonCloud(const ghoul::Dictionary& dictiona _nPolygonSides = p.polygonSides.value_or(_nPolygonSides); // The texture to use for the rendering will be generated in initializeGl. Make sure - // we use it in the rnedering + // we use it in the rendering _hasSpriteTexture = true; -} -void RenderablePolygonCloud::initializeGL() { - ZoneScoped; - - RenderablePointCloud::initializeGL(); - createPolygonTexture(); + _textureMode = TextureInputMode::Other; + removePropertySubOwner(_texture); } void RenderablePolygonCloud::deinitializeGL() { @@ -92,16 +89,24 @@ void RenderablePolygonCloud::deinitializeGL() { RenderablePointCloud::deinitializeGL(); } -void RenderablePolygonCloud::bindTextureForRendering() const { - glBindTexture(GL_TEXTURE_2D, _pTexture); -} - -void RenderablePolygonCloud::createPolygonTexture() { +void RenderablePolygonCloud::initializeCustomTexture() { ZoneScoped; + if (_textureIsInitialized) { + LWARNING("RenderablePolygonCloud texture cannot be updated during runtime"); + return; + } + LDEBUG("Creating Polygon Texture"); constexpr gl::GLsizei TexSize = 512; + // We don't use the helper function for the format and internal format here, + // as we don't want the compression to be used for the polygon texture and we + // always want alpha. This is also why we do not need to update the texture + bool useAlpha = true; + gl::GLenum format = gl::GLenum(glFormat(useAlpha)); + gl::GLenum internalFormat = GL_RGBA8; + glGenTextures(1, &_pTexture); glBindTexture(GL_TEXTURE_2D, _pTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -113,16 +118,35 @@ void RenderablePolygonCloud::createPolygonTexture() { glTexImage2D( GL_TEXTURE_2D, 0, - GL_RGBA8, + internalFormat, TexSize, TexSize, 0, - GL_RGBA, - GL_BYTE, + format, + GL_UNSIGNED_BYTE, nullptr ); renderToTexture(_pTexture, TexSize, TexSize); + + // Download the data and use it to intialize the data we need to rendering. + // Allocate memory: N channels, with one byte each + constexpr unsigned int nChannels = 4; + unsigned int arraySize = TexSize * TexSize * nChannels; + std::vector pixelData; + pixelData.resize(arraySize); + glBindTexture(GL_TEXTURE_2D, _pTexture); + glGetTexImage(GL_TEXTURE_2D, 0, format, GL_UNSIGNED_BYTE, pixelData.data()); + + // Create array from data, size and format + unsigned int id = 0; + glGenTextures(1, &id); + glBindTexture(GL_TEXTURE_2D_ARRAY, id); + initAndAllocateTextureArray(id, glm::uvec2(TexSize), 1, useAlpha); + fillAndUploadTextureLayer(0, 0, 0, glm::uvec2(TexSize), useAlpha, pixelData.data()); + glBindTexture(GL_TEXTURE_2D_ARRAY, 0); + + _textureIsInitialized = true; } void RenderablePolygonCloud::renderToTexture(GLuint textureToRenderTo, @@ -191,7 +215,6 @@ void RenderablePolygonCloud::renderPolygonGeometry(GLuint vao) { glClearBufferfv(GL_COLOR, 0, glm::value_ptr(Black)); program->setUniform("sides", _nPolygonSides); - program->setUniform("polygonColor", _colorSettings.pointColor); glBindVertexArray(vao); glDrawArrays(GL_POINTS, 0, 1); diff --git a/modules/base/rendering/pointcloud/renderablepolygoncloud.h b/modules/base/rendering/pointcloud/renderablepolygoncloud.h index 745f16b0ac..9af0282401 100644 --- a/modules/base/rendering/pointcloud/renderablepolygoncloud.h +++ b/modules/base/rendering/pointcloud/renderablepolygoncloud.h @@ -44,25 +44,24 @@ public: explicit RenderablePolygonCloud(const ghoul::Dictionary& dictionary); ~RenderablePolygonCloud() override = default; - void initializeGL() override; void deinitializeGL() override; static documentation::Documentation Documentation(); private: - void createPolygonTexture(); + void initializeCustomTexture() override; void renderToTexture(GLuint textureToRenderTo, GLuint textureWidth, GLuint textureHeight); void renderPolygonGeometry(GLuint vao); - void bindTextureForRendering() const override; - int _nPolygonSides = 3; GLuint _pTexture = 0; GLuint _polygonVao = 0; GLuint _polygonVbo = 0; + + bool _textureIsInitialized = false; }; } // namespace openspace diff --git a/modules/base/rendering/pointcloud/sizemappingcomponent.cpp b/modules/base/rendering/pointcloud/sizemappingcomponent.cpp new file mode 100644 index 0000000000..a69d580d3e --- /dev/null +++ b/modules/base/rendering/pointcloud/sizemappingcomponent.cpp @@ -0,0 +1,131 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * 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 std::string_view _loggerCat = "SizeMapping"; + + constexpr openspace::properties::Property::PropertyInfo EnabledInfo = { + "Enabled", + "Size Mapping Enabled", + "If this value is set to 'true' and at least one column was loaded as an option " + "for size mapping, the chosen data column will be used to scale the size of the " + "points. The first option in the list is selected per default.", + openspace::properties::Property::Visibility::NoviceUser + }; + + constexpr openspace::properties::Property::PropertyInfo OptionInfo = { + "Parameter", + "Parameter Option", + "This value determines which parameter is used for scaling of the point. The " + "parameter value will be used as a multiplicative factor to scale the size of " + "the points. Note that they may however still be scaled by max size adjustment " + "effects.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + constexpr openspace::properties::Property::PropertyInfo ScaleFactorInfo = { + "ScaleFactor", + "Scale Factor", + "This value is a multiplicative factor that is applied to the data values that " + "are used to scale the points, when size mapping is applied.", + openspace::properties::Property::Visibility::AdvancedUser + }; + + struct [[codegen::Dictionary(SizeMappingComponent)]] Parameters { + // [[codegen::verbatim(EnabledInfo.description)]] + std::optional enabled; + + // A list specifying all parameters that may be used for size mapping, i.e. + // scaling the points based on the provided data columns + std::optional> parameterOptions; + + // [[codegen::verbatim(OptionInfo.description)]] + std::optional parameter; + + // [[codegen::verbatim(ScaleFactorInfo.description)]] + std::optional scaleFactor; + }; +#include "sizemappingcomponent_codegen.cpp" +} // namespace + +namespace openspace { + +documentation::Documentation SizeMappingComponent::Documentation() { + return codegen::doc("base_sizemappingcomponent"); +} + +SizeMappingComponent::SizeMappingComponent() + : properties::PropertyOwner({ "SizeMapping", "Size Mapping", "" }) + , enabled(EnabledInfo, true) + , parameterOption( + OptionInfo, + properties::OptionProperty::DisplayType::Dropdown + ) + , scaleFactor(ScaleFactorInfo, 1.f, 0.f, 1000.f) +{ + addProperty(enabled); + addProperty(parameterOption); + addProperty(scaleFactor); +} + +SizeMappingComponent::SizeMappingComponent(const ghoul::Dictionary& dictionary) + : SizeMappingComponent() +{ + const Parameters p = codegen::bake(dictionary); + + enabled = p.enabled.value_or(enabled); + + int indexOfProvidedOption = -1; + + if (p.parameterOptions.has_value()) { + std::vector opts = *p.parameterOptions; + for (size_t i = 0; i < opts.size(); ++i) { + // Note that options are added in order + parameterOption.addOption(static_cast(i), opts[i]); + + if (p.parameter.has_value() && *p.parameter == opts[i]) { + indexOfProvidedOption = i; + } + } + } + + if (indexOfProvidedOption >= 0) { + parameterOption = indexOfProvidedOption; + } + else if (p.parameter.has_value()) { + LERROR(fmt::format( + "Error when reading Parameter. Could not find provided parameter '{}' in " + "list of parameter options. Using default.", *p.parameter + )); + } + + scaleFactor = p.scaleFactor.value_or(scaleFactor); +} + +} // namespace openspace diff --git a/modules/base/rendering/pointcloud/sizemappingcomponent.h b/modules/base/rendering/pointcloud/sizemappingcomponent.h new file mode 100644 index 0000000000..800436a09f --- /dev/null +++ b/modules/base/rendering/pointcloud/sizemappingcomponent.h @@ -0,0 +1,56 @@ +/***************************************************************************************** + * * + * OpenSpace * + * * + * Copyright (c) 2014-2024 * + * * + * 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___SIZEMAPPINGCOMPONENT___H__ +#define __OPENSPACE_MODULE_BASE___SIZEMAPPINGCOMPONENT___H__ + +#include + +#include +#include +#include + +namespace openspace { + +namespace documentation { struct Documentation; } + +/** + * This is a component that can be used to hold parameters and properties for scaling + * point cloud points (or other data-based entities) based a parameter in a dataset. + */ +struct SizeMappingComponent : public properties::PropertyOwner { + SizeMappingComponent(); + explicit SizeMappingComponent(const ghoul::Dictionary& dictionary); + ~SizeMappingComponent() override = default; + + static documentation::Documentation Documentation(); + + properties::BoolProperty enabled; + properties::OptionProperty parameterOption; + properties::FloatProperty scaleFactor; +}; + +} // namespace openspace + +#endif // __OPENSPACE_MODULE_BASE___SIZEMAPPINGCOMPONENT___H__ diff --git a/modules/base/shaders/pointcloud/billboardpoint_fs.glsl b/modules/base/shaders/pointcloud/billboardpoint_fs.glsl index 629d1ff7a3..3e69bd8913 100644 --- a/modules/base/shaders/pointcloud/billboardpoint_fs.glsl +++ b/modules/base/shaders/pointcloud/billboardpoint_fs.glsl @@ -28,6 +28,7 @@ flat in float gs_colorParameter; flat in float vs_screenSpaceDepth; flat in vec4 vs_positionViewSpace; in vec2 texCoord; +flat in int layer; uniform float opacity; uniform vec3 color; @@ -42,7 +43,7 @@ uniform vec4 belowRangeColor; uniform bool useBelowRangeColor; uniform bool hasSpriteTexture; -uniform sampler2D spriteTexture; +uniform sampler2DArray spriteTexture; uniform bool useColorMap; uniform sampler1D colorMapTexture; @@ -85,23 +86,19 @@ Fragment getFragment() { // Moving the origin to the center and calculating the length float lengthFromCenter = length((texCoord - vec2(0.5)) * 2.0); - if (!hasSpriteTexture) { - if (lengthFromCenter > 1.0) { - discard; - } + if (!hasSpriteTexture && (lengthFromCenter > 1.0)) { + discard; } - vec4 fullColor = vec4(1.0); + vec4 fullColor = glm::vec4(color, 1.0); if (useColorMap) { fullColor = sampleColorMap(gs_colorParameter); } - else { - fullColor.rgb = color; - } if (hasSpriteTexture) { - fullColor *= texture(spriteTexture, texCoord); - } else if (enableOutline && (lengthFromCenter > (1.0 - outlineWeight))) { + fullColor *= texture(spriteTexture, vec3(texCoord, layer)); + } + else if (enableOutline && (lengthFromCenter > (1.0 - outlineWeight))) { fullColor.rgb = outlineColor; } diff --git a/modules/base/shaders/pointcloud/billboardpoint_gs.glsl b/modules/base/shaders/pointcloud/billboardpoint_gs.glsl index 87f9d5eca3..a3114b8dd9 100644 --- a/modules/base/shaders/pointcloud/billboardpoint_gs.glsl +++ b/modules/base/shaders/pointcloud/billboardpoint_gs.glsl @@ -27,12 +27,14 @@ #include "PowerScaling/powerScalingMath.hglsl" layout(points) in; +flat in float textureLayer[]; flat in float colorParameter[]; flat in float scalingParameter[]; layout(triangle_strip, max_vertices = 4) out; flat out float gs_colorParameter; out vec2 texCoord; +flat out int layer; flat out float vs_screenSpaceDepth; flat out vec4 vs_positionViewSpace; @@ -45,6 +47,7 @@ uniform dmat4 projectionMatrix; uniform dmat4 modelMatrix; uniform bool enableMaxSizeControl; uniform bool hasDvarScaling; +uniform float dvarScaleFactor; // RenderOption: CameraViewDirection uniform vec3 up; @@ -58,6 +61,8 @@ uniform vec3 cameraLookUp; // The max size is an angle, in degrees, for the diameter uniform float maxAngularSize; +uniform vec2 aspectRatioScale; + const vec2 corners[4] = vec2[4]( vec2(0.0, 0.0), vec2(1.0, 0.0), @@ -70,13 +75,14 @@ const int RenderOptionCameraPositionNormal = 1; void main() { vec4 pos = gl_in[0].gl_Position; + layer = int(textureLayer[0]); gs_colorParameter = colorParameter[0]; dvec4 dpos = modelMatrix * dvec4(dvec3(pos.xyz), 1.0); float scaleMultiply = pow(10.0, scaleExponent); if (hasDvarScaling) { - scaleMultiply *= scalingParameter[0]; + scaleMultiply *= scalingParameter[0] * dvarScaleFactor; } vec3 scaledRight = vec3(0.0); @@ -116,9 +122,9 @@ void main() { dmat4 cameraViewProjectionMatrix = projectionMatrix * cameraViewMatrix; vec4 dposClip = vec4(cameraViewProjectionMatrix * dpos); - vec4 scaledRightClip = scaleFactor * + vec4 scaledRightClip = scaleFactor * aspectRatioScale.x * vec4(cameraViewProjectionMatrix * dvec4(scaledRight, 0.0)); - vec4 scaledUpClip = scaleFactor * + vec4 scaledUpClip = scaleFactor * aspectRatioScale.y * vec4(cameraViewProjectionMatrix * dvec4(scaledUp, 0.0)); vec4 dposViewSpace= vec4(cameraViewMatrix * dpos); diff --git a/modules/base/shaders/pointcloud/billboardpoint_interpolated_vs.glsl b/modules/base/shaders/pointcloud/billboardpoint_interpolated_vs.glsl index 88b558620a..a6a3407f2e 100644 --- a/modules/base/shaders/pointcloud/billboardpoint_interpolated_vs.glsl +++ b/modules/base/shaders/pointcloud/billboardpoint_interpolated_vs.glsl @@ -38,9 +38,12 @@ in float in_colorParameter1; in float in_scalingParameter0; in float in_scalingParameter1; +in float in_textureLayer; + uniform bool useSpline; uniform float interpolationValue; +flat out float textureLayer; flat out float colorParameter; flat out float scalingParameter; @@ -87,5 +90,7 @@ void main() { ); } + textureLayer = in_textureLayer; + gl_Position = vec4(position, 1.0); } diff --git a/modules/base/shaders/pointcloud/billboardpoint_vs.glsl b/modules/base/shaders/pointcloud/billboardpoint_vs.glsl index 476a994e11..2a1e661ba5 100644 --- a/modules/base/shaders/pointcloud/billboardpoint_vs.glsl +++ b/modules/base/shaders/pointcloud/billboardpoint_vs.glsl @@ -27,13 +27,16 @@ #include "PowerScaling/powerScaling_vs.hglsl" in vec3 in_position; +in float in_textureLayer; in float in_colorParameter; in float in_scalingParameter; +flat out float textureLayer; flat out float colorParameter; flat out float scalingParameter; void main() { + textureLayer = in_textureLayer; colorParameter = in_colorParameter; scalingParameter = in_scalingParameter; gl_Position = vec4(in_position, 1.0); diff --git a/modules/base/shaders/polygon_fs.glsl b/modules/base/shaders/polygon_fs.glsl index 495bdc6232..7f44e3fd7d 100644 --- a/modules/base/shaders/polygon_fs.glsl +++ b/modules/base/shaders/polygon_fs.glsl @@ -26,9 +26,6 @@ out vec4 finalColor; -uniform vec3 polygonColor; - - void main() { - finalColor = vec4(polygonColor, 1.0); + finalColor = vec4(1.0); } diff --git a/src/data/csvloader.cpp b/src/data/csvloader.cpp index b9ad97c06e..7ab566f015 100644 --- a/src/data/csvloader.cpp +++ b/src/data/csvloader.cpp @@ -33,11 +33,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace { @@ -94,6 +96,7 @@ Dataset loadCsvFile(std::filesystem::path filePath, std::optional s int yColumn = -1; int zColumn = -1; int nameColumn = -1; + int textureColumn = -1; int nDataColumns = 0; const bool hasExcludeColumns = specs.has_value() && specs->hasExcludeColumns(); @@ -119,11 +122,17 @@ Dataset loadCsvFile(std::filesystem::path filePath, std::optional s else if (isNameColumn(col, specs)) { nameColumn = static_cast(i); } - else if (hasExcludeColumns && (*specs).isExcludeColumn(col)) { + else if (hasExcludeColumns && specs->isExcludeColumn(col)) { skipColumns.push_back(i); continue; } else { + // Note that the texture column is also a regular column. Just save the index + if (isTextureColumn(col, specs)) { + res.textureDataIndex = nDataColumns; + textureColumn = static_cast(i); + } + res.variables.push_back({ .index = nDataColumns, .name = col @@ -132,6 +141,33 @@ Dataset loadCsvFile(std::filesystem::path filePath, std::optional s } } + // Some errors / warnings + if (specs.has_value()) { + bool hasAllProvided = specs->checkIfAllProvidedColumnsExist(columns); + if (!hasAllProvided) { + LERROR(fmt::format( + "Error loading data file {}. Not all columns provided in data mapping " + "exists in dataset", filePath + )); + } + } + + bool hasProvidedTextureFile = specs.has_value() && specs->textureMap.has_value(); + bool hasTextureIndex = (res.textureDataIndex >= 0); + + if (hasProvidedTextureFile && !hasTextureIndex && !specs->textureColumn.has_value()) { + throw ghoul::RuntimeError(fmt::format( + "Error loading data file {}. No texture column was specified in the data " + "mapping", filePath + )); + } + if (!hasProvidedTextureFile && hasTextureIndex) { + throw ghoul::RuntimeError(fmt::format( + "Error loading data file {}. Missing texture map file location in data " + "mapping", filePath + )); + } + if (xColumn < 0 || yColumn < 0 || zColumn < 0) { // One or more position columns weren't read LERROR(fmt::format( @@ -142,6 +178,8 @@ Dataset loadCsvFile(std::filesystem::path filePath, std::optional s LINFO(fmt::format("Loading {} rows with {} columns", rows.size(), columns.size())); ProgressBar progress = ProgressBar(static_cast(rows.size())); + std::set uniqueTextureIndicesInData; + // Skip first row (column names) for (size_t rowIdx = 1; rowIdx < rows.size(); ++rowIdx) { const std::vector& row = rows[rowIdx]; @@ -180,6 +218,10 @@ Dataset loadCsvFile(std::filesystem::path filePath, std::optional s else { entry.data.push_back(value); } + + if (i == textureColumn) { + uniqueTextureIndicesInData.emplace(static_cast(value)); + } } const glm::vec3 positive = glm::abs(entry.position); @@ -193,6 +235,82 @@ Dataset loadCsvFile(std::filesystem::path filePath, std::optional s progress.print(static_cast(rowIdx + 1)); } + // Load the textures. Skip textures that are not included in the dataset + if (hasProvidedTextureFile) { + const std::filesystem::path path = *specs->textureMap; + if (!std::filesystem::is_regular_file(path)) { + throw ghoul::RuntimeError(fmt::format( + "Failed to open texture map file {}", path + )); + } + res.textures = loadTextureMapFile(path, uniqueTextureIndicesInData); + } + + return res; +} + +std::vector loadTextureMapFile(std::filesystem::path path, + const std::set& texturesInData) +{ + ghoul_assert(std::filesystem::exists(path), "File must exist"); + + std::ifstream file(path); + if (!file.good()) { + throw ghoul::RuntimeError(fmt::format( + "Failed to open texture map file {}", path + )); + } + + int currentLineNumber = 0; + + std::vector res; + + std::string line; + while (std::getline(file, line)) { + ghoul::trimWhitespace(line); + currentLineNumber++; + + if (line.empty() || line.starts_with("#")) { + continue; + } + + std::vector tokens = ghoul::tokenizeString(line, ' '); + int nNonEmptyTokens = static_cast(std::count_if( + tokens.begin(), + tokens.end(), + [](const std::string& t) { return !t.empty(); } + )); + + if (nNonEmptyTokens > 2) { + throw ghoul::RuntimeError(fmt::format( + "Error loading texture map file {}: Line {} has too many parameters. " + "Expected 2: an integer index followed by a filename, where the file " + "name may not include whitespaces", + path, currentLineNumber + )); + } + + std::stringstream str(line); + + // Each line is following the template: + // + Dataset::Texture texture; + str >> texture.index >> texture.file; + + for (const Dataset::Texture& t : res) { + if (t.index == texture.index) { + throw ghoul::RuntimeError(fmt::format( + "Error loading texture map file {}: Texture index '{}' defined twice", + path, texture.index + )); + } + } + + if (texturesInData.contains(texture.index)) { + res.push_back(texture); + } + } + return res; } diff --git a/src/data/datamapping.cpp b/src/data/datamapping.cpp index 3b8f24cd8a..0d14002a2e 100644 --- a/src/data/datamapping.cpp +++ b/src/data/datamapping.cpp @@ -31,6 +31,8 @@ #include namespace { + constexpr std::string_view _loggerCat = "RenderablePolygonCloud"; + constexpr std::string_view DefaultX = "x"; constexpr std::string_view DefaultY = "y"; constexpr std::string_view DefaultZ = "z"; @@ -48,19 +50,19 @@ namespace { if (mapping.has_value()) { switch (columnCase) { case PositionColumn::X: - column = (*mapping).xColumnName.value_or(column); + column = mapping->xColumnName.value_or(column); break; case PositionColumn::Y: - column = (*mapping).yColumnName.value_or(column); + column = mapping->yColumnName.value_or(column); break; case PositionColumn::Z: - column = (*mapping).zColumnName.value_or(column); + column = mapping->zColumnName.value_or(column); break; } } // Per default, allow both lower case and upper case versions of column names - if (!mapping.has_value() || !(*mapping).isCaseSensitive) { + if (!mapping.has_value() || !mapping->isCaseSensitive) { column = ghoul::toLowerCase(column); testColumn = ghoul::toLowerCase(testColumn); } @@ -68,6 +70,27 @@ namespace { return testColumn == column; } + bool isSameStringColumn(const std::string& left, const std::string& right, + bool isCaseSensitive) + { + std::string l = isCaseSensitive ? ghoul::toLowerCase(left) : left; + std::string r = isCaseSensitive ? ghoul::toLowerCase(right) : right; + return (l == r); + } + + bool containsColumn(const std::string& c, const std::vector& columns, + bool isCaseSensitive) + { + auto it = std::find_if( + columns.begin(), + columns.end(), + [&c, &isCaseSensitive](const std::string& col) { + return isSameStringColumn(c, col, isCaseSensitive); + } + ); + return it != columns.end(); + } + // This is a data mapping structure that can be used when creating point cloud // datasets, e.g. from a CSV or Speck file. // @@ -92,6 +115,18 @@ namespace { // files, where the name is given by the comment at the end of each line std::optional name; + // Specifies a column name for a column that has the data for which texture to + // use for each point (given as an integer index). If included, a texture map + // file need to be included as well + std::optional textureColumn; + + // A file where each line contains an integer index and an image file name. + // Not valid for SPECK files, which includes this information as part of its + // data format. This map will be used to map the data in the TextureColumn to + // an image file to use for rendering the points. Note that only the files with + // indices that are used in the dataset will actually be loaded + std::optional textureMapFile; + // Specifies whether to do case sensitive checks when reading column names. // Default is not to, so that 'X' and 'x' are both valid column names for the // x position column, for example @@ -124,6 +159,8 @@ DataMapping DataMapping::createFromDictionary(const ghoul::Dictionary& dictionar result.yColumnName = p.y; result.zColumnName = p.z; result.nameColumn = p.name; + result.textureColumn = p.textureColumn; + result.textureMap = p.textureMapFile; result.missingDataValue = p.missingDataValue; @@ -142,6 +179,28 @@ bool DataMapping::isExcludeColumn(std::string_view column) const { return (found != excludeColumns.end()); } +bool DataMapping::checkIfAllProvidedColumnsExist( + const std::vector& columns) const +{ + auto checkColumnIsOk = [this, &columns](std::optional col, + std::string_view key) + { + if (col.has_value() && !containsColumn(*col, columns, isCaseSensitive)) { + LWARNING(fmt::format("Could not find provided {} column: '{}'", key, *col)); + return false; + } + return true; + }; + + bool hasAll = true; + hasAll &= checkColumnIsOk(xColumnName, "X"); + hasAll &= checkColumnIsOk(yColumnName, "Y"); + hasAll &= checkColumnIsOk(zColumnName, "Z"); + hasAll &= checkColumnIsOk(nameColumn, "Name"); + hasAll &= checkColumnIsOk(textureColumn, "Texture"); + return hasAll; +} + std::string generateHashString(const DataMapping& dm) { std::string a; for (const std::string_view c : dm.excludeColumns) { @@ -150,13 +209,14 @@ std::string generateHashString(const DataMapping& dm) { unsigned int excludeColumnsHash = ghoul::hashCRC32(a); return fmt::format( - "DM|x{}|y{}|z{}|name{}|m{}|{}|{}", + "DM|{}|{}|{}|{}|{}|{}|{}|{}", dm.xColumnName.value_or(""), dm.yColumnName.value_or(""), dm.zColumnName.value_or(""), dm.nameColumn.value_or(""), + dm.textureColumn.value_or(""), dm.missingDataValue.has_value() ? ghoul::to_string(*dm.missingDataValue) : "", - dm.isCaseSensitive ? "1" : "0", + dm.isCaseSensitive ? 1 : 0, excludeColumnsHash ); } @@ -181,14 +241,14 @@ bool isNameColumn(const std::string& c, const std::optional& mappin if (!mapping.has_value() || !mapping->nameColumn.has_value()) { return false; } + return isSameStringColumn(c, *mapping->nameColumn, mapping->isCaseSensitive); +} - std::string testColumn = c; - std::string mappedColumn = *mapping->nameColumn; - if (!mapping->isCaseSensitive) { - testColumn = ghoul::toLowerCase(testColumn); - mappedColumn = ghoul::toLowerCase(mappedColumn); +bool isTextureColumn(const std::string& c, const std::optional& mapping) { + if (!mapping.has_value() || !mapping->textureColumn.has_value()) { + return false; } - return testColumn == mappedColumn; + return isSameStringColumn(c, *mapping->textureColumn, mapping->isCaseSensitive); } } // namespace openspace::dataloader diff --git a/src/data/speckloader.cpp b/src/data/speckloader.cpp index 3b192c0973..9dc2e3e6db 100644 --- a/src/data/speckloader.cpp +++ b/src/data/speckloader.cpp @@ -176,12 +176,27 @@ Dataset loadSpeckFile(std::filesystem::path path, std::optional spe // 2: texture 1 M1.sgi // The parameter in #1 is currently being ignored + std::vector tokens = ghoul::tokenizeString(line, ' '); + int nNonEmptyTokens = static_cast(std::count_if( + tokens.begin(), + tokens.end(), + [](const std::string& t) { return !t.empty(); } + )); + + if (nNonEmptyTokens > 4) { + throw ghoul::RuntimeError(fmt::format( + "Error loading speck file {}: Too many arguments for texture on line {}", + path, currentLineNumber + )); + } + + bool hasExtraParameter = nNonEmptyTokens > 3; + std::stringstream str(line); std::string dummy; str >> dummy; - - if (line.find('-') != std::string::npos) { + if (hasExtraParameter) { str >> dummy; }