Files
OpenSpace/modules/globebrowsing/src/renderableglobe.cpp
2025-05-05 14:41:05 +02:00

2684 lines
101 KiB
C++

/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2025 *
* *
* 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/globebrowsing/src/renderableglobe.h>
#include <modules/debugging/rendering/debugrenderer.h>
#include <modules/globebrowsing/src/basictypes.h>
#include <modules/globebrowsing/src/gpulayergroup.h>
#include <modules/globebrowsing/src/layer.h>
#include <modules/globebrowsing/src/layergroup.h>
#include <modules/globebrowsing/src/tileprovider/tileprovider.h>
#include <openspace/documentation/documentation.h>
#include <openspace/documentation/verifier.h>
#include <openspace/engine/globals.h>
#include <openspace/interaction/sessionrecordinghandler.h>
#include <openspace/query/query.h>
#include <openspace/rendering/renderengine.h>
#include <openspace/scene/scenegraphnode.h>
#include <openspace/scene/scene.h>
#include <openspace/util/memorymanager.h>
#include <openspace/util/spicemanager.h>
#include <openspace/util/time.h>
#include <openspace/util/updatestructures.h>
#include <ghoul/filesystem/filesystem.h>
#include <ghoul/logging/logmanager.h>
#include <ghoul/misc/memorypool.h>
#include <ghoul/misc/profiling.h>
#include <ghoul/opengl/texture.h>
#include <ghoul/opengl/textureunit.h>
#include <ghoul/opengl/openglstatecache.h>
#include <ghoul/opengl/programobject.h>
#include <ghoul/systemcapabilities/openglcapabilitiescomponent.h>
#include <numeric>
#include <queue>
#include <vector>
#if defined(__APPLE__) || (defined(__linux__) && defined(__clang__))
#include <experimental/memory_resource>
namespace std {
using namespace experimental;
} // namespace std
#else
#include <memory_resource>
#endif
namespace {
constexpr std::string_view _loggerCat = "RenderableGlobe";
// Global flags to modify the RenderableGlobe
constexpr bool LimitLevelByAvailableData = true;
constexpr bool PreformHorizonCulling = true;
// Shadow structure
struct ShadowRenderingStruct {
double xu = 0.0;
double xp = 0.0;
double rs = 0.0;
double rc = 0.0;
glm::dvec3 sourceCasterVec = glm::dvec3(0.0);
glm::dvec3 casterPositionVec = glm::dvec3(0.0);
bool isShadowing = false;
};
const openspace::globebrowsing::AABB3 CullingFrustum{
glm::vec3(-1.f, -1.f, 0.f),
glm::vec3( 1.f, 1.f, 1e35f)
};
constexpr float DefaultHeight = 0.f;
// I tried reducing this to 16, but it left the rendering with artifacts when the
// atmosphere was enabled. The best guess to the circular artifacts are due to the
// lack of resolution when a height field is enabled, leading the triangles to cut
// into the planets surface, causing issues with the ray depth of the atmosphere
// raycaster. We tried a simple solution that uses two grids and switches between
// them at a cutoff level, and I think this might still be the best solution for the
// time being. --abock 2018-10-30
constexpr int DefaultSkirtedGridSegments = 64;
constexpr int UnknownDesiredLevel = -1;
constexpr int DefaultHeightTileResolution = 512;
const openspace::globebrowsing::GeodeticPatch Coverage =
openspace::globebrowsing::GeodeticPatch(0, 0, 90, 180);
const openspace::globebrowsing::TileIndex LeftHemisphereIndex =
openspace::globebrowsing::TileIndex(0, 0, 1);
const openspace::globebrowsing::TileIndex RightHemisphereIndex =
openspace::globebrowsing::TileIndex(1, 0, 1);
constexpr openspace::properties::Property::PropertyInfo ShowChunkEdgeInfo = {
"ShowChunkEdges",
"Show chunk edges",
"Shows the borders between chunks in a red highlight.",
openspace::properties::Property::Visibility::Developer
};
constexpr openspace::properties::Property::PropertyInfo LevelProjectedAreaInfo = {
"LevelByProjectedAreaElseDistance",
"Level by projected area (else distance)",
"If true, the tile level is determined by the area projected on screen. If "
"false, the distance to the center of the tile is used instead.",
openspace::properties::Property::Visibility::Developer
};
constexpr openspace::properties::Property::PropertyInfo PerformFrustumCullingInfo = {
"PerformFrustumCulling",
"Perform frustum culling",
"If this value is set to true, frustum culling will be performed.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo ResetTileProviderInfo = {
"ResetTileProviders",
"Reset tile providers",
"Reset all tile provides for the globe and reload the data.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo ModelSpaceRenderingInfo = {
"ModelSpaceRenderingCutoffLevel",
"Model Space Rendering Cutoff Level",
"The tile level that is used as the cut off between rendering tiles using the "
"globe model rendering vs the flat in-game rendering method. The value is a "
"trade-off between not having precision errors in the rendering and representing "
"a tile as flat or curved.",
openspace::properties::Property::Visibility::Developer
};
constexpr openspace::properties::Property::PropertyInfo DynamicLodIterationCountInfo =
{
"DynamicLodIterationCount",
"Data availability checks before LOD factor impact",
"The number of checks that have to fail/succeed in a row before the dynamic "
"level-of-detail adjusts the actual level-of-detail up or down during a session "
"recording.",
openspace::properties::Property::Visibility::Developer
};
constexpr openspace::properties::Property::PropertyInfo PerformShadingInfo = {
"PerformShading",
"Perform shading",
"Specifies whether the planet should be shaded by the primary light source or "
"not. If disabled, all parts of the planet are illuminated. Note that if the "
"globe has a corresponding atmosphere, there is a separate setting to control "
"the shadowing induced by the atmosphere.",
openspace::properties::Property::Visibility::NoviceUser
};
constexpr openspace::properties::Property::PropertyInfo AccurateNormalsInfo = {
"UseAccurateNormals",
"Use Accurate Normals",
"Determines whether higher-accuracy normals should be used in the rendering. "
"These normals are calculated based on the height field information and are thus "
"only available if the planet has a height map layer.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo AmbientIntensityInfo = {
"AmbientIntensity",
"Ambient Intensity",
"The intensity factor for the ambient light used for light shading.",
openspace::properties::Property::Visibility::User
};
constexpr openspace::properties::Property::PropertyInfo LightSourceNodeInfo = {
"LightSourceNode",
"Light Source",
"The identifier of a scene graph node that should be used as the source of "
"illumination for the globe. If not specified, the solar system's Sun is used.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo EclipseInfo = {
"Eclipse",
"Eclipse",
"Enables/Disables Eclipse shadows on this globe.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo EclipseHardShadowsInfo = {
"EclipseHardShadows",
"Eclipse Hard Shadows",
"Enables the rendering of eclipse shadows using hard shadows.",
openspace::properties::Property::Visibility::User
};
constexpr openspace::properties::Property::PropertyInfo ShadowMappingInfo = {
"ShadowMapping",
"Shadow Mapping",
"Enables shadow mapping algorithm. Used by renderable rings, too.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo RenderAtDistanceInfo = {
"RenderAtDistance",
"Render at Distance",
"Tells the rendering engine not to perform distance based performance culling "
"for this globe. Turning this property on will let the globe to be seen at far "
"away distances when normally it would be hidden.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo ZFightingPercentageInfo = {
"ZFightingPercentage",
"Z-Fighting Percentage",
"The percentage of the correct distance to the surface being shadowed.",
openspace::properties::Property::Visibility::Developer
};
constexpr openspace::properties::Property::PropertyInfo NumberShadowSamplesInfo = {
"NumberShadowSamples",
"Number of Shadow Samples",
"The number of samples used during shadow mapping calculation (Percentage Closer "
"Filtering).",
openspace::properties::Property::Visibility::Developer
};
constexpr openspace::properties::Property::PropertyInfo TargetLodScaleFactorInfo = {
"TargetLodScaleFactor",
"Target Level of Detail Scale Factor",
"Determines the targeted level-of-detail of the tiles for this globe. A higher "
"value means that the tiles rendered are a higher resolution for the same "
"distance of the camera to the planet.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo CurrentLodScaleFactorInfo = {
"CurrentLodScaleFactor",
"Current Level of Detail Scale Factor (Read Only)",
"The currently used scale factor whose target value is deteremined by "
"'TargetLodScaleFactor'.",
openspace::properties::Property::Visibility::AdvancedUser
};
constexpr openspace::properties::Property::PropertyInfo OrenNayarRoughnessInfo = {
"OrenNayarRoughness",
"orenNayarRoughness",
"The roughness factor that is used for the Oren-Nayar lighting mode.",
openspace::properties::Property::Visibility::Developer
};
constexpr openspace::properties::Property::PropertyInfo NActiveLayersInfo = {
"NActiveLayers",
"Number of active layers",
"The number of currently active layers. If this value reaches the maximum, "
"bad things will happen.",
openspace::properties::Property::Visibility::User
};
struct [[codegen::Dictionary(RenderableGlobe)]] Parameters {
// The radii for this planet. If only one value is given, all three radii are
// set to that value.
std::optional<std::variant<glm::dvec3, double>> radii;
// [[codegen::verbatim(PerformShadingInfo.description)]]
std::optional<bool> performShading;
// [[codegen::verbatim(AccurateNormalsInfo.description)]]
std::optional<bool> useAccurateNormals;
// [[codegen::verbatim(AmbientIntensityInfo.description)]]
std::optional<float> ambientIntensity;
// [[codegen::verbatim(LightSourceNodeInfo.description)]]
std::optional<std::string> lightSourceNode;
// [[codegen::verbatim(RenderAtDistanceInfo.description)]]
std::optional<bool> renderAtDistance;
// [[codegen::verbatim(TargetLodScaleFactorInfo.description)]]
std::optional<float> targetLodScaleFactor;
// [[codegen::verbatim(OrenNayarRoughnessInfo.description)]]
std::optional<float> orenNayarRoughness;
enum class [[codegen::map(openspace::globebrowsing::layers::Group::ID)]] Group {
HeightLayers,
ColorLayers,
Overlays,
NightLayers,
WaterMasks,
};
// A list of layers that should be added to the globe.
std::optional<std::map<Group, std::vector<ghoul::Dictionary>>> layers
[[codegen::reference("globebrowsing_layer")]];
// Specifies information about planetary labels that can be rendered on the
// object's surface.
std::optional<ghoul::Dictionary> labels
[[codegen::reference("globebrowsing_globelabelscomponent")]];
struct ShadowGroup {
struct Source {
std::string name;
double radius;
};
// A list of objects (light sources) that may cause shadows from the provided
// list of shadow casting objects.
std::vector<Source> sources;
struct Caster {
std::string name;
double radius;
};
// A list of potential shadow casters.
std::vector<Caster> casters;
};
// Information about any object that might cause shadows to appear on the
// globe.
std::optional<ShadowGroup> shadowGroup;
// Details about the rings of the globe, if it has any.
std::optional<ghoul::Dictionary> rings
[[codegen::reference("globebrowsing_rings_component")]];
std::optional<ghoul::Dictionary> shadows
[[codegen::reference("globebrowsing_shadows_component")]];
};
#include "renderableglobe_codegen.cpp"
} // namespace
using namespace openspace::properties;
namespace openspace::globebrowsing {
namespace {
bool isLeaf(const Chunk& cn) {
return cn.children[0] == nullptr;
}
const Chunk& findChunkNode(const Chunk& node, const Geodetic2& location) {
const Chunk* n = &node;
while (!isLeaf(*n)) {
const Geodetic2 center = n->surfacePatch.center();
int index = 0;
if (center.lon < location.lon) {
index++;
}
if (location.lat < center.lat) {
index++;
index++;
}
n = n->children[static_cast<Quad>(index)];
}
return *n;
}
#if defined(__APPLE__) || (defined(__linux__) && defined(__clang__))
using ChunkTileVector = std::vector<std::pair<ChunkTile, const LayerRenderSettings*>>;
#else
using ChunkTileVector =
std::pmr::vector<std::pair<ChunkTile, const LayerRenderSettings*>>;
#endif
ChunkTileVector tilesAndSettingsUnsorted(const LayerGroup& layerGroup,
const TileIndex& tileIndex)
{
ZoneScoped;
#if defined(__APPLE__) || (defined(__linux__) && defined(__clang__))
ChunkTileVector tilesAndSettings;
#else
ChunkTileVector tilesAndSettings(&global::memoryManager->TemporaryMemory);
#endif
for (Layer* layer : layerGroup.activeLayers()) {
if (layer->tileProvider()) {
tilesAndSettings.emplace_back(
layer->tileProvider()->chunkTile(tileIndex),
&layer->renderSettings()
);
}
}
std::reverse(tilesAndSettings.begin(), tilesAndSettings.end());
return tilesAndSettings;
}
BoundingHeights boundingHeightsForChunk(const Chunk& chunk, const LayerManager& lm) {
ZoneScoped;
using ChunkTileSettingsPair = std::pair<ChunkTile, const LayerRenderSettings*>;
BoundingHeights boundingHeights { 0.f, 0.f, false, true };
// The raster of a height map is the first one. We assume that the height map is
// a single raster image. If it is not we will just use the first raster
// (that is channel 0).
const size_t HeightChannel = 0;
const LayerGroup& heightmaps = lm.layerGroup(layers::Group::ID::HeightLayers);
const ChunkTileVector chunkTileSettingPairs = tilesAndSettingsUnsorted(
heightmaps,
chunk.tileIndex
);
bool lastHadMissingData = true;
for (const ChunkTileSettingsPair& chunkTileSettingsPair : chunkTileSettingPairs) {
const ChunkTile& chunkTile = chunkTileSettingsPair.first;
const LayerRenderSettings* settings = chunkTileSettingsPair.second;
const bool goodTile = (chunkTile.tile.status == Tile::Status::OK);
const bool hasTileMetaData = chunkTile.tile.metaData.has_value();
if (goodTile && hasTileMetaData) {
const TileMetaData& tileMetaData = *chunkTile.tile.metaData;
const float minValue = settings->performLayerSettings(
tileMetaData.minValues[HeightChannel]
);
const float maxValue = settings->performLayerSettings(
tileMetaData.maxValues[HeightChannel]
);
if (!boundingHeights.available) {
if (tileMetaData.hasMissingData[HeightChannel]) {
boundingHeights.min = std::min(DefaultHeight, minValue);
boundingHeights.max = std::max(DefaultHeight, maxValue);
}
else {
boundingHeights.min = minValue;
boundingHeights.max = maxValue;
}
boundingHeights.available = true;
}
else {
boundingHeights.min = std::min(boundingHeights.min, minValue);
boundingHeights.max = std::max(boundingHeights.max, maxValue);
}
lastHadMissingData = tileMetaData.hasMissingData[HeightChannel];
}
else if (chunkTile.tile.status == Tile::Status::Unavailable) {
boundingHeights.tileOK = false;
}
// Allow for early termination
if (!lastHadMissingData) {
break;
}
}
return boundingHeights;
}
bool colorAvailableForChunk(const Chunk& chunk, const LayerManager& lm) {
ZoneScoped;
const LayerGroup& colorLayers = lm.layerGroup(layers::Group::ID::ColorLayers);
for (Layer* lyr : colorLayers.activeLayers()) {
if (lyr->tileProvider()) {
const ChunkTile t = lyr->tileProvider()->chunkTile(chunk.tileIndex);
if (t.tile.status == Tile::Status::Unavailable) {
return false;
}
}
}
return true;
}
std::array<glm::dvec4, 8> boundingCornersForChunk(const Chunk& chunk,
const Ellipsoid& ellipsoid,
const BoundingHeights& heights)
{
ZoneScoped;
// assume worst case
const double patchCenterRadius = ellipsoid.maximumRadius();
const double maxCenterRadius = patchCenterRadius + heights.max;
const Geodetic2 halfSize = chunk.surfacePatch.halfSize();
// As the patch is curved, the maximum height offsets at the corners must be long
// enough to cover large enough to cover a heights.max at the center of the
// patch.
// Approximating scaleToCoverCenter by assuming the latitude and longitude angles
// of "halfSize" are equal to the angles they create from the center of the
// globe to the patch corners. This is true for the longitude direction when
// the ellipsoid can be approximated as a sphere and for the latitude for patches
// close to the equator. Close to the pole this will lead to a bigger than needed
// value for scaleToCoverCenter. However, this is a simple calculation and a good
// Approximation.
const double y1 = tan(halfSize.lat);
const double y2 = tan(halfSize.lon);
const double scaleToCoverCenter = sqrt(1 + pow(y1, 2) + pow(y2, 2));
const double maxCornerHeight = maxCenterRadius * scaleToCoverCenter -
patchCenterRadius;
const bool chunkIsNorthOfEquator = chunk.surfacePatch.isNorthern();
// The minimum height offset, however, we can simply
const double minCornerHeight = heights.min;
std::array<glm::dvec4, 8> corners;
const double latCloseToEquator = chunk.surfacePatch.edgeLatitudeNearestEquator();
const Geodetic3 p1Geodetic = {
{ latCloseToEquator, chunk.surfacePatch.minLon() },
maxCornerHeight
};
const Geodetic3 p2Geodetic = {
{ latCloseToEquator, chunk.surfacePatch.maxLon() },
maxCornerHeight
};
const glm::vec3 p1 = ellipsoid.cartesianPosition(p1Geodetic);
const glm::vec3 p2 = ellipsoid.cartesianPosition(p2Geodetic);
const glm::vec3 p = 0.5f * (p1 + p2);
const Geodetic2 pGeodetic = ellipsoid.cartesianToGeodetic2(p);
const double latDiff = latCloseToEquator - pGeodetic.lat;
for (size_t i = 0; i < 8; i++) {
const Quad q = static_cast<Quad>(i % 4);
const double cornerHeight = i < 4 ? minCornerHeight : maxCornerHeight;
Geodetic3 cornerGeodetic = { chunk.surfacePatch.corner(q), cornerHeight };
const bool cornerIsNorthern = !((i / 2) % 2);
const bool cornerCloseToEquator = chunkIsNorthOfEquator ^ cornerIsNorthern;
if (cornerCloseToEquator) {
cornerGeodetic.geodetic2.lat += latDiff;
}
corners[i] = glm::dvec4(ellipsoid.cartesianPosition(cornerGeodetic), 1.0);
}
return corners;
}
void expand(AABB3& bb, const glm::vec3& p) {
bb.min = glm::min(bb.min, p);
bb.max = glm::max(bb.max, p);
}
bool intersects(const AABB3& bb, const AABB3& o) {
return (bb.min.x <= o.max.x) && (o.min.x <= bb.max.x)
&& (bb.min.y <= o.max.y) && (o.min.y <= bb.max.y)
&& (bb.min.z <= o.max.z) && (o.min.z <= bb.max.z);
}
/**
* Calculates the direction towards the local light source. If \p illumination is a
* `nullptr`, it is interpreted to be (0,0,0)
*/
glm::dvec3 directionToLightSource(const glm::dvec3& pos, SceneGraphNode* lightSource) {
// For the lighting we use the provided node, or the Sun
SceneGraphNode* node =
lightSource ? lightSource : sceneGraph()->sceneGraphNode("Sun");
if (node) {
return node->worldPosition() - pos;
}
else {
const glm::dvec3 dir = length(pos) > 0.0 ? glm::normalize(-pos) : glm::dvec3(0.0);
return dir;
}
}
} // namespace
Chunk::Chunk(const TileIndex& ti)
: tileIndex(ti)
, surfacePatch(ti)
, status(Status::DoNothing)
{}
documentation::Documentation RenderableGlobe::Documentation() {
return codegen::doc<Parameters>("globebrowsing_renderableglobe");
}
RenderableGlobe::RenderableGlobe(const ghoul::Dictionary& dictionary)
: Renderable(dictionary)
, _performShading(PerformShadingInfo, true)
, _useAccurateNormals(AccurateNormalsInfo, false)
, _ambientIntensity(AmbientIntensityInfo, 0.05f, 0.f, 1.f)
, _lightSourceNodeName(LightSourceNodeInfo)
, _renderAtDistance(RenderAtDistanceInfo, false)
, _eclipseShadowsEnabled(EclipseInfo, false)
, _eclipseHardShadows(EclipseHardShadowsInfo, false)
, _targetLodScaleFactor(TargetLodScaleFactorInfo, 15.f, 1.f, 50.f)
, _currentLodScaleFactor(CurrentLodScaleFactorInfo, 15.f, 1.f, 50.f)
, _orenNayarRoughness(OrenNayarRoughnessInfo, 0.f, 0.f, 1.f)
, _nActiveLayers(NActiveLayersInfo, 0, 0, OpenGLCap.maxTextureUnits() / 3)
, _debugProperties({
BoolProperty(ShowChunkEdgeInfo, false),
BoolProperty(LevelProjectedAreaInfo, true),
TriggerProperty(ResetTileProviderInfo),
BoolProperty(PerformFrustumCullingInfo, true),
IntProperty(ModelSpaceRenderingInfo, 14, 1, 22),
IntProperty(DynamicLodIterationCountInfo, 16, 4, 128)
})
, _debugPropertyOwner({ "Debug" })
, _shadowMappingProperties({
BoolProperty(ShadowMappingInfo, false),
FloatProperty(ZFightingPercentageInfo, 0.995f, 0.000001f, 1.f),
IntProperty(NumberShadowSamplesInfo, 5, 1, 7)
})
, _shadowMappingPropertyOwner({ "ShadowMapping", "Shadow Mapping"})
, _grid(DefaultSkirtedGridSegments, DefaultSkirtedGridSegments)
, _leftRoot(Chunk(LeftHemisphereIndex))
, _rightRoot(Chunk(RightHemisphereIndex))
{
const Parameters p = codegen::bake<Parameters>(dictionary);
_currentLodScaleFactor.setReadOnly(true);
// Read the radii in to its own dictionary
if (p.radii.has_value()) {
if (std::holds_alternative<glm::dvec3>(*p.radii)) {
_ellipsoid = Ellipsoid(std::get<glm::dvec3>(*p.radii));
setBoundingSphere(_ellipsoid.maximumRadius());
}
else if (std::holds_alternative<double>(*p.radii)) {
const double radius = std::get<double>(*p.radii);
_ellipsoid = Ellipsoid({ radius, radius, radius });
setBoundingSphere(_ellipsoid.maximumRadius());
}
else {
throw ghoul::MissingCaseException();
}
}
// For globes, the interaction sphere is always the same as the bounding sphere
setInteractionSphere(boundingSphere());
// Init layer manager
std::map<layers::Group::ID, std::vector<ghoul::Dictionary>> layers;
if (p.layers.has_value()) {
for (const auto& [key, value] : *p.layers) {
layers[codegen::map<layers::Group::ID>(key)] = value;
}
}
_layerManager.initialize(layers);
addProperty(Fadeable::_opacity);
_performShading = p.performShading.value_or(_performShading);
addProperty(_performShading);
_useAccurateNormals = p.useAccurateNormals.value_or(_useAccurateNormals);
addProperty(_useAccurateNormals);
_renderAtDistance = p.renderAtDistance.value_or(_renderAtDistance);
addProperty(_renderAtDistance);
_ambientIntensity = p.ambientIntensity.value_or(_ambientIntensity);
addProperty(_ambientIntensity);
_lightSourceNodeName.onChange([this]() {
if (_lightSourceNodeName.value().empty()) {
_lightSourceNode = nullptr;
return;
}
SceneGraphNode* n = sceneGraphNode(_lightSourceNodeName);
if (!n) {
LERROR(std::format(
"Could not find node '{}' as illumination for '{}'",
_lightSourceNodeName.value(), identifier()
));
}
else {
_lightSourceNode = n;
}
});
_lightSourceNodeName = p.lightSourceNode.value_or("");
addProperty(_lightSourceNodeName);
if (p.shadowGroup.has_value()) {
std::vector<Ellipsoid::ShadowConfiguration> shadowConfArray;
for (const Parameters::ShadowGroup::Source& source : p.shadowGroup->sources) {
for (const Parameters::ShadowGroup::Caster& caster : p.shadowGroup->casters) {
Ellipsoid::ShadowConfiguration sc;
sc.source = std::pair<std::string, double>(source.name, source.radius);
sc.caster = std::pair<std::string, double>(caster.name, caster.radius);
shadowConfArray.push_back(sc);
}
}
_ellipsoid.setShadowConfigurationArray(shadowConfArray);
// TODO: Missing value from parameters
addProperty(_eclipseShadowsEnabled);
addProperty(_eclipseHardShadows);
}
_shadowMappingPropertyOwner.addProperty(_shadowMappingProperties.shadowMapping);
_shadowMappingPropertyOwner.addProperty(_shadowMappingProperties.zFightingPercentage);
_shadowMappingPropertyOwner.addProperty(_shadowMappingProperties.nShadowSamples);
_shadowMappingProperties.nShadowSamples.onChange([this]() {
_shadersNeedRecompilation = true;
});
addPropertySubOwner(_shadowMappingPropertyOwner);
_targetLodScaleFactor = p.targetLodScaleFactor.value_or(_targetLodScaleFactor);
_targetLodScaleFactor.onChange([this]() {
const float sf = _targetLodScaleFactor;
_currentLodScaleFactor = sf;
_lodScaleFactorDirty = true;
});
addProperty(_targetLodScaleFactor);
addProperty(_currentLodScaleFactor);
_orenNayarRoughness = p.orenNayarRoughness.value_or(_orenNayarRoughness);
addProperty(_orenNayarRoughness);
_nActiveLayers.setReadOnly(true);
addProperty(_nActiveLayers);
_debugPropertyOwner.addProperty(_debugProperties.showChunkEdges);
_debugPropertyOwner.addProperty(_debugProperties.levelByProjectedAreaElseDistance);
_debugProperties.resetTileProviders.onChange([&]() { _resetTileProviders = true; });
_debugPropertyOwner.addProperty(_debugProperties.resetTileProviders);
_debugPropertyOwner.addProperty(_debugProperties.performFrustumCulling);
_debugPropertyOwner.addProperty(_debugProperties.modelSpaceRenderingCutoffLevel);
_debugPropertyOwner.addProperty(_debugProperties.dynamicLodIterationCount);
addPropertySubOwner(_debugPropertyOwner);
auto notifyShaderRecompilation = [this]() {
_shadersNeedRecompilation = true;
};
_useAccurateNormals.onChange(notifyShaderRecompilation);
_eclipseShadowsEnabled.onChange(notifyShaderRecompilation);
_eclipseHardShadows.onChange(notifyShaderRecompilation);
_performShading.onChange(notifyShaderRecompilation);
_debugProperties.showChunkEdges.onChange(notifyShaderRecompilation);
_shadowMappingProperties.shadowMapping.onChange(notifyShaderRecompilation);
_layerManager.onChange([this](Layer* l) {
_shadersNeedRecompilation = true;
_chunkCornersDirty = true;
_nLayersIsDirty = true;
_lastChangedLayer = l;
});
addPropertySubOwner(_layerManager);
_globalChunkBuffer.resize(2048);
_localChunkBuffer.resize(2048);
_traversalMemory.resize(512);
_labelsDictionary = p.labels.value_or(_labelsDictionary);
if (!_labelsDictionary.isEmpty()) {
// Fading of the labels should also depend on the fading of the globe
_globeLabelsComponent.setParentFadeable(this);
addPropertySubOwner(_globeLabelsComponent);
}
// Init geojson manager
_geoJsonManager.initialize(this);
addPropertySubOwner(_geoJsonManager);
// Components
if (p.rings.has_value()) {
_ringsComponent = std::make_unique<RingsComponent>(*p.rings);
_ringsComponent->setParentFadeable(this);
_ringsComponent->initialize();
addPropertySubOwner(_ringsComponent.get());
}
if (p.shadows.has_value()) {
_shadowComponent = std::make_unique<ShadowComponent>(*p.shadows);
_shadowComponent->initialize();
addPropertySubOwner(_shadowComponent.get());
_shadowMappingProperties.shadowMapping = true;
}
// Use a secondary renderbin for labels, and other things that we want to be able to
// render with transparency, on top of the globe, after the atmosphere step
_secondaryRenderBin = RenderBin::PostDeferredTransparent;
}
void RenderableGlobe::initializeGL() {
if (!_labelsDictionary.isEmpty()) {
_globeLabelsComponent.initialize(_labelsDictionary, this);
}
_layerManager.update();
_grid.initializeGL();
if (_ringsComponent) {
_ringsComponent->initializeGL();
}
if (_shadowComponent) {
_shadowComponent->initializeGL();
}
// Recompile the shaders directly so that it is not done the first time the render
// function is called.
recompileShaders();
}
void RenderableGlobe::deinitialize() {
_layerManager.deinitialize();
}
void RenderableGlobe::deinitializeGL() {
if (_localRenderer.program) {
global::renderEngine->removeRenderProgram(_localRenderer.program.get());
_localRenderer.program = nullptr;
}
if (_globalRenderer.program) {
global::renderEngine->removeRenderProgram(_globalRenderer.program.get());
_globalRenderer.program = nullptr;
}
_geoJsonManager.deinitializeGL();
_grid.deinitializeGL();
if (_ringsComponent) {
_ringsComponent->deinitializeGL();
}
if (_shadowComponent) {
_shadowComponent->deinitializeGL();
}
}
bool RenderableGlobe::isReady() const {
return true;
}
void RenderableGlobe::render(const RenderData& data, RendererTasks& rendererTask) {
const double distanceToCamera = glm::distance(
data.camera.positionVec3(),
data.modelTransform.translation
);
// This distance will be enough to render the globe as one pixel if the field of
// view is 'fov' radians and the screen resolution is 'res' pixels.
//constexpr double fov = 2 * glm::pi<double>() / 6; // 60 degrees
//constexpr double tfov = tan(fov / 2.0); // doesn't work unfortunately
constexpr double tfov = 0.5773502691896257;
constexpr int res = 2880;
const double distance = res * boundingSphere() / tfov;
if ((distanceToCamera < distance) || (_renderAtDistance)) {
try {
if (_shadowComponent && _shadowComponent->isEnabled()) {
// Set matrices and other GL states
const RenderData lightRenderData(_shadowComponent->begin(data));
glDisable(GL_BLEND);
// Render from light source point of view
renderChunks(lightRenderData, rendererTask, {}, true);
if (_ringsComponent && _ringsComponent->isEnabled() &&
_ringsComponent->isVisible())
{
_ringsComponent->draw(
lightRenderData,
RingsComponent::RenderPass::GeometryOnly
);
}
glEnable(GL_BLEND);
_shadowComponent->end();
// Render again from original point of view
renderChunks(data, rendererTask, _shadowComponent->shadowMapData());
if (_ringsComponent && _ringsComponent->isEnabled() &&
_ringsComponent->isVisible())
{
_ringsComponent->draw(
data,
RingsComponent::RenderPass::GeometryAndShading,
_shadowComponent->shadowMapData()
);
}
}
else {
renderChunks(data, rendererTask);
if (_ringsComponent && _ringsComponent->isEnabled() &&
_ringsComponent->isVisible())
{
_ringsComponent->draw(
data,
RingsComponent::RenderPass::GeometryAndShading
);
}
}
}
catch (const ghoul::opengl::TextureUnit::TextureUnitError&) {
const std::string& layer =
_lastChangedLayer ? _lastChangedLayer->guiName() : "";
LWARNINGC(
guiName(),
layer.empty() ?
"Too many layers enabled" :
"Too many layers enabled, disabling layer: " + layer
);
// We bailed out in the middle of the rendering, so some TextureUnits are
// still bound and we would fail in some next render function for sure
for (GPULayerGroup& l : _globalRenderer.gpuLayerGroups) {
l.deactivate();
}
for (GPULayerGroup& l : _localRenderer.gpuLayerGroups) {
l.deactivate();
}
if (_lastChangedLayer) {
_lastChangedLayer->setEnabled(false);
}
}
}
_lastChangedLayer = nullptr;
// Reset
global::renderEngine->openglStateCache().resetBlendState();
global::renderEngine->openglStateCache().resetDepthState();
}
void RenderableGlobe::renderSecondary(const RenderData& data, RendererTasks&) {
try {
_globeLabelsComponent.draw(data);
}
catch (const ghoul::opengl::TextureUnit::TextureUnitError& e) {
LERROR(std::format("Error on drawing globe labels '{}'", e.message));
}
if (_geoJsonManager.isReady()) {
_geoJsonManager.render(data);
}
}
void RenderableGlobe::update(const UpdateData& data) {
ZoneScoped;
if (_localRenderer.program && _localRenderer.program->isDirty()) [[unlikely]] {
_localRenderer.program->rebuildFromFile();
_localRenderer.program->setUniform("xSegments", _grid.xSegments);
ghoul::opengl::updateUniformLocations(
*_localRenderer.program,
_localRenderer.uniformCache
);
}
if (_globalRenderer.program && _globalRenderer.program->isDirty()) [[unlikely]] {
_globalRenderer.program->rebuildFromFile();
_globalRenderer.program->setUniform("xSegments", _grid.xSegments);
// Ellipsoid Radius (Model Space)
_globalRenderer.program->setUniform(
"radiiSquared",
glm::vec3(_ellipsoid.radii() * _ellipsoid.radii())
);
ghoul::opengl::updateUniformLocations(
*_globalRenderer.program,
_globalRenderer.uniformCache
);
}
double bs = _ellipsoid.maximumRadius() * glm::compMax(data.modelTransform.scale);
if (_ringsComponent) {
const double ringSize = _ringsComponent->size();
if (ringSize > bs) {
bs = ringSize;
}
}
setBoundingSphere(bs);
setInteractionSphere(bs);
const glm::dmat4 translation =
glm::translate(glm::dmat4(1.0), data.modelTransform.translation);
const glm::dmat4 rotation = glm::dmat4(data.modelTransform.rotation);
const glm::dmat4 scaling = glm::scale(glm::dmat4(1.0), data.modelTransform.scale);
_cachedModelTransform = translation * rotation * scaling;
_cachedInverseModelTransform = glm::inverse(_cachedModelTransform);
if (_resetTileProviders) [[unlikely]] {
_layerManager.reset();
_resetTileProviders = false;
}
if (_ringsComponent) {
_ringsComponent->update(data);
}
if (_shadowComponent) {
_shadowComponent->update(data);
}
// abock (2020-08-21)
// This is a bit nasty every since we removed the second update call from the render
// loop. The problem is when we enable a new layer, the dirty flags above will be set
// to true, but the update method hasn't run yet. So we need to move the layerManager
// update method to the render function call, but we don't want to *actually* update
// the layers once per render call; hence this nasty dirty flag
//
// How it is without in the frame when we enable a layer:
//
// RenderableGlobe::update() // updated with the old number of layers
// // Lua script to enable layer is executed and sets the dirty flags
// RenderableGlobe::render() // rendering with the new number of layers but the
// // LayerManager hasn't updated yet :o
_layerManagerDirty = true;
_geoJsonManager.update();
}
bool RenderableGlobe::renderedWithDesiredData() const {
return _allChunksAvailable;
}
const LayerManager& RenderableGlobe::layerManager() const {
return _layerManager;
}
LayerManager& RenderableGlobe::layerManager() {
return _layerManager;
}
const GeoJsonManager& RenderableGlobe::geoJsonManager() const {
return _geoJsonManager;
}
GeoJsonManager& RenderableGlobe::geoJsonManager() {
return _geoJsonManager;
}
Ellipsoid RenderableGlobe::ellipsoid() const {
return _ellipsoid;
}
const glm::dmat4& RenderableGlobe::modelTransform() const {
return _cachedModelTransform;
}
void RenderableGlobe::invalidateShader() {
_shadersNeedRecompilation = true;
}
//////////////////////////////////////////////////////////////////////////////////////////
// Rendering code
//////////////////////////////////////////////////////////////////////////////////////////
void RenderableGlobe::renderChunks(const RenderData& data, RendererTasks&,
const ShadowComponent::ShadowMapData& shadowData,
bool renderGeomOnly)
{
ZoneScoped;
if (_layerManagerDirty) [[unlikely]] {
_layerManager.update();
_layerManagerDirty = false;
}
if (_nLayersIsDirty) [[unlikely]] {
std::array<LayerGroup*, LayerManager::NumLayerGroups> lgs =
_layerManager.layerGroups();
_nActiveLayers = std::accumulate(
lgs.begin(),
lgs.end(),
0,
[](int lhs, LayerGroup* lg) {
return lhs + static_cast<int>(lg->activeLayers().size());
}
);
_nLayersIsDirty = false;
}
if (_shadersNeedRecompilation) [[unlikely]] {
recompileShaders();
}
//
// Setting frame-const uniforms that are not view dependent
//
if (_layerManager.hasAnyBlendingLayersEnabled()) {
if (_lodScaleFactorDirty) [[unlikely]] {
const float dsf = static_cast<float>(
_currentLodScaleFactor * _ellipsoid.minimumRadius()
);
// We are setting the setIgnoreUniformLocationError as it is not super trivial
// and brittle to figure out apriori whether the uniform was optimized away
// or not. It should be something long the lines of:
// (hasBlendingLayers && (has multiple different layer types)) || (uses
// accurate shading) [maybe]
// it's easier to just try to set it and ignore the error, since this is only
// happening on a few frames
using IgnoreError = ghoul::opengl::ProgramObject::IgnoreError;
_globalRenderer.program->setIgnoreUniformLocationError(IgnoreError::Yes);
_globalRenderer.program->setUniform("distanceScaleFactor", dsf);
_globalRenderer.program->setIgnoreUniformLocationError(IgnoreError::No);
_localRenderer.program->setIgnoreUniformLocationError(IgnoreError::Yes);
_localRenderer.program->setUniform("distanceScaleFactor", dsf);
_localRenderer.program->setIgnoreUniformLocationError(IgnoreError::No);
_lodScaleFactorDirty = false;
}
}
if (_performShading) {
const float onr = _orenNayarRoughness;
_localRenderer.program->setUniform("orenNayarRoughness", onr);
_globalRenderer.program->setUniform("orenNayarRoughness", onr);
const float amb = _ambientIntensity;
_localRenderer.program->setUniform("ambientIntensity", amb);
_globalRenderer.program->setUniform("ambientIntensity", amb);
}
_localRenderer.program->setUniform("opacity", opacity());
_globalRenderer.program->setUniform("opacity", opacity());
if (_globalRenderer.updatedSinceLastCall) {
const std::array<LayerGroup*, LayerManager::NumLayerGroups>& layerGroups =
_layerManager.layerGroups();
for (size_t i = 0; i < layerGroups.size(); i++) {
_globalRenderer.gpuLayerGroups[i].bind(
*_globalRenderer.program,
*layerGroups[i]
);
}
const float dsf = static_cast<float>(
_currentLodScaleFactor * _ellipsoid.minimumRadius()
);
using IgnoreError = ghoul::opengl::ProgramObject::IgnoreError;
_globalRenderer.program->setIgnoreUniformLocationError(IgnoreError::Yes);
_globalRenderer.program->setUniform("distanceScaleFactor", dsf);
_globalRenderer.program->setIgnoreUniformLocationError(IgnoreError::No);
_globalRenderer.updatedSinceLastCall = false;
}
if (_localRenderer.updatedSinceLastCall) {
const std::array<LayerGroup*, LayerManager::NumLayerGroups>& layerGroups =
_layerManager.layerGroups();
for (size_t i = 0; i < layerGroups.size(); i++) {
_localRenderer.gpuLayerGroups[i].bind(
*_localRenderer.program,
*layerGroups[i]
);
}
const float dsf = static_cast<float>(
_currentLodScaleFactor * _ellipsoid.minimumRadius()
);
using IgnoreError = ghoul::opengl::ProgramObject::IgnoreError;
_localRenderer.program->setIgnoreUniformLocationError(IgnoreError::Yes);
_localRenderer.program->setUniform("distanceScaleFactor", dsf);
_localRenderer.program->setIgnoreUniformLocationError(IgnoreError::No);
_localRenderer.updatedSinceLastCall = false;
}
// Calculate the MVP matrix
const glm::dmat4& viewTransform = data.camera.combinedViewMatrix();
const glm::dmat4 vp = glm::dmat4(data.camera.sgctInternal.projectionMatrix()) *
viewTransform;
const glm::dmat4 mvp = vp * _cachedModelTransform;
_allChunksAvailable = true;
updateChunkTree(_leftRoot, data, mvp);
updateChunkTree(_rightRoot, data, mvp);
_chunkCornersDirty = false;
_iterationsOfAvailableData =
(_allChunksAvailable ? _iterationsOfAvailableData + 1 : 0);
_iterationsOfUnavailableData =
(_allChunksAvailable ? 0 : _iterationsOfUnavailableData + 1);
//
// Setting uniforms that don't change between chunks but are view dependent
//
// Global shader
if (_layerManager.hasAnyBlendingLayersEnabled()) {
// Calculations are done in the reference frame of the globe. Hence, the
// camera position needs to be transformed with the inverse model matrix
const glm::dvec3 cameraPosition = glm::dvec3(
_cachedInverseModelTransform * glm::dvec4(data.camera.positionVec3(), 1.0)
);
using IgnoreError = ghoul::opengl::ProgramObject::IgnoreError;
_globalRenderer.program->setIgnoreUniformLocationError(IgnoreError::Yes);
// The cameraPosition is not used if a globe only has a solid color, but it would
// be costlier to figure that out and will only trigger rarely, so instead we just
// ignore the location error
_globalRenderer.program->setUniform("cameraPosition", glm::vec3(cameraPosition));
_globalRenderer.program->setIgnoreUniformLocationError(IgnoreError::No);
}
const glm::mat4 modelViewTransform = glm::mat4(viewTransform * _cachedModelTransform);
const glm::mat4 modelViewProjectionTransform =
data.camera.sgctInternal.projectionMatrix() * modelViewTransform;
// Upload the uniform variables
_globalRenderer.program->setUniform(
"modelViewProjectionTransform",
modelViewProjectionTransform
);
_globalRenderer.program->setUniform("modelViewTransform", modelViewTransform);
const bool hasHeightLayer =
!_layerManager.layerGroup(layers::Group::ID::HeightLayers).activeLayers().empty();
if (_useAccurateNormals && hasHeightLayer) {
// Apply an extra scaling to the height if the object is scaled
_globalRenderer.program->setUniform(
"heightScale",
static_cast<float>(
glm::compMax(data.modelTransform.scale) * data.camera.scaling()
)
);
}
using namespace layers;
const bool nightLayersActive =
!_layerManager.layerGroup(Group::ID::NightLayers).activeLayers().empty();
const bool waterLayersActive =
!_layerManager.layerGroup(Group::ID::WaterMasks).activeLayers().empty();
if (nightLayersActive || waterLayersActive || _performShading) {
const glm::dvec3 directionToSunWorldSpace =
directionToLightSource(data.modelTransform.translation, _lightSourceNode);
const glm::vec3 directionToSunCameraSpace = glm::vec3(viewTransform *
glm::dvec4(directionToSunWorldSpace, 0));
// @TODO (abock, 2020-04-14); This is just a bandaid for issue #1136. The better
// way is to figure out with the uniform is optimized away. I assume that it is
// because the shader doesn't get recompiled when the last layer of the night
// or water is disabled; so the shader thinks it has to do the calculation, but
// there are actually no layers left
using IgnoreError = ghoul::opengl::ProgramObject::IgnoreError;
_localRenderer.program->setIgnoreUniformLocationError(IgnoreError::Yes);
_localRenderer.program->setUniform(
"lightDirectionCameraSpace",
-glm::normalize(directionToSunCameraSpace)
);
_localRenderer.program->setIgnoreUniformLocationError(IgnoreError::Yes);
}
// Local shader
_localRenderer.program->setUniform(
"projectionTransform",
data.camera.sgctInternal.projectionMatrix()
);
if (nightLayersActive || waterLayersActive || _performShading) {
const glm::dvec3 directionToSunWorldSpace =
directionToLightSource(data.modelTransform.translation, _lightSourceNode);
const glm::vec3 directionToSunCameraSpace = glm::vec3(viewTransform *
glm::dvec4(directionToSunWorldSpace, 0));
// @TODO (abock, 2020-04-14); This is just a bandaid for issue #1136. The better
// way is to figure out with the uniform is optimized away. I assume that it is
// because the shader doesn't get recompiled when the last layer of the night
// or water is disabled; so the shader thinks it has to do the calculation, but
// there are actually no layers left
using IgnoreError = ghoul::opengl::ProgramObject::IgnoreError;
_globalRenderer.program->setIgnoreUniformLocationError(IgnoreError::Yes);
_globalRenderer.program->setUniform(
"lightDirectionCameraSpace",
-glm::normalize(directionToSunCameraSpace)
);
_globalRenderer.program->setIgnoreUniformLocationError(IgnoreError::Yes);
}
int globalCount = 0;
int localCount = 0;
auto traversal = [](const Chunk& node, std::vector<const Chunk*>& global,
int& iGlobal, std::vector<const Chunk*>& local, int& iLocal, int cutoff,
std::vector<const Chunk*>& traversalMemory)
{
ZoneScopedN("traversal");
traversalMemory.clear();
// Loop through nodes in breadths first order
traversalMemory.push_back(&node);
while (!traversalMemory.empty()) {
const Chunk* n = traversalMemory.front();
traversalMemory.erase(traversalMemory.begin());
if (isLeaf(*n) && n->isVisible) {
if (n->tileIndex.level < cutoff) {
global[iGlobal] = n;
iGlobal++;
}
else {
local[iLocal] = n;
iLocal++;
}
}
// Add children to queue, if any
if (!isLeaf(*n)) {
for (int i = 0; i < 4; i++) {
traversalMemory.push_back(n->children[i]);
}
}
}
};
traversal(
_leftRoot,
_globalChunkBuffer,
globalCount,
_localChunkBuffer,
localCount,
_debugProperties.modelSpaceRenderingCutoffLevel,
_traversalMemory
);
traversal(
_rightRoot,
_globalChunkBuffer,
globalCount,
_localChunkBuffer,
localCount,
_debugProperties.modelSpaceRenderingCutoffLevel,
_traversalMemory
);
// Render all chunks that want to be rendered globally
_globalRenderer.program->activate();
for (int i = 0; i < globalCount; i++) {
renderChunkGlobally(*_globalChunkBuffer[i], data, shadowData, renderGeomOnly);
}
_globalRenderer.program->deactivate();
// Render all chunks that need to be rendered locally
_localRenderer.program->activate();
for (int i = 0; i < localCount; i++) {
renderChunkLocally(*_localChunkBuffer[i], data, shadowData, renderGeomOnly);
}
_localRenderer.program->deactivate();
if (global::sessionRecordingHandler->isSavingFramesDuringPlayback() &&
global::sessionRecordingHandler->shouldWaitForTileLoading())
{
// If our tile cache is very full, we assume we need to adjust the level of detail
// dynamically to not keep rendering frames with unavailable data
// After certain number of iterations(_debugProperties.dynamicLodIterationCount)
// of unavailable/available data in a row, we assume that a change could be made.
const int iterCount = _debugProperties.dynamicLodIterationCount;
const bool exceededIterations =
static_cast<int>(_iterationsOfUnavailableData) > iterCount;
const float clf = _currentLodScaleFactor;
const float clfMin = _currentLodScaleFactor.minValue();
const float targetLod = _targetLodScaleFactor;
const bool validLodFactor = clf > clfMin;
if (exceededIterations && validLodFactor) {
_currentLodScaleFactor = _currentLodScaleFactor - 0.1f;
_iterationsOfUnavailableData = 0;
_lodScaleFactorDirty = true;
} // Make 2 times the iterations with available data to move it up again
else if (static_cast<int>(_iterationsOfAvailableData) >
(iterCount * 2) && clf < targetLod)
{
_currentLodScaleFactor = _currentLodScaleFactor + 0.1f;
_iterationsOfAvailableData = 0;
_lodScaleFactorDirty = true;
}
}
}
void RenderableGlobe::renderChunkGlobally(const Chunk& chunk, const RenderData& data,
const ShadowComponent::ShadowMapData& shadowData,
bool renderGeomOnly)
{
ZoneScoped;
TracyGpuZone("renderChunkGlobally");
const TileIndex& tileIndex = chunk.tileIndex;
ghoul::opengl::ProgramObject& program = *_globalRenderer.program;
std::array<LayerGroup*, LayerManager::NumLayerGroups> layerGroups =
_layerManager.layerGroups();
for (size_t i = 0; i < layerGroups.size(); i++) {
_globalRenderer.gpuLayerGroups[i].setValue(program, *layerGroups[i], tileIndex);
}
// The length of the skirts is proportional to its size
program.setUniform(
_globalRenderer.uniformCache.skirtLength,
static_cast<float>(
glm::min(
chunk.surfacePatch.halfSize().lat * 1000000,
_ellipsoid.minimumRadius()
)
)
);
if (_layerManager.hasAnyBlendingLayersEnabled()) {
program.setUniform("chunkLevel", chunk.tileIndex.level);
}
// Calculate other uniform variables needed for rendering
const Geodetic2 swCorner = chunk.surfacePatch.corner(Quad::SOUTH_WEST);
const Geodetic2& patchSize = chunk.surfacePatch.size();
program.setUniform(
_globalRenderer.uniformCache.minLatLon,
glm::vec2(swCorner.lon, swCorner.lat)
);
program.setUniform(
_globalRenderer.uniformCache.lonLatScalingFactor,
glm::vec2(patchSize.lon, patchSize.lat)
);
setCommonUniforms(program, chunk, data);
if (_eclipseShadowsEnabled && !_ellipsoid.shadowConfigurationArray().empty()) {
calculateEclipseShadows(program, data, ShadowCompType::GLOBAL_SHADOW);
}
// Shadow Mapping
ghoul::opengl::TextureUnit shadowMapUnit;
if (_shadowMappingProperties.shadowMapping && shadowData.shadowDepthTexture != 0) {
// Adding the model transformation to the final shadow matrix so we have a
// complete transformation from the model coordinates to the clip space of the
// light position.
program.setUniform(
"shadowMatrix",
shadowData.shadowMatrix * modelTransform()
);
shadowMapUnit.activate();
glBindTexture(GL_TEXTURE_2D, shadowData.shadowDepthTexture);
program.setUniform("shadowMapTexture", shadowMapUnit);
program.setUniform(
"zFightingPercentage",
_shadowMappingProperties.zFightingPercentage
);
}
else if (_shadowMappingProperties.shadowMapping && _shadowComponent) {
shadowMapUnit.activate();
// JCC: Avoiding a to recompiling the shaders or having more than one
// set of shaders for this step.
glBindTexture(GL_TEXTURE_2D, _shadowComponent->dDepthTexture());
program.setUniform("shadowMapTexture", shadowMapUnit);
}
glEnable(GL_DEPTH_TEST);
if (!renderGeomOnly) {
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
}
_grid.drawUsingActiveProgram();
for (GPULayerGroup& l : _globalRenderer.gpuLayerGroups) {
l.deactivate();
}
}
void RenderableGlobe::renderChunkLocally(const Chunk& chunk, const RenderData& data,
const ShadowComponent::ShadowMapData& shadowData,
bool renderGeomOnly)
{
ZoneScoped;
TracyGpuZone("renderChunkLocally");
//PerfMeasure("locally");
const TileIndex& tileIndex = chunk.tileIndex;
ghoul::opengl::ProgramObject& program = *_localRenderer.program;
const std::array<LayerGroup*, LayerManager::NumLayerGroups>& layerGroups =
_layerManager.layerGroups();
for (size_t i = 0; i < layerGroups.size(); i++) {
_localRenderer.gpuLayerGroups[i].setValue(program, *layerGroups[i], tileIndex);
}
// The length of the skirts is proportional to its size
program.setUniform(
_localRenderer.uniformCache.skirtLength,
static_cast<float>(
glm::min(
chunk.surfacePatch.halfSize().lat * 1000000,
_ellipsoid.minimumRadius()
)
)
);
if (_layerManager.hasAnyBlendingLayersEnabled()) {
program.setUniform("chunkLevel", chunk.tileIndex.level);
}
// Calculate other uniform variables needed for rendering
// Send the matrix inverse to the fragment for the global and local shader (JCC)
const glm::dmat4 viewTransform = data.camera.combinedViewMatrix();
const glm::dmat4 modelViewTransform = viewTransform * _cachedModelTransform;
std::array<glm::dvec3, 4> cornersCameraSpace;
std::array<glm::dvec3, 4> cornersModelSpace;
for (int i = 0; i < 4; i++) {
const Quad q = static_cast<Quad>(i);
const Geodetic2 corner = chunk.surfacePatch.corner(q);
const glm::dvec3 cornerModelSpace = _ellipsoid.cartesianSurfacePosition(corner);
cornersModelSpace[i] = cornerModelSpace;
const glm::dvec3 cornerCameraSpace = glm::dvec3(
modelViewTransform * glm::dvec4(cornerModelSpace, 1.0)
);
cornersCameraSpace[i] = cornerCameraSpace;
}
_localRenderer.program->setUniform(
_localRenderer.uniformCache.p01,
glm::vec3(cornersCameraSpace[0])
);
_localRenderer.program->setUniform(
_localRenderer.uniformCache.p11,
glm::vec3(cornersCameraSpace[1])
);
_localRenderer.program->setUniform(
_localRenderer.uniformCache.p00,
glm::vec3(cornersCameraSpace[2])
);
_localRenderer.program->setUniform(
_localRenderer.uniformCache.p10,
glm::vec3(cornersCameraSpace[3])
);
// TODO: Patch normal can be calculated for all corners and then linearly
// interpolated on the GPU to avoid cracks for high altitudes.
// JCC: Camera space includes the SGCT View transformation.
const glm::vec3 patchNormalCameraSpace = glm::normalize(
glm::cross(
cornersCameraSpace[Quad::SOUTH_EAST] - cornersCameraSpace[Quad::SOUTH_WEST],
cornersCameraSpace[Quad::NORTH_EAST] - cornersCameraSpace[Quad::SOUTH_WEST]
)
);
program.setUniform(
_localRenderer.uniformCache.patchNormalCameraSpace,
patchNormalCameraSpace
);
using namespace layers;
if (!_layerManager.layerGroup(Group::ID::HeightLayers).activeLayers().empty()) {
// Apply an extra scaling to the height if the object is scaled
program.setUniform(
"heightScale",
static_cast<float>(
glm::compMax(data.modelTransform.scale) * data.camera.scaling()
)
);
}
setCommonUniforms(program, chunk, data);
if (_eclipseShadowsEnabled && !_ellipsoid.shadowConfigurationArray().empty()) {
calculateEclipseShadows(program, data, ShadowCompType::LOCAL_SHADOW);
}
// Shadow Mapping
ghoul::opengl::TextureUnit shadowMapUnit;
if (_shadowMappingProperties.shadowMapping && shadowData.shadowDepthTexture != 0) {
// Adding the model transformation to the final shadow matrix so we have a
// complete transformation from the model coordinates to the clip space of the
// light position.
program.setUniform(
"shadowMatrix",
shadowData.shadowMatrix * modelTransform()
);
shadowMapUnit.activate();
glBindTexture(GL_TEXTURE_2D, shadowData.shadowDepthTexture);
program.setUniform("shadowMapTexture", shadowMapUnit);
program.setUniform(
"zFightingPercentage",
_shadowMappingProperties.zFightingPercentage
);
}
else if (_shadowMappingProperties.shadowMapping) {
shadowMapUnit.activate();
// JCC: Avoiding a to recompiling the shaders or having more than one
// set of shaders for this step.
glBindTexture(GL_TEXTURE_2D, _shadowComponent->dDepthTexture());
program.setUniform("shadowMapTexture", shadowMapUnit);
}
glEnable(GL_DEPTH_TEST);
if (!renderGeomOnly) {
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
}
_grid.drawUsingActiveProgram();
for (GPULayerGroup& l : _localRenderer.gpuLayerGroups) {
l.deactivate();
}
}
void RenderableGlobe::debugRenderChunk(const Chunk& chunk, const glm::dmat4& mvp,
bool renderBounds) const
{
ZoneScoped;
const std::array<glm::dvec4, 8>& modelSpaceCorners = chunk.corners;
std::vector<glm::vec4> clippingSpaceCorners(8);
AABB3 screenSpaceBounds;
for (size_t i = 0; i < 8; i++) {
const glm::vec4& clippingSpaceCorner = mvp * modelSpaceCorners[i];
clippingSpaceCorners[i] = clippingSpaceCorner;
glm::vec3 screenSpaceCorner =
glm::vec3((1.f / clippingSpaceCorner.w) * clippingSpaceCorner);
expand(screenSpaceBounds, std::move(screenSpaceCorner));
}
const unsigned int colorBits = 1 + chunk.tileIndex.level % 6;
const glm::vec4 color = glm::vec4(
colorBits & 1,
colorBits & 2,
colorBits & 4,
0.3f
);
if (renderBounds) {
DebugRenderer::ref().renderNiceBox(clippingSpaceCorners, color);
}
}
//////////////////////////////////////////////////////////////////////////////////////////
// Shader code
//////////////////////////////////////////////////////////////////////////////////////////
void RenderableGlobe::setCommonUniforms(ghoul::opengl::ProgramObject& programObject,
const Chunk& chunk, const RenderData& data)
{
using namespace layers;
ZoneScoped;
if (_useAccurateNormals &&
!_layerManager.layerGroup(Group::ID::HeightLayers).activeLayers().empty())
{
const glm::dvec3 corner00 = _ellipsoid.cartesianSurfacePosition(
chunk.surfacePatch.corner(Quad::SOUTH_WEST)
);
const glm::dvec3 corner10 = _ellipsoid.cartesianSurfacePosition(
chunk.surfacePatch.corner(Quad::SOUTH_EAST)
);
const glm::dvec3 corner01 = _ellipsoid.cartesianSurfacePosition(
chunk.surfacePatch.corner(Quad::NORTH_WEST)
);
const glm::dvec3 corner11 = _ellipsoid.cartesianSurfacePosition(
chunk.surfacePatch.corner(Quad::NORTH_EAST)
);
const glm::mat4 modelViewTransform = glm::mat4(
data.camera.combinedViewMatrix() * _cachedModelTransform
);
const glm::mat3& modelViewTransformMat3 = glm::mat3(modelViewTransform);
// This is an assumption that the height tile has a resolution of 512 * 512
// If it does not it will still produce "correct" normals. If the resolution is
// higher the shadows will be softer, if it is lower, pixels will be visible.
// Since default is 512 this will most likely work fine.
constexpr float TileDelta = 1.f / DefaultHeightTileResolution;
const glm::vec3 deltaTheta0 = modelViewTransformMat3 *
(glm::vec3(corner10 - corner00) * TileDelta);
const glm::vec3 deltaTheta1 = modelViewTransformMat3 *
(glm::vec3(corner11 - corner01) * TileDelta);
const glm::vec3 deltaPhi0 = modelViewTransformMat3 *
(glm::vec3(corner01 - corner00) * TileDelta);
const glm::vec3 deltaPhi1 = modelViewTransformMat3 *
(glm::vec3(corner11 - corner10) * TileDelta);
// Upload uniforms
programObject.setUniform("deltaTheta0", glm::length(deltaTheta0));
programObject.setUniform("deltaTheta1", glm::length(deltaTheta1));
programObject.setUniform("deltaPhi0", glm::length(deltaPhi0));
programObject.setUniform("deltaPhi1", glm::length(deltaPhi1));
programObject.setUniform("tileDelta", TileDelta);
}
}
void RenderableGlobe::recompileShaders() {
ZoneScoped;
struct LayerShaderPreprocessingData {
struct LayerGroupPreprocessingData {
int lastLayerIdx;
bool layerBlendingEnabled;
std::vector<layers::Layer::ID> layerType;
std::vector<layers::Blend::ID> blendMode;
std::vector<layers::Adjustment::ID> layerAdjustmentType;
};
std::array<LayerGroupPreprocessingData, layers::Groups.size()> layeredTextureInfo;
std::vector<std::pair<std::string, std::string>> keyValuePairs;
};
//
// Create LayerShaderPreprocessingData
//
LayerShaderPreprocessingData preprocessingData;
for (size_t i = 0; i < layers::Groups.size(); i++) {
LayerShaderPreprocessingData::LayerGroupPreprocessingData layeredTextureInfo;
const LayerGroup& group = _layerManager.layerGroup(layers::Group::ID(i));
const std::vector<Layer*>& layers = group.activeLayers();
// This check was implicit before; not sure if it will fire or will be handled
// elsewhere
//ghoul_assert(
// !layerGroup.activeLayers().empty(),
// "If activeLayers is empty the following line will lead to an overflow"
//);
layeredTextureInfo.lastLayerIdx = static_cast<int>(
group.activeLayers().size() - 1
);
layeredTextureInfo.layerBlendingEnabled = group.layerBlendingEnabled();
for (Layer* layer : layers) {
layeredTextureInfo.layerType.push_back(layer->type());
layeredTextureInfo.blendMode.push_back(layer->blendMode());
layeredTextureInfo.layerAdjustmentType.push_back(
layer->layerAdjustment().type()
);
}
preprocessingData.layeredTextureInfo[i] = layeredTextureInfo;
}
std::vector<std::pair<std::string, std::string>>& pairs =
preprocessingData.keyValuePairs;
const bool hasHeightLayer =
!_layerManager.layerGroup(layers::Group::ID::HeightLayers).activeLayers().empty();
pairs.emplace_back(
"useAccurateNormals",
std::to_string(_useAccurateNormals && hasHeightLayer)
);
pairs.emplace_back("performShading", std::to_string(_performShading));
pairs.emplace_back("useEclipseShadows", std::to_string(_eclipseShadowsEnabled));
pairs.emplace_back("useEclipseHardShadows", std::to_string(_eclipseHardShadows));
pairs.emplace_back(
"enableShadowMapping",
std::to_string(_shadowMappingProperties.shadowMapping && _shadowComponent)
);
pairs.emplace_back("showChunkEdges", std::to_string(_debugProperties.showChunkEdges));
pairs.emplace_back("showHeightResolution", "0");
pairs.emplace_back("showHeightIntensities", "0");
pairs.emplace_back("defaultHeight", std::to_string(DefaultHeight));
//
// Create dictionary from layerpreprocessing data
//
ghoul::Dictionary shaderDictionary;
// Different layer types can be height layers or color layers for example.
// These are used differently within the shaders.
for (size_t i = 0; i < preprocessingData.layeredTextureInfo.size(); i++) {
// lastLayerIndex must be at least 0 for the shader to compile,
// the layer type is inactivated by setting use to false
shaderDictionary.setValue(
std::format("lastLayerIndex{}", layers::Groups[i].identifier),
glm::max(preprocessingData.layeredTextureInfo[i].lastLayerIdx, 0)
);
shaderDictionary.setValue(
std::format("use{}", layers::Groups[i].identifier),
preprocessingData.layeredTextureInfo[i].lastLayerIdx >= 0
);
shaderDictionary.setValue(
std::format("blend{}", layers::Groups[i].identifier),
preprocessingData.layeredTextureInfo[i].layerBlendingEnabled
);
// This is to avoid errors from shader preprocessor
shaderDictionary.setValue(
std::format("{}0LayerType", layers::Groups[i].identifier),
0
);
const std::string groupName = std::string(layers::Groups[i].identifier);
for (int j = 0;
j < preprocessingData.layeredTextureInfo[i].lastLayerIdx + 1;
j++)
{
shaderDictionary.setValue(
groupName + std::to_string(j) + "LayerType",
static_cast<int>(preprocessingData.layeredTextureInfo[i].layerType[j])
);
}
// This is to avoid errors from shader preprocessor
shaderDictionary.setValue(groupName + "0" + "BlendMode", 0);
for (int j = 0;
j < preprocessingData.layeredTextureInfo[i].lastLayerIdx + 1;
j++)
{
shaderDictionary.setValue(
groupName + std::to_string(j) + "BlendMode",
static_cast<int>(preprocessingData.layeredTextureInfo[i].blendMode[j])
);
}
// This is to avoid errors from shader preprocessor
std::string layerAdjustmentType = groupName + "0" + "LayerAdjustmentType";
shaderDictionary.setValue(std::move(layerAdjustmentType), 0);
for (int j = 0;
j < preprocessingData.layeredTextureInfo[i].lastLayerIdx + 1;
j++)
{
shaderDictionary.setValue(
groupName + std::to_string(j) + "LayerAdjustmentType",
static_cast<int>(
preprocessingData.layeredTextureInfo[i].layerAdjustmentType[j]
)
);
}
}
ghoul::Dictionary layerGroupNames;
for (size_t i = 0; i < layers::Groups.size(); i++) {
layerGroupNames.setValue(
std::to_string(i),
std::string(layers::Groups[i].identifier)
);
}
shaderDictionary.setValue("layerGroups", layerGroupNames);
for (const std::pair<std::string, std::string>& p : preprocessingData.keyValuePairs) {
shaderDictionary.setValue(p.first, p.second);
}
// Shadow Mapping Samples
shaderDictionary.setValue(
"nShadowSamples",
_shadowMappingProperties.nShadowSamples - 1
);
// Exclise Shadow Samples
const int nEclipseShadows = static_cast<int>(
_ellipsoid.shadowConfigurationArray().size()
);
shaderDictionary.setValue("nEclipseShadows", nEclipseShadows - 1);
//
// Create local shader
//
global::renderEngine->removeRenderProgram(_localRenderer.program.get());
_localRenderer.program = global::renderEngine->buildRenderProgram(
"LocalChunkedLodPatch",
absPath("${MODULE_GLOBEBROWSING}/shaders/localrenderer_vs.glsl"),
absPath("${MODULE_GLOBEBROWSING}/shaders/renderer_fs.glsl"),
shaderDictionary
);
ghoul_assert(_localRenderer.program, "Failed to initialize programObject");
_localRenderer.updatedSinceLastCall = true;
_localRenderer.program->setUniform("xSegments", _grid.xSegments);
ghoul::opengl::updateUniformLocations(
*_localRenderer.program,
_localRenderer.uniformCache
);
//
// Create global shader
//
global::renderEngine->removeRenderProgram(_globalRenderer.program.get());
_globalRenderer.program = global::renderEngine->buildRenderProgram(
"GlobalChunkedLodPatch",
absPath("${MODULE_GLOBEBROWSING}/shaders/globalrenderer_vs.glsl"),
absPath("${MODULE_GLOBEBROWSING}/shaders/renderer_fs.glsl"),
shaderDictionary
);
ghoul_assert(_globalRenderer.program, "Failed to initialize programObject");
_globalRenderer.program->setUniform("xSegments", _grid.xSegments);
// Ellipsoid Radius (Model Space)
_globalRenderer.program->setUniform(
"radiiSquared",
glm::vec3(_ellipsoid.radii() * _ellipsoid.radii())
);
ghoul::opengl::updateUniformLocations(
*_globalRenderer.program,
_globalRenderer.uniformCache
);
_globalRenderer.updatedSinceLastCall = true;
_shadersNeedRecompilation = false;
}
SurfacePositionHandle RenderableGlobe::calculateSurfacePositionHandle(
const glm::dvec3& targetModelSpace) const
{
ZoneScoped;
glm::dvec3 centerToEllipsoidSurface =
_ellipsoid.geodeticSurfaceProjection(targetModelSpace);
const glm::dvec3 ellipsoidSrfToTarget = targetModelSpace - centerToEllipsoidSurface;
// ellipsoidSurfaceOutDirection will point towards the target, we want the outward
// direction. Therefore it must be flipped in case the target is under the reference
// ellipsoid so that it always points outwards
glm::dvec3 ellipsoidSurfaceOutDirection = glm::normalize(ellipsoidSrfToTarget);
if (glm::dot(ellipsoidSurfaceOutDirection, centerToEllipsoidSurface) < 0) {
ellipsoidSurfaceOutDirection *= -1.0;
}
double heightToSurface = getHeight(targetModelSpace);
heightToSurface = glm::isnan(heightToSurface) ? 0.0 : heightToSurface;
centerToEllipsoidSurface = glm::isnan(glm::length(centerToEllipsoidSurface)) ?
(glm::dvec3(0.0, 1.0, 0.0) * interactionSphere()) :
centerToEllipsoidSurface;
ellipsoidSurfaceOutDirection = glm::isnan(glm::length(ellipsoidSurfaceOutDirection)) ?
glm::dvec3(0.0, 1.0, 0.0) : ellipsoidSurfaceOutDirection;
return {
centerToEllipsoidSurface,
ellipsoidSurfaceOutDirection,
heightToSurface
};
}
bool RenderableGlobe::testIfCullable(const Chunk& chunk,
const RenderData& renderData,
const BoundingHeights& heights,
const glm::dmat4& mvp) const
{
ZoneScoped;
return (PreformHorizonCulling && isCullableByHorizon(chunk, renderData, heights)) ||
(_debugProperties.performFrustumCulling &&
isCullableByFrustum(chunk, renderData, mvp));
}
int RenderableGlobe::desiredLevel(const Chunk& chunk, const RenderData& renderData,
const BoundingHeights& heights) const
{
ZoneScoped;
const int desiredLevel = _debugProperties.levelByProjectedAreaElseDistance ?
desiredLevelByProjectedArea(chunk, renderData, heights) :
desiredLevelByDistance(chunk, renderData, heights);
const int levelByAvailableData = desiredLevelByAvailableTileData(chunk);
if (LimitLevelByAvailableData && (levelByAvailableData != UnknownDesiredLevel)) {
const int l = glm::min(desiredLevel, levelByAvailableData);
return glm::clamp(l, MinSplitDepth, MaxSplitDepth);
}
else {
return glm::clamp(desiredLevel, MinSplitDepth, MaxSplitDepth);
}
}
float RenderableGlobe::getHeight(const glm::dvec3& position) const {
ZoneScoped;
float height = 0;
// Get the uv coordinates to sample from
const Geodetic2 geodeticPosition = _ellipsoid.cartesianToGeodetic2(position);
const Chunk& node = geodeticPosition.lon < Coverage.center().lon ?
findChunkNode(_leftRoot, geodeticPosition) :
findChunkNode(_rightRoot, geodeticPosition);
const int chunkLevel = node.tileIndex.level;
const int numIndicesAtLevel = 1 << chunkLevel;
const double u = 0.5 + geodeticPosition.lon / glm::two_pi<double>();
const double v = 0.25 - geodeticPosition.lat / glm::two_pi<double>();
const double xIndexSpace = u * numIndicesAtLevel;
const double yIndexSpace = v * numIndicesAtLevel;
const int x = static_cast<int>(floor(xIndexSpace));
const int y = static_cast<int>(floor(yIndexSpace));
ghoul_assert(chunkLevel < std::numeric_limits<uint8_t>::max(), "Too high level");
const TileIndex tileIndex(x, y, static_cast<uint8_t>(chunkLevel));
const GeodeticPatch patch = GeodeticPatch(tileIndex);
const Geodetic2 northEast = patch.corner(Quad::NORTH_EAST);
const Geodetic2 southWest = patch.corner(Quad::SOUTH_WEST);
const Geodetic2 geoDiffPatch = {
.lat = northEast.lat - southWest.lat,
.lon = northEast.lon - southWest.lon
};
const Geodetic2 geoDiffPoint = {
.lat = geodeticPosition.lat - southWest.lat,
.lon = geodeticPosition.lon - southWest.lon
};
const glm::vec2 patchUV = glm::vec2(
geoDiffPoint.lon / geoDiffPatch.lon,
geoDiffPoint.lat / geoDiffPatch.lat
);
// Get the tile providers for the height maps
const std::vector<Layer*>& heightMapLayers =
_layerManager.layerGroup(layers::Group::ID::HeightLayers).activeLayers();
for (Layer* layer : heightMapLayers) {
TileProvider* tileProvider = layer->tileProvider();
if (!tileProvider) {
continue;
}
// Transform the uv coordinates to the current tile texture
const ChunkTile chunkTile = tileProvider->chunkTile(tileIndex);
const Tile& tile = chunkTile.tile;
const TileUvTransform& uvTransform = chunkTile.uvTransform;
const TileDepthTransform& depthTransform = tileProvider->depthTransform();
if (tile.status != Tile::Status::OK) {
return 0;
}
ghoul::opengl::Texture* tileTexture = tile.texture;
if (!tileTexture) {
return 0;
}
const glm::vec2& transformedUv = layer->tileUvToTextureSamplePosition(
uvTransform,
patchUV
);
// Sample and do linear interpolation
// (could possibly be moved as a function in ghoul texture)
// Suggestion: a function in ghoul::opengl::Texture that takes uv coordinates
// in range [0,1] and uses the set interpolation method and clamping.
const glm::uvec3 dimensions = tileTexture->dimensions();
glm::vec2 samplePos = transformedUv * glm::vec2(dimensions);
// @TODO (emmbr, 2023-06-14) This 0.5f offset was added as a bandaid for issue
// #2696. It seems to improve the behavior, but I am not certain of why. And the
// underlying problem is still there and should at some point be looked at again
samplePos -= glm::vec2(0.5f);
glm::uvec2 samplePos00 = samplePos;
samplePos00 = glm::clamp(
samplePos00,
glm::uvec2(0, 0),
glm::uvec2(dimensions) - glm::uvec2(1)
);
const glm::vec2 samplePosFract = samplePos - glm::vec2(samplePos00);
const glm::uvec2 samplePos10 = glm::min(
samplePos00 + glm::uvec2(1, 0),
glm::uvec2(dimensions) - glm::uvec2(1)
);
const glm::uvec2 samplePos01 = glm::min(
samplePos00 + glm::uvec2(0, 1),
glm::uvec2(dimensions) - glm::uvec2(1)
);
const glm::uvec2 samplePos11 = glm::min(
samplePos00 + glm::uvec2(1, 1),
glm::uvec2(dimensions) - glm::uvec2(1)
);
const float sample00 = tileTexture->texelAsFloat(samplePos00).x;
const float sample10 = tileTexture->texelAsFloat(samplePos10).x;
const float sample01 = tileTexture->texelAsFloat(samplePos01).x;
const float sample11 = tileTexture->texelAsFloat(samplePos11).x;
// In case the texture has NaN or no data values don't use this height map.
const bool anySampleIsNaN =
std::isnan(sample00) ||
std::isnan(sample01) ||
std::isnan(sample10) ||
std::isnan(sample11);
const bool anySampleIsNoData =
sample00 == tileProvider->noDataValueAsFloat() ||
sample01 == tileProvider->noDataValueAsFloat() ||
sample10 == tileProvider->noDataValueAsFloat() ||
sample11 == tileProvider->noDataValueAsFloat();
if (anySampleIsNaN || anySampleIsNoData) {
continue;
}
const float sample0 = sample00 * (1.f - samplePosFract.x) +
sample10 * samplePosFract.x;
const float sample1 = sample01 * (1.f - samplePosFract.x) +
sample11 * samplePosFract.x;
const float sample = sample0 * (1.f - samplePosFract.y) +
sample1 * samplePosFract.y;
// Same as is used in the shader. This is not a perfect solution but
// if the sample is actually a no-data-value (min_float) the interpolated
// value might not be. Therefore we have a cut-off. Assuming no data value
// is smaller than -100000
if (sample > -100000) {
// Perform depth transform to get the value in meters
height = depthTransform.offset + depthTransform.scale * sample;
// Make sure that the height value follows the layer settings.
// For example if the multiplier is set to a value bigger than one,
// the sampled height should be modified as well.
height = layer->renderSettings().performLayerSettings(height);
}
}
// Return the result
return height;
}
void RenderableGlobe::calculateEclipseShadows(ghoul::opengl::ProgramObject& programObject,
const RenderData& data, ShadowCompType stype)
{
ZoneScoped;
constexpr double KM_TO_M = 1000.0;
ghoul_assert(
!_ellipsoid.shadowConfigurationArray().empty(),
"Needs to have eclipse shadows enabled"
);
// Shadow calculations..
std::vector<ShadowRenderingStruct> shadowDataArray;
const std::vector<Ellipsoid::ShadowConfiguration>& shadowConfArray =
_ellipsoid.shadowConfigurationArray();
shadowDataArray.reserve(shadowConfArray.size());
double lt = 0.0;
for (const auto& shadowConf : shadowConfArray) {
// TO REMEMBER: all distances and lengths in world coordinates are in
// meters!!! We need to move this to view space...
// Getting source and caster:
glm::dvec3 sourcePos = SpiceManager::ref().targetPosition(
shadowConf.source.first,
"SSB",
"GALACTIC",
{},
data.time.j2000Seconds(),
lt
);
sourcePos *= KM_TO_M; // converting to meters
glm::dvec3 casterPos = SpiceManager::ref().targetPosition(
shadowConf.caster.first,
"SSB",
"GALACTIC",
{},
data.time.j2000Seconds(),
lt
);
casterPos *= KM_TO_M; // converting to meters
const std::string source = shadowConf.source.first;
SceneGraphNode* sourceNode =
global::renderEngine->scene()->sceneGraphNode(source);
const std::string caster = shadowConf.caster.first;
SceneGraphNode* casterNode =
global::renderEngine->scene()->sceneGraphNode(caster);
if ((sourceNode == nullptr) || (casterNode == nullptr)) {
LERRORC(
"Renderableglobe",
"Invalid scenegraph node for the shadow's caster or shadow's receiver"
);
return;
}
const double sourceRadiusScale = std::max(glm::compMax(sourceNode->scale()), 1.0);
const double casterRadiusScale = std::max(glm::compMax(casterNode->scale()), 1.0);
// First we determine if the caster is shadowing the current planet (all
// calculations in World Coordinates):
const glm::dvec3 planetCasterVec = casterPos - data.modelTransform.translation;
const glm::dvec3 sourceCasterVec = casterPos - sourcePos;
const double sc_length = glm::length(sourceCasterVec);
const glm::dvec3 planetCaster_proj =
(glm::dot(planetCasterVec, sourceCasterVec) / (sc_length*sc_length)) *
sourceCasterVec;
const double d_test = glm::length(planetCasterVec - planetCaster_proj);
const double xp_test = shadowConf.caster.second * casterRadiusScale *
sc_length / (shadowConf.source.second * sourceRadiusScale +
shadowConf.caster.second * casterRadiusScale);
const double rp_test = shadowConf.caster.second * casterRadiusScale *
(glm::length(planetCaster_proj) + xp_test) / xp_test;
const glm::dvec3 sunPos = SpiceManager::ref().targetPosition(
"SUN",
"SSB",
"GALACTIC",
{},
data.time.j2000Seconds(),
lt
);
const double casterDistSun = glm::length(casterPos - sunPos);
const double planetDistSun =
glm::length(data.modelTransform.translation - sunPos);
ShadowRenderingStruct shadowData;
shadowData.isShadowing = false;
// Eclipse shadows considers planets and moons as spheres
if (((d_test - rp_test) < (_ellipsoid.radii().x * KM_TO_M)) &&
(casterDistSun < planetDistSun))
{
// The current caster is shadowing the current planet
shadowData.isShadowing = true;
shadowData.rs = shadowConf.source.second * sourceRadiusScale;
shadowData.rc = shadowConf.caster.second * casterRadiusScale;
shadowData.sourceCasterVec = glm::normalize(sourceCasterVec);
shadowData.xp = xp_test;
shadowData.xu = shadowData.rc * sc_length / (shadowData.rs - shadowData.rc);
shadowData.casterPositionVec = casterPos;
}
shadowDataArray.push_back(shadowData);
}
const std::string uniformVarName("shadowDataArray[");
unsigned int counter = 0;
for (const ShadowRenderingStruct& sd : shadowDataArray) {
constexpr std::string_view NameIsShadowing = "shadowDataArray[{}].isShadowing";
constexpr std::string_view NameXp = "shadowDataArray[{}].xp";
constexpr std::string_view NameXu = "shadowDataArray[{}].xu";
constexpr std::string_view NameRc = "shadowDataArray[{}].rc";
constexpr std::string_view NameSource = "shadowDataArray[{}].sourceCasterVec";
constexpr std::string_view NamePos = "shadowDataArray[{}].casterPositionVec";
programObject.setUniform(std::format(NameIsShadowing, counter), sd.isShadowing);
if (sd.isShadowing) {
programObject.setUniform(std::format(NameXp, counter), sd.xp);
programObject.setUniform(std::format(NameXu, counter), sd.xu);
programObject.setUniform(std::format(NameRc, counter), sd.rc);
programObject.setUniform(
std::format(NameSource, counter), sd.sourceCasterVec
);
programObject.setUniform(
std::format(NamePos, counter), sd.casterPositionVec
);
}
counter++;
}
if (stype == ShadowCompType::LOCAL_SHADOW) {
programObject.setUniform(
"inverseViewTransform",
glm::inverse(data.camera.combinedViewMatrix())
);
}
else if (stype == ShadowCompType::GLOBAL_SHADOW) {
programObject.setUniform("modelTransform", _cachedModelTransform);
}
// JCC: Removed in favor of: #define USE_ECLIPSE_HARD_SHADOWS #{useEclipseHardShadows}
/*programObject.setUniform(
"hardShadows",
_eclipseHardShadows
);*/
//programObject.setUniform("calculateEclipseShadows", true);
}
//////////////////////////////////////////////////////////////////////////////////////////
// Desired Level
//////////////////////////////////////////////////////////////////////////////////////////
int RenderableGlobe::desiredLevelByDistance(const Chunk& chunk,
const RenderData& data,
const BoundingHeights& heights) const
{
ZoneScoped;
// Calculations are done in the reference frame of the globe
// (model space). Hence, the camera position needs to be transformed
// with the inverse model matrix
const glm::dvec3 cameraPosition = glm::dvec3(_cachedInverseModelTransform *
glm::dvec4(data.camera.positionVec3(), 1.0));
const Geodetic2 pointOnPatch = chunk.surfacePatch.closestPoint(
_ellipsoid.cartesianToGeodetic2(cameraPosition)
);
const glm::dvec3 patchNormal = _ellipsoid.geodeticSurfaceNormal(pointOnPatch);
glm::dvec3 patchPosition = _ellipsoid.cartesianSurfacePosition(pointOnPatch);
const double heightToChunk = heights.min;
// Offset position according to height
patchPosition += patchNormal * heightToChunk;
const glm::dvec3 cameraToChunk = patchPosition - cameraPosition;
// Calculate desired level based on distance
const double distanceToPatch = glm::length(cameraToChunk);
const double distance = distanceToPatch;
const double scaleFactor = _currentLodScaleFactor * _ellipsoid.minimumRadius();
const double projectedScaleFactor = scaleFactor / distance;
const int desiredLevel = static_cast<int>(ceil(log2(projectedScaleFactor)));
return desiredLevel;
}
int RenderableGlobe::desiredLevelByProjectedArea(const Chunk& chunk,
const RenderData& data,
const BoundingHeights& heights) const
{
ZoneScoped;
// Calculations are done in the reference frame of the globe
// (model space). Hence, the camera position needs to be transformed
// with the inverse model matrix
const glm::dvec3 cameraPosition = glm::dvec3(
_cachedInverseModelTransform * glm::dvec4(data.camera.positionVec3(), 1.0)
);
// Approach:
// The projected area of the chunk will be calculated based on a small area that
// is close to the camera, and the scaled up to represent the full area.
// The advantage of doing this is that it will better handle the cases where the
// full patch is very curved (e.g. stretches from latitude 0 to 90 deg).
const Geodetic2 closestCorner = chunk.surfacePatch.closestCorner(
_ellipsoid.cartesianToGeodetic2(cameraPosition)
);
// Camera
// |
// V
//
// oo
// [ ]<
// *geodetic space*
//
// closestCorner
// +-----------------+ <-- north east corner
// | |
// | center |
// | |
// +-----------------+ <-- south east corner
const Geodetic2 center = chunk.surfacePatch.center();
const Geodetic3 c = { center, heights.min };
const Geodetic3 c1 = { Geodetic2{ center.lat, closestCorner.lon }, heights.min };
const Geodetic3 c2 = { Geodetic2{ closestCorner.lat, center.lon }, heights.min };
// Camera
// |
// V
//
// oo
// [ ]<
// *geodetic space*
//
// +--------c2-------+ <-- north east corner
// | |
// c1 c |
// | |
// +-----------------+ <-- south east corner
// Go from geodetic to cartesian space and project onto unit sphere
const glm::dvec3 camToCenter = -cameraPosition;
const glm::dvec3 A = glm::normalize(camToCenter + _ellipsoid.cartesianPosition(c));
const glm::dvec3 B = glm::normalize(camToCenter + _ellipsoid.cartesianPosition(c1));
const glm::dvec3 C = glm::normalize(camToCenter + _ellipsoid.cartesianPosition(c2));
// Camera *cartesian space*
// | +--------+---+
// V __--'' __--'' /
// C-------A--------- +
// oo / / /
//[ ]< +-------B----------+
//
// If the geodetic patch is small (i.e. has small width), that means the patch in
// cartesian space will be almost flat, and in turn, the triangle ABC will roughly
// correspond to 1/8 of the full area
const glm::dvec3 AB = B - A;
const glm::dvec3 AC = C - A;
const double areaABC = 0.5 * glm::length(glm::cross(AC, AB));
const double projectedChunkAreaApprox = 8 * areaABC;
const double scaledArea = _currentLodScaleFactor * projectedChunkAreaApprox;
return chunk.tileIndex.level + static_cast<int>(round(scaledArea - 1));
}
int RenderableGlobe::desiredLevelByAvailableTileData(const Chunk& chunk) const {
ZoneScoped;
const int currLevel = chunk.tileIndex.level;
for (const layers::Group& gi : layers::Groups) {
const std::vector<Layer*>& lyrs = _layerManager.layerGroup(gi.id).activeLayers();
for (Layer* layer : lyrs) {
const Tile::Status status = layer->tileStatus(chunk.tileIndex);
// Ensure that the current tile is OK and that the tileprovider for the
// current layer has enough data to support an additional level.
if (status == Tile::Status::OK &&
layer->tileProvider()->maxLevel() > currLevel + 1)
{
return UnknownDesiredLevel;
}
}
}
return currLevel - 1;
}
//////////////////////////////////////////////////////////////////////////////////////////
// Culling
//////////////////////////////////////////////////////////////////////////////////////////
bool RenderableGlobe::isCullableByFrustum(const Chunk& chunk,
const RenderData&,
const glm::dmat4& mvp) const
{
ZoneScoped;
const std::array<glm::dvec4, 8>& corners = chunk.corners;
// Create a bounding box that fits the patch corners
AABB3 bounds; // in screen space
for (size_t i = 0; i < 8; i++) {
const glm::dvec4 cornerClippingSpace = mvp * corners[i];
const glm::dvec3 ndc = glm::dvec3(
(1.f / glm::abs(cornerClippingSpace.w)) * cornerClippingSpace
);
expand(bounds, ndc);
}
return !(intersects(CullingFrustum, bounds));
}
bool RenderableGlobe::isCullableByHorizon(const Chunk& chunk,
const RenderData& renderData,
const BoundingHeights& heights) const
{
ZoneScoped;
// Calculations are done in the reference frame of the globe. Hence, the camera
// position needs to be transformed with the inverse model matrix
const GeodeticPatch& patch = chunk.surfacePatch;
const float maxHeight = heights.max;
const glm::dvec3 globePos = glm::dvec3(0.0, 0.0, 0.0); // In model space it is 0
const double minimumGlobeRadius = _ellipsoid.minimumRadius();
const glm::dvec3 cameraPos = glm::dvec3(
_cachedInverseModelTransform * glm::dvec4(renderData.camera.positionVec3(), 1.0)
);
const glm::dvec3& globeToCamera = cameraPos;
const Geodetic2 camPosOnGlobe = _ellipsoid.cartesianToGeodetic2(globeToCamera);
const Geodetic2 closestPatchPoint = patch.closestPoint(camPosOnGlobe);
glm::dvec3 objectPos = _ellipsoid.cartesianSurfacePosition(closestPatchPoint);
// objectPosition is closest in latlon space but not guaranteed to be closest in
// castesian coordinates. Therefore we compare it to the corners and pick the
// real closest point,
std::array<glm::dvec3, 4> corners = {
_ellipsoid.cartesianSurfacePosition(chunk.surfacePatch.corner(NORTH_WEST)),
_ellipsoid.cartesianSurfacePosition(chunk.surfacePatch.corner(NORTH_EAST)),
_ellipsoid.cartesianSurfacePosition(chunk.surfacePatch.corner(SOUTH_WEST)),
_ellipsoid.cartesianSurfacePosition(chunk.surfacePatch.corner(SOUTH_EAST))
};
for (int i = 0; i < 4; i++) {
const double distance = glm::length(cameraPos - corners[i]);
if (distance < glm::length(cameraPos - objectPos)) {
objectPos = corners[i];
}
}
const double objectP = std::pow(glm::length(objectPos - globePos), 2);
const double horizonP = std::pow(minimumGlobeRadius - maxHeight, 2);
if (objectP < horizonP) {
return false;
}
const double cameraP = std::pow(glm::length(cameraPos - globePos), 2);
const double minR = std::pow(minimumGlobeRadius, 2);
if (cameraP < minR) {
return false;
}
const double minimumAllowedDistanceToObjFromHorizon = std::sqrt(objectP - horizonP);
const double distanceToHorizon = std::sqrt(cameraP - minR);
// Minimum allowed for the object to be occluded
const double minimumAllowedDistanceToObjectSquared =
std::pow(distanceToHorizon + minimumAllowedDistanceToObjFromHorizon, 2) +
std::pow(maxHeight, 2);
const double distanceToObjectSquared = std::pow(
glm::length(objectPos - cameraPos),
2
);
return distanceToObjectSquared > minimumAllowedDistanceToObjectSquared;
}
//////////////////////////////////////////////////////////////////////////////////////////
// Chunk node handling
//////////////////////////////////////////////////////////////////////////////////////////
void RenderableGlobe::splitChunkNode(Chunk& cn, int depth) {
ZoneScoped;
if (depth > 0 && isLeaf(cn)) {
std::vector<void*> memory = _chunkPool.allocate(
static_cast<int>(cn.children.size())
);
for (size_t i = 0; i < cn.children.size(); i++) {
cn.children[i] = new (memory[i]) Chunk(
cn.tileIndex.child(static_cast<Quad>(i))
);
const BoundingHeights& heights = boundingHeightsForChunk(
*(cn.children[i]),
_layerManager
);
cn.children[i]->corners = boundingCornersForChunk(
*cn.children[i],
_ellipsoid,
heights
);
}
}
if (depth > 1) {
for (Chunk* child : cn.children) {
splitChunkNode(*child, depth - 1);
}
}
}
void RenderableGlobe::freeChunkNode(Chunk* n) {
ZoneScoped;
_chunkPool.free(n);
for (Chunk* c : n->children) {
if (c) {
freeChunkNode(c);
}
}
n->children.fill(nullptr);
}
void RenderableGlobe::mergeChunkNode(Chunk& cn) {
ZoneScoped;
for (Chunk* child : cn.children) {
if (child) {
mergeChunkNode(*child);
freeChunkNode(child);
}
}
cn.children.fill(nullptr);
}
bool RenderableGlobe::updateChunkTree(Chunk& cn, const RenderData& data,
const glm::dmat4& mvp)
{
ZoneScoped;
// abock: I tried turning this into a queue and use iteration, rather than recursion
// but that made the code harder to understand as the breadth-first traversal
// requires parents to be passed through the pipe twice (first to add the
// children and then again it self to be processed after the children finish).
// In addition, this didn't even improve performance --- 2018-10-04
if (isLeaf(cn)) {
ZoneScopedN("leaf");
updateChunk(cn, data, mvp);
if (cn.status == Chunk::Status::WantSplit) {
splitChunkNode(cn, 1);
}
else if (cn.status == Chunk::Status::DoNothing && (!cn.colorTileOK)) {
// Checking cn.heightTileOK caused always not avaiable for certain HiRISE data
_allChunksAvailable = false;
}
return cn.status == Chunk::Status::WantMerge;
}
else {
ZoneScopedN("!leaf");
char requestedMergeMask = 0;
for (int i = 0; i < 4; i++) {
if (updateChunkTree(*cn.children[i], data, mvp)) {
requestedMergeMask |= (1 << i);
}
}
const bool allChildrenWantsMerge = requestedMergeMask == 0xf;
updateChunk(cn, data, mvp);
if (allChildrenWantsMerge && (cn.status != Chunk::Status::WantSplit)) {
mergeChunkNode(cn);
}
else if (cn.status == Chunk::Status::WantSplit) {
splitChunkNode(cn, 1);
}
else if (cn.status == Chunk::Status::DoNothing && (!cn.colorTileOK)) {
_allChunksAvailable = false;
}
return false;
}
}
void RenderableGlobe::updateChunk(Chunk& chunk, const RenderData& data,
const glm::dmat4& mvp) const
{
ZoneScoped;
const BoundingHeights& heights = boundingHeightsForChunk(chunk, _layerManager);
chunk.heightTileOK = heights.tileOK;
chunk.colorTileOK = colorAvailableForChunk(chunk, _layerManager);
if (_chunkCornersDirty) {
chunk.corners = boundingCornersForChunk(chunk, _ellipsoid, heights);
// The flag gets set to false globally after the updateChunkTree calls
}
if (testIfCullable(chunk, data, heights, mvp)) {
chunk.isVisible = false;
chunk.status = Chunk::Status::WantMerge;
}
else {
chunk.isVisible = true;
}
const int dl = desiredLevel(chunk, data, heights);
if (dl < chunk.tileIndex.level) {
chunk.status = Chunk::Status::WantMerge;
}
else if (chunk.tileIndex.level < dl) {
chunk.status = Chunk::Status::WantSplit;
}
else {
chunk.status = Chunk::Status::DoNothing;
}
}
} // namespace openspace::globebrowsing