Files
OpenSpace/modules/base/rendering/pointcloud/renderablepointcloud.cpp
Emma Broman 6cb4a7efef Fix issue with texture not updating on property change for RenderablePointCloud
Fix issue with texture not being rendered when the texture file path is set during runtime
2024-06-18 13:47:35 +02:00

1770 lines
66 KiB
C++

/*****************************************************************************************
* *
* 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 <modules/base/rendering/pointcloud/renderablepointcloud.h>
#include <modules/base/basemodule.h>
#include <openspace/documentation/documentation.h>
#include <openspace/documentation/verifier.h>
#include <openspace/engine/globals.h>
#include <openspace/util/updatestructures.h>
#include <openspace/rendering/renderengine.h>
#include <ghoul/filesystem/file.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/glm.h>
#include <ghoul/io/texture/texturereader.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/misc/crc32.h>
#include <ghoul/misc/templatefactory.h>
#include <ghoul/misc/profiling.h>
#include <ghoul/opengl/openglstatecache.h>
#include <ghoul/opengl/programobject.h>
#include <ghoul/opengl/texture.h>
#include <ghoul/opengl/textureconversion.h>
#include <ghoul/opengl/textureunit.h>
#include <glm/gtx/string_cast.hpp>
#include <glm/gtx/quaternion.hpp>
#include <glm/gtx/vector_angle.hpp>
#include <array>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <locale>
#include <optional>
#include <string>
namespace {
constexpr std::string_view _loggerCat = "RenderablePointCloud";
enum RenderOption {
ViewDirection = 0,
PositionNormal,
FixedRotation
};
enum OutlineStyle {
Round = 0,
Square,
Bottom
};
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 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 of 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
};
constexpr openspace::properties::Property::PropertyInfo PointColorInfo = {
"FixedColor",
"Fixed Color",
"The color of the points, when no color map is used.",
openspace::properties::Property::Visibility::NoviceUser
};
constexpr openspace::properties::Property::PropertyInfo DrawElementsInfo = {
"DrawElements",
"Draw Elements",
"Enables/Disables the drawing of the points.",
openspace::properties::Property::Visibility::NoviceUser
};
const openspace::properties::PropertyOwner::PropertyOwnerInfo LabelsInfo = {
"Labels",
"Labels",
"The labels for the points. If no label file is provided, the labels will be "
"created to match the points in the data file. For a CSV file, you should then "
"specify which column is the 'Name' column in the data mapping. For SPECK files "
"the labels are created from the comment at the end of each line."
};
constexpr openspace::properties::Property::PropertyInfo FadeInDistancesInfo = {
"FadeInDistances",
"Fade-In Start and End Distances",
"Determines the initial and final distances from the origin of the dataset at "
"which the points will start and end fading-in. The distances are specified in "
"the same unit as the points, that is, the one provodied as the Unit, or meters. "
"With normal fading the points are fully visible once the camera is outside this "
"range and fully invisible when inside the range. With inverted fading the "
"case is the opposite: the points are visible inside when closer than the min "
"value of the range and invisible when further away.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo EnableDistanceFadeInfo = {
"Enabled",
"Enable Distance-based Fading",
"Enables/disables the Fade-in effect based on camera distance. Automatically set "
"to true if FadeInDistances are specified in the asset.",
openspace::properties::Property::Visibility::User
};
constexpr openspace::properties::Property::PropertyInfo InvertFadeInfo = {
"Invert",
"Invert",
"If true, inverts the fading so that the points are invisible when the camera "
"is further away than the max fade distance and fully visible when it is closer "
"than the min distance.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo UseAdditiveBlendingInfo = {
"UseAdditiveBlending",
"Use Additive Blending",
"If true (default), the color of points rendered on top of each other is "
"blended additively, resulting in a brighter color where points overlap. "
"If false, no such blending will take place and the color of the point "
"will not be modified by blending. Note that this may lead to weird behaviors "
"when the points are rendered with transparency.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo UseOrientationDataInfo = {
"UseOrientationData",
"Use Orientation Data",
"If true, the orientation data in the dataset is included when rendering the "
"points, if there is any. To see the rotation, you also need to set the "
"\"Orientation Render Option\" to \"Fixed Rotation\".",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo OrientationRenderOptionInfo =
{
"OrientationRenderOption",
"Orientation Render Option",
"Controls how the planes for the points will be oriented. \"Camera View "
"Direction\" rotates the points so that the plane is orthogonal to the viewing "
"direction of the camera (useful for planar displays), and \"Camera Position "
"Normal\" rotates the points towards the position of the camera (useful for "
"spherical displays, like dome theaters). In both these cases the points will "
"be billboarded towards the camera. In contrast, \"Fixed Rotation\" does not "
"rotate the points at all based on the camera and should be used when the "
"dataset contains orientation information for the points.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo NumShownDataPointsInfo = {
"NumberOfDataPoints",
"Number of Shown Data Points",
"Information about how many points are being rendered.",
openspace::properties::Property::Visibility::User
};
constexpr openspace::properties::Property::PropertyInfo HasOrientationDataInfo = {
"HasOrientationData",
"Has Orientation Data",
"Set to true if orientation data was read from the dataset.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo ScaleExponentInfo = {
"ScaleExponent",
"Scale Exponent",
"An exponential scale value used to set the absolute size of the point. In "
"general, the larger distance the dataset covers, the larger this value should "
"be. If not included, it is computed based on the maximum positional component "
"of the data points. This is useful for showing the dataset at all, but you will "
"likely want to change it to something that looks good. Note that a scale "
"exponent of 0 leads to the points having a diameter of 1 meter, i.e. no "
"exponential scaling.",
openspace::properties::Property::Visibility::User
};
constexpr openspace::properties::Property::PropertyInfo ScaleFactorInfo = {
"ScaleFactor",
"Scale Factor",
"A multiplicative factor used to adjust the size of the points, after the "
"exponential scaling and any max size control effects. Simply just increases "
"or decreases the visual size of the points.",
openspace::properties::Property::Visibility::User
};
constexpr openspace::properties::Property::PropertyInfo UseMaxSizeControlInfo = {
"EnableMaxSizeControl",
"Enable Max Size Control",
"If true, the Max Size property will be used as an upper limit for the size of "
"the point. This reduces the size of the points when approaching them, so that "
"they stick to a maximum visual size depending on the Max Size value.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo MaxSizeInfo = {
"MaxSize",
"Max Size",
"Controls the maximum allowed size for the points, when the max size control "
"feature is enabled. This limits the visual size of the points based on the "
"distance to the camera. The larger the value, the larger the points may be. "
"In the background, the computations are made by limiting the size to a certain "
"angle based on the field of view of the camera. So a value of 1 limits the "
"point size to take up a maximum of one degree of the view space.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo SizeMappingEnabledInfo = {
"Enabled",
"Size Mapping Enabled",
"Decides whether size mapping should be enabled. If 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.",
openspace::properties::Property::Visibility::NoviceUser
};
constexpr openspace::properties::Property::PropertyInfo SizeMappingOptionInfo = {
"Parameter",
"Parameter Option",
"The name of a data parameter used for scaling of the points. The parameter "
"value will be used as a multiplicative factor to scale the size of the points. "
"Note that they may still be scaled by max size adjustment effects.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo EnableOutlineInfo = {
"EnableOutline",
"Enable Point Outline",
"Determines whether each point should have an outline or not.",
openspace::properties::Property::Visibility::User
};
constexpr openspace::properties::Property::PropertyInfo OutlineColorInfo = {
"OutlineColor",
"Outline Color",
"The color of the outline. Darker colors will be less visible if \"Additive "
"Blending\" is enabled.",
openspace::properties::Property::Visibility::User
};
constexpr openspace::properties::Property::PropertyInfo OutlineWidthInfo = {
"OutlineWidth",
"Outline Width",
"The thickness of the outline, given as a value relative to the size of the "
"point. A value of 0 will not show any outline, while a value of 1 will cover "
"the whole point.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo OutlineStyleInfo = {
"OutlineStyle",
"Outline Style",
"Decides the style of the outline (round, square, or a line at the bottom). "
"The style also affects the shape of the points.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo ApplyColorMapToOutlineInfo = {
"ApplyColorMapToOutline",
"Apply Color Map to Outline",
"If true and the outline is enabled, the color map will be applied to the "
"outline rather than the point body. Only works if color mapping is enabled.",
openspace::properties::Property::Visibility::AdvancedUser
};
// A RenderablePointCloud can be used to render point-based datasets in 3D space,
// optionally including color mapping, a sprite texture and labels. There are several
// properties that affect the visuals of the points, such as settings for scaling,
// fading, sprite texture, color mapping and whether the colors of overlapping points
// should be blended additively or not.
//
// The points are rendered as planes whose size depends on a few different things:
//
// - At the core, scaling is done based on an exponential value, the `ScaleExponent`.
// A relatively small change to this value will lead to a large change in size.
// When no exponent is set, one will be created based on the coordinates in the
// dataset. The points will be visible, but may be appeared as too large or small.
// One option is to not specify the exponent when loading the dataset for the the,
// first time, to make sure the points are visual, and then adapt the value
// interactively when OpenSpace is running until you find a value that you find
// suitable.
//
// - There is also an option to limit the size of the points based on a given max
// size value.
//
// - And an option to scale the points based on a data value (see `SizeMapping` in
// `SizeSettings`)
//
// - To easily change the visual size of the points, the multiplicative `ScaleFactor`
// may be used. A value of 2 makes the points twice as large, visually, compared
// to 1.
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<std::filesystem::path> file;
// If true (default), the loaded dataset and color map will be cached so that they
// can be loaded faster at a later time. This does however mean that any updates
// to the values in the dataset will not lead to changes in the rendering without
// first removing the cached file. Set it to false to disable caching. This can be
// useful for example when working on importing a new dataset or when making
// changes to the color map.
std::optional<bool> useCaching;
// A dictionary specifying details on how to load the dataset. Updating the data
// mapping will lead to a new cached version of the dataset.
std::optional<ghoul::Dictionary> dataMapping
[[codegen::reference("dataloader_datamapping")]];
struct Texture {
// [[codegen::verbatim(TextureEnabledInfo.description)]]
std::optional<bool> enabled;
// [[codegen::verbatim(SpriteTextureInfo.description)]]
std::optional<std::filesystem::path> 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<std::filesystem::path> folder [[codegen::directory()]];
// [[codegen::verbatim(AllowTextureCompressionInfo.description)]]
std::optional<bool> allowCompression;
// [[codegen::verbatim(UseAlphaInfo.description)]]
std::optional<bool> useAlphaChannel;
};
// Settings related to the texturing of the points.
std::optional<Texture> texture;
// [[codegen::verbatim(DrawElementsInfo.description)]]
std::optional<bool> drawElements;
enum class [[codegen::map(RenderOption)]] RenderOption {
ViewDirection [[codegen::key("Camera View Direction")]],
PositionNormal [[codegen::key("Camera Position Normal")]],
FixedRotation [[codegen::key("Fixed Rotation")]]
};
// [[codegen::verbatim(OrientationRenderOptionInfo.description)]]
std::optional<RenderOption> orientationRenderOption;
// [[codegen::verbatim(UseOrientationDataInfo.description)]]
std::optional<bool> useOrientationData;
// [[codegen::verbatim(UseAdditiveBlendingInfo.description)]]
std::optional<bool> useAdditiveBlending;
// If true, skip the first data point in the loaded dataset.
std::optional<bool> skipFirstDataPoint;
enum class [[codegen::map(openspace::DistanceUnit)]] Unit {
Meter [[codegen::key("m")]],
Kilometer [[codegen::key("Km")]],
Parsec [[codegen::key("pc")]],
Kiloparsec [[codegen::key("Kpc")]],
Megaparsec [[codegen::key("Mpc")]],
Gigaparsec [[codegen::key("Gpc")]],
Gigalightyear [[codegen::key("Gly")]]
};
// The unit used for all distances. Should match the unit of any
// distances/positions in the data files.
std::optional<Unit> unit;
// [[codegen::verbatim(LabelsInfo.description)]]
std::optional<ghoul::Dictionary> labels
[[codegen::reference("labelscomponent")]];
struct SizeSettings {
// Settings related to scaling the points based on data.
std::optional<ghoul::Dictionary> sizeMapping
[[codegen::reference("base_sizemappingcomponent")]];
// [[codegen::verbatim(ScaleExponentInfo.description)]]
std::optional<float> scaleExponent;
// [[codegen::verbatim(ScaleFactorInfo.description)]]
std::optional<float> scaleFactor;
// [[codegen::verbatim(UseMaxSizeControlInfo.description)]]
std::optional<bool> enableMaxSizeControl;
// [[codegen::verbatim(MaxSizeInfo.description)]]
std::optional<float> maxSize;
};
// Settings related to the scale of the points, whether they should limit to
// a certain max size, etc.
std::optional<SizeSettings> sizeSettings;
struct ColorSettings {
// [[codegen::verbatim(PointColorInfo.description)]]
std::optional<glm::vec3> fixedColor [[codegen::color()]];
// Settings related to the choice of color map, parameters, etc.
std::optional<ghoul::Dictionary> colorMapping
[[codegen::reference("colormappingcomponent")]];
// [[codegen::verbatim(EnableOutlineInfo.description)]]
std::optional<bool> enableOutline;
// [[codegen::verbatim(OutlineColorInfo.description)]]
std::optional<glm::vec3> outlineColor;
// [[codegen::verbatim(OutlineWidthInfo.description)]]
std::optional<float> outlineWidth;
enum class [[codegen::map(OutlineStyle)]] OutlineStyle {
Round,
Square,
Bottom
};
// [[codegen::verbatim(OutlineStyleInfo.description)]]
std::optional<OutlineStyle> outlineStyle;
// [[codegen::verbatim(ApplyColorMapToOutlineInfo.description)]]
std::optional<bool> applyColorMapToOutline;
};
// Settings related to the coloring of the points, such as a fixed color,
// color map, etc.
std::optional<ColorSettings> coloring;
struct Fading {
// [[codegen::verbatim(EnableDistanceFadeInfo.description)]]
std::optional<bool> enabled;
// [[codegen::verbatim(FadeInDistancesInfo.description)]]
std::optional<glm::dvec2> fadeInDistances;
// [[codegen::verbatim(InvertFadeInfo.description)]]
std::optional<bool> invert;
};
// Settings related to fading based on camera distance. Can be used to either
// fade away or fade in the points when reaching a certain distance from the
// origin of the dataset.
std::optional<Fading> fading;
// Transformation matrix to be applied to the position of each object.
std::optional<glm::dmat4x4> transformationMatrix;
};
#include "renderablepointcloud_codegen.cpp"
} // namespace
namespace openspace {
documentation::Documentation RenderablePointCloud::Documentation() {
return codegen::doc<Parameters>("base_renderablepointcloud");
}
RenderablePointCloud::SizeSettings::SizeSettings(const ghoul::Dictionary& dictionary)
: properties::PropertyOwner({ "Sizing", "Sizing", ""})
, scaleExponent(ScaleExponentInfo, 1.f, 0.f, 25.f)
, scaleFactor(ScaleFactorInfo, 1.f, 0.f, 100.f)
, useMaxSizeControl(UseMaxSizeControlInfo, false)
, maxAngularSize(MaxSizeInfo, 1.f, 0.f, 45.f)
{
const Parameters p = codegen::bake<Parameters>(dictionary);
if (p.sizeSettings.has_value()) {
const Parameters::SizeSettings settings = *p.sizeSettings;
scaleFactor = settings.scaleFactor.value_or(scaleFactor);
scaleExponent = settings.scaleExponent.value_or(scaleExponent);
useMaxSizeControl = settings.enableMaxSizeControl.value_or(useMaxSizeControl);
maxAngularSize = settings.maxSize.value_or(maxAngularSize);
if (settings.sizeMapping.has_value()) {
sizeMapping = std::make_unique<SizeMappingComponent>(
*settings.sizeMapping
);
addPropertySubOwner(sizeMapping.get());
}
}
addProperty(scaleFactor);
addProperty(scaleExponent);
addProperty(useMaxSizeControl);
addProperty(maxAngularSize);
}
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))
, enableOutline(EnableOutlineInfo, false)
, outlineColor(OutlineColorInfo, glm::vec3(0.23f), glm::vec3(0.f), glm::vec3(1.f))
, outlineWidth(OutlineWidthInfo, 0.2f, 0.f, 1.f)
, outlineStyle(OutlineStyleInfo)
, applyCmapToOutline(ApplyColorMapToOutlineInfo, false)
{
const Parameters p = codegen::bake<Parameters>(dictionary);
pointColor.setViewOption(properties::Property::ViewOptions::Color);
addProperty(pointColor);
addProperty(enableOutline);
outlineColor.setViewOption(properties::Property::ViewOptions::Color);
addProperty(outlineColor);
addProperty(outlineWidth);
outlineStyle.addOption(OutlineStyle::Round, "Round");
outlineStyle.addOption(OutlineStyle::Square, "Square");
outlineStyle.addOption(OutlineStyle::Bottom, "Bottom");
outlineStyle = OutlineStyle::Round;
addProperty(outlineStyle);
addProperty(applyCmapToOutline);
const bool hasColoring = p.coloring.has_value();
if (hasColoring) {
const Parameters::ColorSettings settings = *p.coloring;
pointColor = settings.fixedColor.value_or(pointColor);
if (settings.colorMapping.has_value()) {
colorMapping = std::make_unique<ColorMappingComponent>(
*settings.colorMapping
);
addPropertySubOwner(colorMapping.get());
}
enableOutline = p.coloring->enableOutline.value_or(enableOutline);
outlineColor = p.coloring->outlineColor.value_or(outlineColor);
outlineWidth = p.coloring->outlineWidth.value_or(outlineWidth);
if (p.coloring->outlineStyle.has_value()) {
outlineStyle = codegen::map<OutlineStyle>(*p.coloring->outlineStyle);
}
applyCmapToOutline = p.coloring->applyColorMapToOutline.value_or(
applyCmapToOutline
);
}
}
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", "" })
, fadeInDistances(
FadeInDistancesInfo,
glm::vec2(0.f),
glm::vec2(0.f),
glm::vec2(100.f)
)
, enabled(EnableDistanceFadeInfo, false)
, invert(InvertFadeInfo, false)
{
const Parameters p = codegen::bake<Parameters>(dictionary);
if (p.fading.has_value()) {
const Parameters::Fading f = *p.fading;
if (f.fadeInDistances.has_value()) {
fadeInDistances = *f.fadeInDistances;
// Set the allowed max value based of that which was entered. Just to give
// useful values for the slider
fadeInDistances.setMaxValue(10.f * glm::vec2(fadeInDistances.value().y));
}
enabled = f.enabled.value_or(f.fadeInDistances.has_value());
invert = f.invert.value_or(invert);
}
addProperty(enabled);
fadeInDistances.setViewOption(properties::Property::ViewOptions::MinMaxRange);
addProperty(fadeInDistances);
addProperty(invert);
}
RenderablePointCloud::RenderablePointCloud(const ghoul::Dictionary& dictionary)
: Renderable(dictionary)
, _sizeSettings(dictionary)
, _colorSettings(dictionary)
, _fading(dictionary)
, _useAdditiveBlending(UseAdditiveBlendingInfo, true)
, _drawElements(DrawElementsInfo, true)
, _useRotation(UseOrientationDataInfo, false)
, _renderOption(
OrientationRenderOptionInfo,
properties::OptionProperty::DisplayType::Dropdown
)
, _nDataPoints(NumShownDataPointsInfo, 0)
, _hasOrientationData(HasOrientationDataInfo, false)
{
ZoneScoped;
const Parameters p = codegen::bake<Parameters>(dictionary);
addProperty(Fadeable::_opacity);
if (p.file.has_value()) {
_hasDataFile = true;
_dataFile = absPath(*p.file);
}
if (p.dataMapping.has_value()) {
_dataMapping = dataloader::DataMapping::createFromDictionary(*p.dataMapping);
}
_drawElements = p.drawElements.value_or(_drawElements);
addProperty(_drawElements);
_renderOption.addOption(RenderOption::ViewDirection, "Camera View Direction");
_renderOption.addOption(RenderOption::PositionNormal, "Camera Position Normal");
_renderOption.addOption(RenderOption::FixedRotation, "Fixed Rotation");
if (p.orientationRenderOption.has_value()) {
_renderOption = codegen::map<RenderOption>(*p.orientationRenderOption);
}
else {
_renderOption = RenderOption::ViewDirection;
}
addProperty(_renderOption);
_useRotation = p.useOrientationData.value_or(_useRotation);
_useRotation.onChange([this]() { _dataIsDirty = true; });
addProperty(_useRotation);
_useAdditiveBlending = p.useAdditiveBlending.value_or(_useAdditiveBlending);
addProperty(_useAdditiveBlending);
if (p.unit.has_value()) {
_unit = codegen::map<DistanceUnit>(*p.unit);
}
else {
_unit = DistanceUnit::Meter;
}
addPropertySubOwner(_texture);
if (p.texture.has_value()) {
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);
if (t.file.has_value()) {
LWARNING(std::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.enabled = t.enabled.value_or(_texture.enabled);
_texture.allowCompression =
t.allowCompression.value_or(_texture.allowCompression);
_texture.useAlphaChannel = t.useAlphaChannel.value_or(_texture.useAlphaChannel);
}
_texture.spriteTexturePath.onChange([this]() {
_spriteTextureIsDirty = true;
_hasSpriteTexture = !_texture.spriteTexturePath.value().empty();
});
_texture.allowCompression.onChange([this]() { _spriteTextureIsDirty = true; });
_texture.useAlphaChannel.onChange([this]() { _spriteTextureIsDirty = true; });
_transformationMatrix = p.transformationMatrix.value_or(_transformationMatrix);
if (_sizeSettings.sizeMapping != nullptr) {
_sizeSettings.sizeMapping->parameterOption.onChange(
[this]() { _dataIsDirty = true; }
);
_sizeSettings.sizeMapping->isRadius.onChange([this]() { _dataIsDirty = true; });
_hasDatavarSize = true;
}
addPropertySubOwner(_sizeSettings);
addPropertySubOwner(_colorSettings);
if (p.fading.has_value()) {
addPropertySubOwner(_fading);
}
if (p.coloring.has_value() && (*p.coloring).colorMapping.has_value()) {
_hasColorMapFile = true;
_colorSettings.colorMapping->dataColumn.onChange(
[this]() { _dataIsDirty = true; }
);
_colorSettings.colorMapping->setRangeFromData.onChange([this]() {
int parameterIndex = currentColorParameterIndex();
_colorSettings.colorMapping->valueRange = _dataset.findValueRange(
parameterIndex
);
});
_colorSettings.colorMapping->colorMapFile.onChange([this]() {
_dataIsDirty = true;
_hasColorMapFile = std::filesystem::exists(
_colorSettings.colorMapping->colorMapFile.value()
);
});
}
_useCaching = p.useCaching.value_or(_useCaching);
_skipFirstDataPoint = p.skipFirstDataPoint.value_or(_skipFirstDataPoint);
// If no scale exponent was specified, compute one that will at least show the
// points based on the scale of the positions in the dataset
if (!p.sizeSettings.has_value() || !p.sizeSettings->scaleExponent.has_value()) {
_shouldComputeScaleExponent = true;
}
if (p.labels.has_value()) {
if (!p.labels->hasKey("File") && _hasDataFile) {
_createLabelsFromDataset = true;
}
_labels = std::make_unique<LabelsComponent>(*p.labels);
_hasLabels = true;
addPropertySubOwner(_labels.get());
// Fading of the labels should depend on the fading of the renderable
_labels->setParentFadeable(this);
}
_nDataPoints.setReadOnly(true);
addProperty(_nDataPoints);
_hasOrientationData.setReadOnly(true);
addProperty(_hasOrientationData);
}
bool RenderablePointCloud::isReady() const {
bool isReady = _program;
if (_hasLabels) {
isReady = isReady && _labels->isReady();
}
return isReady;
}
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) {
if (_useCaching) {
_dataset = dataloader::data::loadFileWithCache(_dataFile, _dataMapping);
}
else {
_dataset = dataloader::data::loadFile(_dataFile, _dataMapping);
}
if (_skipFirstDataPoint) {
_dataset.entries.erase(_dataset.entries.begin());
}
_nDataPoints = static_cast<unsigned int>(_dataset.entries.size());
_hasOrientationData = _dataset.orientationDataIndex >= 0;
// If no scale exponent was specified, compute one that will at least show the
// points based on the scale of the positions in the dataset
if (_shouldComputeScaleExponent) {
double dist = _dataset.maxPositionComponent * toMeter(_unit);
if (dist > 0.0) {
float exponent = static_cast<float>(std::log10(dist));
// Reduce the actually used exponent a little bit, as just using the
// logarithm as is leads to very large points
_sizeSettings.scaleExponent = 0.9f * exponent;
}
}
}
if (_hasDataFile && _hasColorMapFile) {
_colorSettings.colorMapping->initialize(_dataset, _useCaching);
}
if (_hasLabels) {
if (_createLabelsFromDataset) {
_labels->loadLabelsFromDataset(_dataset, _unit);
}
_labels->initialize();
}
}
void RenderablePointCloud::initializeGL() {
ZoneScoped;
initializeShadersAndGlExtras();
ghoul::opengl::updateUniformLocations(*_program, _uniformCache);
if (_hasSpriteTexture) {
switch (_textureMode) {
case TextureInputMode::Single:
initializeSingleTexture();
break;
case TextureInputMode::Multi:
initializeMultiTextures();
break;
case TextureInputMode::Other:
initializeCustomTexture();
break;
default:
break;
}
}
}
void RenderablePointCloud::deinitializeGL() {
glDeleteBuffers(1, &_vbo);
_vbo = 0;
glDeleteVertexArrays(1, &_vao);
_vao = 0;
deinitializeShaders();
clearTextureDataStructures();
}
void RenderablePointCloud::initializeShadersAndGlExtras() {
_program = BaseModule::ProgramObjectManager.request(
"RenderablePointCloud",
[]() {
return global::renderEngine->buildRenderProgram(
"RenderablePointCloud",
absPath("${MODULE_BASE}/shaders/pointcloud/pointcloud_vs.glsl"),
absPath("${MODULE_BASE}/shaders/pointcloud/pointcloud_fs.glsl"),
absPath("${MODULE_BASE}/shaders/pointcloud/pointcloud_gs.glsl")
);
}
);
}
void RenderablePointCloud::deinitializeShaders() {
BaseModule::ProgramObjectManager.release(
"RenderablePointCloud",
[](ghoul::opengl::ProgramObject* p) {
global::renderEngine->removeRenderProgram(p);
}
);
_program = nullptr;
}
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(std::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(std::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<ghoul::opengl::Texture> t =
ghoul::io::TextureReader::ref().loadTexture(path, 2);
bool useAlpha = (t->numberOfChannels() > 3) && _texture.useAlphaChannel;
if (t) {
LINFOC("RenderablePlanesCloud", std::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(std::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<float>(resolution.x);
float h = static_cast<float>(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<gl::GLsizei>(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<const TextureFormat, std::vector<size_t>>;
unsigned int arrayIndex = 0;
for (const Entry& e : _textureMapByFormat) {
glm::uvec2 res = e.first.resolution;
bool useAlpha = e.first.useAlpha;
std::vector<size_t> 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<int>(layer) > nMaxTextureLayers) {
LERROR(std::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++;
}
}
float RenderablePointCloud::computeDistanceFadeValue(const RenderData& data) const {
if (!_fading.enabled) {
return 1.f;
}
float fadeValue = 1.f;
glm::dmat4 invModelMatrix = glm::inverse(calcModelTransform(data));
glm::dvec3 cameraPosModelSpace = glm::dvec3(
invModelMatrix * glm::dvec4(data.camera.positionVec3(), 1.0)
);
float distCamera = static_cast<float>(
glm::length(cameraPosModelSpace) / toMeter(_unit)
);
const glm::vec2 fadeRange = _fading.fadeInDistances;
const float fadeRangeWidth = (fadeRange.y - fadeRange.x);
float funcValue = (distCamera - fadeRange.x) / fadeRangeWidth;
funcValue = glm::clamp(funcValue, 0.f, 1.f);
if (_fading.invert) {
funcValue = 1.f - funcValue;
}
return fadeValue * funcValue;
}
void RenderablePointCloud::setExtraUniforms() {}
void RenderablePointCloud::renderPoints(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.cameraPosition, 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());
_program->setUniform(_uniformCache.scaleExponent, _sizeSettings.scaleExponent);
_program->setUniform(_uniformCache.scaleFactor, _sizeSettings.scaleFactor);
_program->setUniform(
_uniformCache.enableMaxSizeControl,
_sizeSettings.useMaxSizeControl
);
_program->setUniform(_uniformCache.maxAngularSize, _sizeSettings.maxAngularSize);
bool useSizeMapping = _hasDatavarSize && _sizeSettings.sizeMapping &&
_sizeSettings.sizeMapping->enabled;
_program->setUniform(_uniformCache.hasDvarScaling, useSizeMapping);
if (useSizeMapping) {
_program->setUniform(
_uniformCache.dvarScaleFactor,
_sizeSettings.sizeMapping->scaleFactor
);
}
_program->setUniform(_uniformCache.color, _colorSettings.pointColor);
_program->setUniform(_uniformCache.enableOutline, _colorSettings.enableOutline);
_program->setUniform(_uniformCache.outlineColor, _colorSettings.outlineColor);
_program->setUniform(_uniformCache.outlineWeight, _colorSettings.outlineWidth);
_program->setUniform(_uniformCache.outlineStyle, _colorSettings.outlineStyle);
_program->setUniform(_uniformCache.useCmapOutline, _colorSettings.applyCmapToOutline);
bool useColorMap = hasColorData() && _colorSettings.colorMapping->enabled &&
_colorSettings.colorMapping->texture();
_program->setUniform(_uniformCache.useColorMap, useColorMap);
ghoul::opengl::TextureUnit colorMapTextureUnit;
_program->setUniform(_uniformCache.colorMapTexture, colorMapTextureUnit);
if (useColorMap) {
colorMapTextureUnit.activate();
_colorSettings.colorMapping->texture()->bind();
const glm::vec2 range = _colorSettings.colorMapping->valueRange;
_program->setUniform(_uniformCache.cmapRangeMin, range.x);
_program->setUniform(_uniformCache.cmapRangeMax, range.y);
_program->setUniform(
_uniformCache.hideOutsideRange,
_colorSettings.colorMapping->hideOutsideRange
);
_program->setUniform(
_uniformCache.nanColor,
_colorSettings.colorMapping->nanColor
);
_program->setUniform(
_uniformCache.useNanColor,
_colorSettings.colorMapping->useNanColor
);
_program->setUniform(
_uniformCache.aboveRangeColor,
_colorSettings.colorMapping->aboveRangeColor
);
_program->setUniform(
_uniformCache.useAboveRangeColor,
_colorSettings.colorMapping->useAboveRangeColor
);
_program->setUniform(
_uniformCache.belowRangeColor,
_colorSettings.colorMapping->belowRangeColor
);
_program->setUniform(
_uniformCache.useBelowRangeColor,
_colorSettings.colorMapping->useBelowRangeColor
);
}
_program->setUniform(_uniformCache.useOrientationData, useOrientationData());
bool useTexture = _hasSpriteTexture && _texture.enabled;
_program->setUniform(_uniformCache.hasSpriteTexture, useTexture);
ghoul::opengl::TextureUnit spriteTextureUnit;
_program->setUniform(_uniformCache.spriteTexture, spriteTextureUnit);
setExtraUniforms();
glBindVertexArray(_vao);
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<GLsizei>(arrayInfo.nPoints)
);
}
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
}
else {
_program->setUniform(_uniformCache.aspectRatioScale, glm::vec2(1.f));
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(_nDataPoints));
}
glBindVertexArray(0);
_program->deactivate();
global::renderEngine->openglStateCache().resetBlendState();
global::renderEngine->openglStateCache().resetDepthState();
}
void RenderablePointCloud::render(const RenderData& data, RendererTasks&) {
float fadeInVar = computeDistanceFadeValue(data);
if (fadeInVar < 0.01f) {
return;
}
glm::dmat4 modelMatrix = calcModelTransform(data);
glm::dvec3 cameraViewDirectionWorld = -data.camera.viewDirectionWorldSpace();
glm::dvec3 cameraUpDirectionWorld = data.camera.lookUpVectorWorldSpace();
glm::dvec3 orthoRight = glm::normalize(
glm::cross(cameraUpDirectionWorld, cameraViewDirectionWorld)
);
if (orthoRight == glm::dvec3(0.0)) {
glm::dvec3 otherVector = glm::vec3(
cameraUpDirectionWorld.y,
cameraUpDirectionWorld.x,
cameraUpDirectionWorld.z
);
orthoRight = glm::normalize(glm::cross(otherVector, cameraViewDirectionWorld));
}
glm::dvec3 orthoUp = glm::normalize(glm::cross(cameraViewDirectionWorld, orthoRight));
if (_hasDataFile && _drawElements) {
renderPoints(data, modelMatrix, orthoRight, orthoUp, fadeInVar);
}
if (_hasLabels) {
glm::dmat4 modelViewProjectionMatrix =
calcModelViewProjectionTransform(data, modelMatrix);
_labels->render(data, modelViewProjectionMatrix, orthoRight, orthoUp, fadeInVar);
}
}
void RenderablePointCloud::preUpdate() {}
void RenderablePointCloud::update(const UpdateData&) {
ZoneScoped;
preUpdate();
if (_hasColorMapFile) {
_colorSettings.colorMapping->update(_dataset, _useCaching);
}
if (_spriteTextureIsDirty) {
updateSpriteTexture();
}
if (_dataIsDirty) {
updateBufferData();
}
}
glm::dvec3 RenderablePointCloud::transformedPosition(
const dataloader::Dataset::Entry& e) const
{
const double unitMeter = toMeter(_unit);
glm::dvec4 position = glm::dvec4(glm::dvec3(e.position) * unitMeter, 1.0);
return glm::dvec3(_transformationMatrix * position);
}
glm::quat RenderablePointCloud::orientationQuaternion(
const dataloader::Dataset::Entry& e) const
{
const int orientationDataIndex = _dataset.orientationDataIndex;
const glm::vec3 u = glm::normalize(glm::vec3(
_transformationMatrix *
glm::dvec4(
e.data[orientationDataIndex + 0],
e.data[orientationDataIndex + 1],
e.data[orientationDataIndex + 2],
1.f
)
));
const glm::vec3 v = glm::normalize(glm::vec3(
_transformationMatrix *
glm::dvec4(
e.data[orientationDataIndex + 3],
e.data[orientationDataIndex + 4],
e.data[orientationDataIndex + 5],
1.f
)
));
// Get the quaternion that represents the rotation from XY plane to the plane that is
// spanned by the UV vectors.
// First rotate to align the z-axis with plane normal
const glm::vec3 planeNormal = glm::normalize(glm::cross(u, v));
glm::quat q = glm::normalize(glm::rotation(glm::vec3(0.f, 0.f, 1.f), planeNormal));
// Add rotation around plane normal (rotate new x-axis to u)
const glm::vec3 rotatedRight = glm::normalize(
glm::vec3(glm::mat4_cast(q) * glm::vec4(1.f, 0.f, 0.f, 1.f))
);
q = glm::normalize(glm::rotation(rotatedRight, u)) * q;
return q;
}
int RenderablePointCloud::nAttributesPerPoint() const {
int n = 3; // position
n += hasColorData() ? 1 : 0;
n += hasSizeData() ? 1 : 0;
n += useOrientationData() ? 4 : 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<void*>(offset * sizeof(float))
);
return offset + nValues;
}
void RenderablePointCloud::updateBufferData() {
if (!_hasDataFile || _dataset.entries.empty()) {
return;
}
ZoneScopedN("Data dirty");
TracyGpuZone("Data dirty");
LDEBUG("Regenerating data");
std::vector<float> slice = createDataSlice();
int size = static_cast<int>(slice.size());
if (_vao == 0) {
glGenVertexArrays(1, &_vao);
LDEBUG(std::format("Generating Vertex Array id '{}'", _vao));
}
if (_vbo == 0) {
glGenBuffers(1, &_vbo);
LDEBUG(std::format("Generating Vertex Buffer Object id '{}'", _vbo));
}
glBindVertexArray(_vao);
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
glBufferData(GL_ARRAY_BUFFER, size * sizeof(float), slice.data(), GL_STATIC_DRAW);
const int attibsPerPoint = nAttributesPerPoint();
int offset = 0;
offset = bufferVertexAttribute("in_position", 3, attibsPerPoint, offset);
if (hasColorData()) {
offset = bufferVertexAttribute("in_colorParameter", 1, attibsPerPoint, offset);
}
if (hasSizeData()) {
offset = bufferVertexAttribute("in_scalingParameter", 1, attibsPerPoint, offset);
}
if (useOrientationData()) {
offset = bufferVertexAttribute("in_orientation", 4, attibsPerPoint, offset);
}
if (_hasSpriteTexture) {
offset = bufferVertexAttribute("in_textureLayer", 1, attibsPerPoint, offset);
}
glBindVertexArray(0);
_dataIsDirty = false;
}
void RenderablePointCloud::updateSpriteTexture() {
bool shouldUpdate = _hasSpriteTexture && _spriteTextureIsDirty;
if (!shouldUpdate) {
return;
}
ZoneScopedN("Sprite texture");
TracyGpuZone("Sprite texture");
clearTextureDataStructures();
// We also have to update the dataset, to update the texture array offsets
_dataIsDirty = true;
// 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 {
const properties::OptionProperty& property =
_colorSettings.colorMapping->dataColumn;
if (!_hasColorMapFile || property.options().empty()) {
return -1;
}
return _dataset.index(property.option().description);
}
int RenderablePointCloud::currentSizeParameterIndex() const {
const properties::OptionProperty& property =
_sizeSettings.sizeMapping->parameterOption;
if (!_hasDatavarSize || property.options().empty()) {
return -1;
}
return _dataset.index(property.option().description);
}
bool RenderablePointCloud::hasColorData() const {
const int colorParamIndex = currentColorParameterIndex();
return _hasColorMapFile && colorParamIndex >= 0;
}
bool RenderablePointCloud::hasSizeData() const {
const int sizeParamIndex = currentSizeParameterIndex();
return _hasDatavarSize && sizeParamIndex >= 0;
}
bool RenderablePointCloud::hasMultiTextureData() const {
// What datavar is the texture, if any
const int textureIdIndex = _dataset.textureDataIndex;
return _hasSpriteTexture && textureIdIndex >= 0;
}
bool RenderablePointCloud::useOrientationData() const {
return _hasOrientationData && _useRotation;
}
void RenderablePointCloud::addPositionDataForPoint(unsigned int index,
std::vector<float>& 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<float>(position[j]));
}
maxRadius = std::max(maxRadius, r);
}
void RenderablePointCloud::addColorAndSizeDataForPoint(unsigned int index,
std::vector<float>& result) const
{
const dataloader::Dataset::Entry& e = _dataset.entries[index];
if (hasColorData()) {
const int colorParamIndex = currentColorParameterIndex();
result.push_back(e.data[colorParamIndex]);
}
if (hasSizeData()) {
const int sizeParamIndex = currentSizeParameterIndex();
// @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
// Convert to diameter if data is given as radius
float multiplier = _sizeSettings.sizeMapping->isRadius ? 2.f : 1.f;
result.push_back(multiplier * e.data[sizeParamIndex]);
}
}
void RenderablePointCloud::addOrientationDataForPoint(unsigned int index,
std::vector<float>& result) const
{
const dataloader::Dataset::Entry& e = _dataset.entries[index];
glm::quat q = orientationQuaternion(e);
result.push_back(q.x);
result.push_back(q.y);
result.push_back(q.z);
result.push_back(q.w);
}
std::vector<float> RenderablePointCloud::createDataSlice() {
ZoneScoped;
if (_dataset.entries.empty()) {
return std::vector<float>();
}
double maxRadius = 0.0;
// 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<std::vector<float>> subResults = std::vector<std::vector<float>>(
!_textureArrays.empty() ? _textureArrays.size() : 1
);
// Reserve enough space for all points in each for now
for (std::vector<float>& subres : subResults) {
subres.reserve(nAttributesPerPoint() * _dataset.entries.size());
}
for (unsigned int i = 0; i < _nDataPoints; i++) {
const dataloader::Dataset::Entry& e = _dataset.entries[i];
unsigned int subresultIndex = 0;
// Default texture layer for single texture is zero
float textureLayer = 0.f;
bool useMultiTexture = (_textureMode == TextureInputMode::Multi) &&
hasMultiTextureData();
if (useMultiTexture) {
int texId = static_cast<int>(e.data[_dataset.textureDataIndex]);
size_t texIndex = _indexInDataToTextureIndex[texId];
textureLayer = static_cast<float>(
_textureIndexToArrayMap[texIndex].layer
);
subresultIndex = _textureIndexToArrayMap[texIndex].arrayId;
}
std::vector<float>& subArrayToUse = subResults[subresultIndex];
// Add position, color and size data (subclasses may compute these differently)
addPositionDataForPoint(i, subArrayToUse, maxRadius);
addColorAndSizeDataForPoint(i, subArrayToUse);
if (useOrientationData()) {
addOrientationDataForPoint(i, subArrayToUse);
}
// Texture layer
if (_hasSpriteTexture) {
subArrayToUse.push_back(static_cast<float>(textureLayer));
}
}
for (std::vector<float>& subres : subResults) {
subres.shrink_to_fit();
}
// Combine subresults, which should be in same order as texture arrays
std::vector<float> 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<int>(subResults[i].size()) / nAttributesPerPoint();
if (!_textureArrays.empty()) {
_textureArrays[i].nPoints = nVertices;
_textureArrays[i].startOffset = static_cast<GLint>(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 Tex = ghoul::opengl::Texture;
return useAlpha ? Tex::Format::RGBA : Tex::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<uint64_t>(k.resolution.x) << 32;
res += static_cast<uint64_t>(k.resolution.y) << 16;
res += k.useAlpha ? 0 : 1;
return res;
}
} // namespace openspace