Files
OpenSpace/modules/globebrowsing/src/renderableglobe.cpp
Alexander Bock 612b9bbc7f Moving more documentation to use codegen (#1549)
- Make use of more codegen in more classes
- Fix verifier for Color4Verifier
2021-03-29 21:50:26 +02:00

2568 lines
95 KiB
C++

/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2021 *
* *
* 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/renderableglobe.h>
#include <modules/globebrowsing/src/tileprovider.h>
#include <openspace/documentation/documentation.h>
#include <openspace/documentation/verifier.h>
#include <openspace/engine/globals.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/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 {
// Global flags to modify the RenderableGlobe
constexpr const bool LimitLevelByAvailableData = true;
constexpr const bool PerformFrustumCulling = true;
constexpr const 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, 1e35)
};
constexpr const 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 const int DefaultSkirtedGridSegments = 64;
constexpr const int UnknownDesiredLevel = -1;
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",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo ShowChunkBoundsInfo = {
"ShowChunkBounds",
"Show chunk bounds",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo HeightResolutionInfo = {
"ShowHeightResolution",
"Show height resolution",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo HeightIntensityInfo = {
"ShowHeightIntensities",
"Show height intensities",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo LevelProjectedAreaInfo = {
"LevelByProjectedAreaElseDistance",
"Level by projected area (else distance)",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo ResetTileProviderInfo = {
"ResetTileProviders",
"Reset tile providers",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo ModelSpaceRenderingInfo = {
"ModelSpaceRenderingCutoffLevel",
"Model Space Rendering Cutoff Level",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo DynamicLodIterationCountInfo =
{
"DynamicLodIterationCount",
"Data availability checks before LOD factor impact",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo PerformShadingInfo = {
"PerformShading",
"Perform shading",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo AccurateNormalsInfo = {
"UseAccurateNormals",
"Use Accurate Normals",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo EclipseInfo = {
"Eclipse",
"Eclipse",
"Enables/Disable Eclipse shadows"
};
constexpr openspace::properties::Property::PropertyInfo EclipseHardShadowsInfo = {
"EclipseHardShadows",
"Eclipse Hard Shadows",
"Enables the rendering of eclipse shadows using hard shadows"
};
constexpr openspace::properties::Property::PropertyInfo ShadowMappingInfo = {
"ShadowMapping",
"Shadow Mapping",
"Enables shadow mapping algorithm. Used by renderable rings too."
};
constexpr openspace::properties::Property::PropertyInfo ZFightingPercentageInfo = {
"ZFightingPercentage",
"Z-Fighting Percentage",
"The percentage of the correct distance to the surface being shadowed. "
"Possible values: [0.0, 1.0]"
};
constexpr openspace::properties::Property::PropertyInfo NumberShadowSamplesInfo = {
"NumberShadowSamples",
"Number of Shadow Samples",
"The number of samples used during shadow mapping calculation "
"(Percentage Closer Filtering)."
};
constexpr openspace::properties::Property::PropertyInfo TargetLodScaleFactorInfo = {
"TargetLodScaleFactor",
"Target Level of Detail Scale Factor",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo CurrentLodScaleFactorInfo = {
"CurrentLodScaleFactor",
"Current Level of Detail Scale Factor (Read Only)",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo CameraMinHeightInfo = {
"CameraMinHeight",
"Camera Minimum Height",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo OrenNayarRoughnessInfo = {
"OrenNayarRoughness",
"orenNayarRoughness",
"" // @TODO Missing documentation
};
constexpr openspace::properties::Property::PropertyInfo NActiveLayersInfo = {
"NActiveLayers",
"Number of active layers",
"This is the number of currently active layers, if this value reaches the "
"maximum, bad things will happen."
};
struct [[codegen::Dictionary(RenderableGlobe)]] Parameters {
// Specifies the radii for this planet. If the Double version of this is used, all
// three radii are assumed to be equal
std::optional<std::variant<glm::dvec3, double>> radii;
// Specifies whether the planet should be shaded by the primary light source or
// not. If it is disabled, all parts of the planet are illuminated
std::optional<bool> performShading;
// A list of all the layers that should be added
std::map<std::string, ghoul::Dictionary> layers
[[codegen::reference("globebrowsing_layermanager")]];
// 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;
};
std::vector<Source> sources;
struct Caster {
std::string name;
double radius;
};
std::vector<Caster> casters;
};
std::optional<ShadowGroup> shadowGroup;
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(
tileprovider::chunkTile(*layer->tileProvider(), 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(layergroupid::GroupID::HeightLayers);
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(layergroupid::GroupID::ColorLayers);
for (Layer* lyr : colorLayers.activeLayers()) {
if (lyr->tileProvider()) {
ChunkTile t = tileprovider::chunkTile(*lyr->tileProvider(), 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;
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);
}
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);
}
} // namespace
Chunk::Chunk(const TileIndex& ti)
: tileIndex(ti)
, surfacePatch(ti)
, status(Status::DoNothing)
{}
documentation::Documentation RenderableGlobe::Documentation() {
documentation::Documentation doc = codegen::doc<Parameters>();
doc.id = "globebrowsing_renderableglobe";
return doc;
}
RenderableGlobe::RenderableGlobe(const ghoul::Dictionary& dictionary)
: Renderable(dictionary)
, _debugProperties({
BoolProperty(ShowChunkEdgeInfo, false),
BoolProperty(ShowChunkBoundsInfo, false),
BoolProperty(HeightResolutionInfo, false),
BoolProperty(HeightIntensityInfo, false),
BoolProperty(LevelProjectedAreaInfo, true),
BoolProperty(ResetTileProviderInfo, false),
IntProperty(ModelSpaceRenderingInfo, 14, 1, 22),
IntProperty(DynamicLodIterationCountInfo, 16, 4, 128)
})
, _generalProperties({
BoolProperty(PerformShadingInfo, true),
BoolProperty(AccurateNormalsInfo, false),
BoolProperty(EclipseInfo, false),
BoolProperty(EclipseHardShadowsInfo, false),
BoolProperty(ShadowMappingInfo, false),
FloatProperty(ZFightingPercentageInfo, 0.995f, 0.000001f, 1.f),
IntProperty(NumberShadowSamplesInfo, 5, 1, 7),
FloatProperty(TargetLodScaleFactorInfo, 15.f, 1.f, 50.f),
FloatProperty(CurrentLodScaleFactorInfo, 15.f, 1.f, 50.f),
FloatProperty(CameraMinHeightInfo, 100.f, 0.f, 1000.f),
FloatProperty(OrenNayarRoughnessInfo, 0.f, 0.f, 1.f),
IntProperty(NActiveLayersInfo, 0, 0, OpenGLCap.maxTextureUnits() / 3)
})
, _debugPropertyOwner({ "Debug" })
, _shadowMappingPropertyOwner({ "ShadowMapping" })
, _grid(DefaultSkirtedGridSegments, DefaultSkirtedGridSegments)
, _leftRoot(Chunk(LeftHemisphereIndex))
, _rightRoot(Chunk(RightHemisphereIndex))
, _ringsComponent(dictionary)
, _shadowComponent(dictionary)
{
const Parameters p = codegen::bake<Parameters>(dictionary);
_generalProperties.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(static_cast<float>(_ellipsoid.maximumRadius()));
}
else if (std::holds_alternative<double>(*p.radii)) {
const double radius = std::get<double>(*p.radii);
_ellipsoid = Ellipsoid({ radius, radius, radius });
setBoundingSphere(static_cast<float>(_ellipsoid.maximumRadius()));
}
else {
throw ghoul::MissingCaseException();
}
}
_generalProperties.performShading =
p.performShading.value_or(_generalProperties.performShading);
// Init layer manager
// @TODO (abock, 2021-03-25) The layermanager should be changed to take a
// std::map<std::string, ghoul::Dictionary> instead and then we don't need to get it
// as a bare dictionary anymore and can use the value from the struct directly
ghoul::Dictionary layersDictionary = dictionary.value<ghoul::Dictionary>("Layers");
_layerManager.initialize(layersDictionary);
addProperty(_generalProperties.performShading);
addProperty(_generalProperties.useAccurateNormals);
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>(source.name, source.radius);
shadowConfArray.push_back(sc);
}
}
_ellipsoid.setShadowConfigurationArray(shadowConfArray);
addProperty(_generalProperties.eclipseShadowsEnabled);
addProperty(_generalProperties.eclipseHardShadows);
}
_shadowMappingPropertyOwner.addProperty(_generalProperties.shadowMapping);
_shadowMappingPropertyOwner.addProperty(_generalProperties.zFightingPercentage);
_shadowMappingPropertyOwner.addProperty(_generalProperties.nShadowSamples);
_generalProperties.nShadowSamples.onChange([&]() {
_shadersNeedRecompilation = true;
});
addPropertySubOwner(_shadowMappingPropertyOwner);
_generalProperties.targetLodScaleFactor.onChange([this]() {
float sf = _generalProperties.targetLodScaleFactor;
_generalProperties.currentLodScaleFactor = sf;
_lodScaleFactorDirty = true;
});
addProperty(_generalProperties.targetLodScaleFactor);
addProperty(_generalProperties.currentLodScaleFactor);
addProperty(_generalProperties.cameraMinHeight);
addProperty(_generalProperties.orenNayarRoughness);
_generalProperties.nActiveLayers.setReadOnly(true);
addProperty(_generalProperties.nActiveLayers);
_debugPropertyOwner.addProperty(_debugProperties.showChunkEdges);
//_debugPropertyOwner.addProperty(_debugProperties.showChunkBounds);
//_debugPropertyOwner.addProperty(_debugProperties.showChunkAABB);
//_debugPropertyOwner.addProperty(_debugProperties.showHeightResolution);
//_debugPropertyOwner.addProperty(_debugProperties.showHeightIntensities);
_debugPropertyOwner.addProperty(_debugProperties.levelByProjectedAreaElseDistance);
_debugPropertyOwner.addProperty(_debugProperties.resetTileProviders);
_debugPropertyOwner.addProperty(_debugProperties.modelSpaceRenderingCutoffLevel);
_debugPropertyOwner.addProperty(_debugProperties.dynamicLodIterationCount);
auto notifyShaderRecompilation = [&]() {
_shadersNeedRecompilation = true;
};
_generalProperties.useAccurateNormals.onChange(notifyShaderRecompilation);
_generalProperties.eclipseShadowsEnabled.onChange(notifyShaderRecompilation);
_generalProperties.eclipseHardShadows.onChange(notifyShaderRecompilation);
_generalProperties.performShading.onChange(notifyShaderRecompilation);
_debugProperties.showChunkEdges.onChange(notifyShaderRecompilation);
_debugProperties.showHeightResolution.onChange(notifyShaderRecompilation);
_debugProperties.showHeightIntensities.onChange(notifyShaderRecompilation);
_layerManager.onChange([&](Layer* l) {
_shadersNeedRecompilation = true;
_chunkCornersDirty = true;
_nLayersIsDirty = true;
_lastChangedLayer = l;
});
addPropertySubOwner(_debugPropertyOwner);
addPropertySubOwner(_layerManager);
_globalChunkBuffer.resize(2048);
_localChunkBuffer.resize(2048);
_traversalMemory.resize(512);
_labelsDictionary = p.labels.value_or(_labelsDictionary);
// Components
_hasRings = p.rings.has_value();
if (_hasRings) {
_ringsComponent.initialize();
addPropertySubOwner(_ringsComponent);
}
_hasShadows = p.shadows.has_value();
if (_hasShadows) {
_shadowComponent.initialize();
addPropertySubOwner(_shadowComponent);
_generalProperties.shadowMapping = true;
}
_generalProperties.shadowMapping.onChange(notifyShaderRecompilation);
}
void RenderableGlobe::initializeGL() {
if (!_labelsDictionary.isEmpty()) {
_globeLabelsComponent.initialize(_labelsDictionary, this);
addPropertySubOwner(_globeLabelsComponent);
}
_layerManager.update();
_grid.initializeGL();
if (_hasRings) {
_ringsComponent.initializeGL();
}
if (_hasShadows) {
_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;
}
_grid.deinitializeGL();
if (_hasRings) {
_ringsComponent.deinitializeGL();
}
if (_hasShadows) {
_shadowComponent.deinitializeGL();
}
}
bool RenderableGlobe::isReady() const {
return true;
}
void RenderableGlobe::render(const RenderData& data, RendererTasks& rendererTask) {
const double distanceToCamera = 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) {
try {
// Before Shadows
_globeLabelsComponent.draw(data);
if (_hasShadows && _shadowComponent.isEnabled()) {
// Set matrices and other GL states
RenderData lightRenderData(_shadowComponent.begin(data));
glDisable(GL_BLEND);
// Render from light source point of view
renderChunks(lightRenderData, rendererTask, {}, true);
if (_hasRings && _ringsComponent.isEnabled()) {
_ringsComponent.draw(lightRenderData, RingsComponent::GeometryOnly);
}
glEnable(GL_BLEND);
_shadowComponent.setViewDepthMap(false);
_shadowComponent.end();
// Render again from original point of view
renderChunks(data, rendererTask, _shadowComponent.shadowMapData());
if (_hasRings && _ringsComponent.isEnabled()) {
_ringsComponent.draw(
data,
RingsComponent::GeometryAndShading,
_shadowComponent.shadowMapData()
);
}
}
else {
renderChunks(data, rendererTask);
if (_hasRings && _ringsComponent.isEnabled()) {
_ringsComponent.draw(data, RingsComponent::GeometryAndShading);
}
}
}
catch (const ghoul::opengl::TextureUnit::TextureUnitError&) {
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;
}
void RenderableGlobe::update(const UpdateData& data) {
ZoneScoped
if (_localRenderer.program && _localRenderer.program->isDirty()) {
_localRenderer.program->rebuildFromFile();
_localRenderer.program->setUniform("xSegments", _grid.xSegments);
if (_debugProperties.showHeightResolution) {
_localRenderer.program->setUniform(
"vertexResolution",
glm::vec2(_grid.xSegments, _grid.ySegments)
);
}
ghoul::opengl::updateUniformLocations(
*_localRenderer.program,
_localRenderer.uniformCache,
{ "skirtLength", "p01", "p11", "p00", "p10", "patchNormalCameraSpace" }
);
}
if (_globalRenderer.program && _globalRenderer.program->isDirty()) {
_globalRenderer.program->rebuildFromFile();
_globalRenderer.program->setUniform("xSegments", _grid.xSegments);
if (_debugProperties.showHeightResolution) {
_globalRenderer.program->setUniform(
"vertexResolution",
glm::vec2(_grid.xSegments, _grid.ySegments)
);
}
// Ellipsoid Radius (Model Space)
_globalRenderer.program->setUniform(
"radiiSquared",
glm::vec3(_ellipsoid.radii() * _ellipsoid.radii())
);
ghoul::opengl::updateUniformLocations(
*_globalRenderer.program,
_globalRenderer.uniformCache,
{ "skirtLength", "minLatLon", "lonLatScalingFactor" }
);
}
setBoundingSphere(static_cast<float>(
_ellipsoid.maximumRadius() * glm::compMax(data.modelTransform.scale)
));
glm::dmat4 translation =
glm::translate(glm::dmat4(1.0), data.modelTransform.translation);
glm::dmat4 rotation = glm::dmat4(data.modelTransform.rotation);
glm::dmat4 scaling = glm::scale(glm::dmat4(1.0), data.modelTransform.scale);
_cachedModelTransform = translation * rotation * scaling;
_cachedInverseModelTransform = glm::inverse(_cachedModelTransform);
if (_debugProperties.resetTileProviders) {
_layerManager.reset();
_debugProperties.resetTileProviders = false;
}
if (_hasRings) {
_ringsComponent.update(data);
}
if (_hasShadows) {
_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;
}
bool RenderableGlobe::renderedWithDesiredData() const {
return _allChunksAvailable;
}
const LayerManager& RenderableGlobe::layerManager() const {
return _layerManager;
}
LayerManager& RenderableGlobe::layerManager() {
return _layerManager;
}
const Ellipsoid& RenderableGlobe::ellipsoid() const {
return _ellipsoid;
}
const glm::dmat4& RenderableGlobe::modelTransform() const {
return _cachedModelTransform;
}
//////////////////////////////////////////////////////////////////////////////////////////
// Rendering code
//////////////////////////////////////////////////////////////////////////////////////////
void RenderableGlobe::renderChunks(const RenderData& data, RendererTasks&,
const ShadowComponent::ShadowMapData& shadowData,
bool renderGeomOnly)
{
ZoneScoped
if (_layerManagerDirty) {
_layerManager.update();
_layerManagerDirty = false;
}
if (_nLayersIsDirty) {
std::array<LayerGroup*, LayerManager::NumLayerGroups> lgs =
_layerManager.layerGroups();
_generalProperties.nActiveLayers = std::accumulate(
lgs.begin(),
lgs.end(),
0,
[](int lhs, LayerGroup* lg) {
return lhs + static_cast<int>(lg->activeLayers().size());
}
);
_nLayersIsDirty = false;
}
if (_shadersNeedRecompilation) {
recompileShaders();
}
//
// Setting frame-const uniforms that are not view dependent
//
if (_layerManager.hasAnyBlendingLayersEnabled()) {
if (_lodScaleFactorDirty) {
const float dsf = static_cast<float>(
_generalProperties.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 (_generalProperties.performShading) {
const float onr = _generalProperties.orenNayarRoughness;
_localRenderer.program->setUniform("orenNayarRoughness", onr);
_globalRenderer.program->setUniform("orenNayarRoughness", onr);
}
if (_globalRenderer.updatedSinceLastCall) {
const std::array<LayerGroup*, LayerManager::NumLayerGroups>& layerGroups =
_layerManager.layerGroups();
for (size_t i = 0; i < layerGroups.size(); ++i) {
const std::string& nameBase = layergroupid::LAYER_GROUP_IDENTIFIERS[i];
_globalRenderer.gpuLayerGroups[i].bind(
*_globalRenderer.program,
*layerGroups[i],
nameBase,
static_cast<int>(i)
);
}
const float dsf = static_cast<float>(
_generalProperties.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) {
const std::string& nameBase = layergroupid::LAYER_GROUP_IDENTIFIERS[i];
_localRenderer.gpuLayerGroups[i].bind(
*_localRenderer.program,
*layerGroups[i],
nameBase,
static_cast<int>(i)
);
}
const float dsf = static_cast<float>(
_generalProperties.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)
);
_globalRenderer.program->setUniform("cameraPosition", glm::vec3(cameraPosition));
}
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(
layergroupid::HeightLayers
).activeLayers().empty();
if (_generalProperties.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()
)
);
}
const bool nightLayersActive =
!_layerManager.layerGroup(layergroupid::NightLayers).activeLayers().empty();
const bool waterLayersActive =
!_layerManager.layerGroup(layergroupid::WaterMasks).activeLayers().empty();
if (nightLayersActive || waterLayersActive || _generalProperties.performShading) {
const glm::dvec3 directionToSunWorldSpace =
length(data.modelTransform.translation) > 0.0 ?
glm::normalize(-data.modelTransform.translation) :
glm::dvec3(0.0);
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 || _generalProperties.performShading) {
const glm::dvec3 directionToSunWorldSpace =
length(data.modelTransform.translation) > 0.0 ?
glm::normalize(-data.modelTransform.translation) :
glm::dvec3(0.0);
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 (_debugProperties.showChunkBounds) {
for (int i = 0; i < globalCount; ++i) {
debugRenderChunk(
*_globalChunkBuffer[i],
mvp,
_debugProperties.showChunkBounds
);
}
for (int i = 0; i < localCount; ++i) {
debugRenderChunk(
*_localChunkBuffer[i],
mvp,
_debugProperties.showChunkBounds
);
}
}
// 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 = _generalProperties.currentLodScaleFactor;
const float clfMin = _generalProperties.currentLodScaleFactor.minValue();
const float targetLod = _generalProperties.targetLodScaleFactor;
const bool validLodFactor = clf > clfMin;
if (exceededIterations && validLodFactor) {
_generalProperties.currentLodScaleFactor =
_generalProperties.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)
{
_generalProperties.currentLodScaleFactor =
_generalProperties.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 (_generalProperties.eclipseShadowsEnabled &&
!_ellipsoid.shadowConfigurationArray().empty())
{
calculateEclipseShadows(program, data, ShadowCompType::GLOBAL_SHADOW);
}
// Shadow Mapping
ghoul::opengl::TextureUnit shadowMapUnit;
if (_generalProperties.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", _generalProperties.zFightingPercentage);
}
else if (_generalProperties.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 : _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)
);
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 = normalize(
cross(
cornersCameraSpace[Quad::SOUTH_EAST] - cornersCameraSpace[Quad::SOUTH_WEST],
cornersCameraSpace[Quad::NORTH_EAST] - cornersCameraSpace[Quad::SOUTH_WEST]
)
);
program.setUniform(
_localRenderer.uniformCache.patchNormalCameraSpace,
patchNormalCameraSpace
);
if (!_layerManager.layerGroup(layergroupid::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 (_generalProperties.eclipseShadowsEnabled &&
!_ellipsoid.shadowConfigurationArray().empty())
{
calculateEclipseShadows(program, data, ShadowCompType::LOCAL_SHADOW);
}
// Shadow Mapping
ghoul::opengl::TextureUnit shadowMapUnit;
if (_generalProperties.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", _generalProperties.zFightingPercentage);
}
else if (_generalProperties.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)
{
ZoneScoped
if (_generalProperties.useAccurateNormals &&
!_layerManager.layerGroup(layergroupid::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 64 * 64
// 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 64 this will most likely work fine.
constexpr const float TileDelta = 1.f / DefaultSkirtedGridSegments;
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<layergroupid::TypeID> layerType;
std::vector<layergroupid::BlendModeID> blendMode;
std::vector<layergroupid::AdjustmentTypeID> layerAdjustmentType;
};
std::array<LayerGroupPreprocessingData, layergroupid::NUM_LAYER_GROUPS>
layeredTextureInfo;
std::vector<std::pair<std::string, std::string>> keyValuePairs;
};
//
// Create LayerShaderPreprocessingData
//
LayerShaderPreprocessingData preprocessingData;
for (size_t i = 0; i < layergroupid::NUM_LAYER_GROUPS; i++) {
LayerShaderPreprocessingData::LayerGroupPreprocessingData layeredTextureInfo;
const LayerGroup& layerGroup = _layerManager.layerGroup(layergroupid::GroupID(i));
const std::vector<Layer*>& layers = layerGroup.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>(
layerGroup.activeLayers().size() - 1
);
layeredTextureInfo.layerBlendingEnabled = layerGroup.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(
layergroupid::HeightLayers
).activeLayers().empty();
pairs.emplace_back("useAccurateNormals",
std::to_string(_generalProperties.useAccurateNormals && hasHeightLayer)
);
pairs.emplace_back(
"performShading",
std::to_string(_generalProperties.performShading)
);
pairs.emplace_back(
"useEclipseShadows",
std::to_string(_generalProperties.eclipseShadowsEnabled)
);
pairs.emplace_back(
"useEclipseHardShadows",
std::to_string(_generalProperties.eclipseHardShadows)
);
pairs.emplace_back(
"enableShadowMapping",
std::to_string(_generalProperties.shadowMapping)
);
pairs.emplace_back("showChunkEdges", std::to_string(_debugProperties.showChunkEdges));
pairs.emplace_back("showHeightResolution",
std::to_string(_debugProperties.showHeightResolution)
);
pairs.emplace_back("showHeightIntensities",
std::to_string(_debugProperties.showHeightIntensities)
);
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
const std::string& groupName = layergroupid::LAYER_GROUP_IDENTIFIERS[i];
shaderDictionary.setValue(
"lastLayerIndex" + groupName,
glm::max(preprocessingData.layeredTextureInfo[i].lastLayerIdx, 0)
);
shaderDictionary.setValue(
"use" + groupName,
preprocessingData.layeredTextureInfo[i].lastLayerIdx >= 0
);
shaderDictionary.setValue(
"blend" + groupName,
preprocessingData.layeredTextureInfo[i].layerBlendingEnabled
);
// This is to avoid errors from shader preprocessor
shaderDictionary.setValue(groupName + "0" + "LayerType", 0);
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 keyLayerAdjustmentType = groupName + "0" + "LayerAdjustmentType";
shaderDictionary.setValue(keyLayerAdjustmentType, 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 (int i = 0; i < layergroupid::NUM_LAYER_GROUPS; ++i) {
layerGroupNames.setValue(
std::to_string(i),
std::string(layergroupid::LAYER_GROUP_IDENTIFIERS[i])
);
}
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", _generalProperties.nShadowSamples - 1);
// Exclise Shadow Samples
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);
if (_debugProperties.showHeightResolution) {
_localRenderer.program->setUniform(
"vertexResolution",
glm::vec2(_grid.xSegments, _grid.ySegments)
);
}
ghoul::opengl::updateUniformLocations(
*_localRenderer.program,
_localRenderer.uniformCache,
{ "skirtLength", "p01", "p11", "p00", "p10", "patchNormalCameraSpace" }
);
//
// 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);
if (_debugProperties.showHeightResolution) {
_globalRenderer.program->setUniform(
"vertexResolution",
glm::vec2(_grid.xSegments, _grid.ySegments)
);
}
// Ellipsoid Radius (Model Space)
_globalRenderer.program->setUniform(
"radiiSquared",
glm::vec3(_ellipsoid.radii() * _ellipsoid.radii())
);
ghoul::opengl::updateUniformLocations(
*_globalRenderer.program,
_globalRenderer.uniformCache,
{ "skirtLength", "minLatLon", "lonLatScalingFactor" }
);
_globalRenderer.updatedSinceLastCall = true;
_shadersNeedRecompilation = false;
}
SurfacePositionHandle RenderableGlobe::calculateSurfacePositionHandle(
const glm::dvec3& targetModelSpace) const
{
ZoneScoped
glm::dvec3 centerToEllipsoidSurface =
_ellipsoid.geodeticSurfaceProjection(targetModelSpace);
glm::dvec3 ellipsoidSurfaceToTarget = 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(ellipsoidSurfaceToTarget);
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) * static_cast<double>(boundingSphere())) :
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)) ||
(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;
//TileIndex::TileIndex(const Geodetic2& point, int level_)
// : level(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 = {
northEast.lat - southWest.lat,
northEast.lon - southWest.lon
};
const Geodetic2 geoDiffPoint = {
geodeticPosition.lat - southWest.lat,
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(layergroupid::GroupID::HeightLayers).activeLayers();
for (Layer* layer : heightMapLayers) {
tileprovider::TileProvider* tileProvider = layer->tileProvider();
if (!tileProvider) {
continue;
}
// Transform the uv coordinates to the current tile texture
const ChunkTile chunkTile = tileprovider::chunkTile(*tileProvider, tileIndex);
const Tile& tile = chunkTile.tile;
const TileUvTransform& uvTransform = chunkTile.uvTransform;
const TileDepthTransform& depthTransform =
tileprovider::depthTransform(*tileProvider);
if (tile.status != Tile::Status::OK) {
return 0;
}
ghoul::opengl::Texture* tileTexture = tile.texture;
if (!tileTexture) {
return 0;
}
glm::vec2 transformedUv = layer->tileUvToTextureSamplePosition(
uvTransform,
patchUV,
glm::uvec2(tileTexture->dimensions())
);
// 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();
const glm::vec2 samplePos = transformedUv * glm::vec2(dimensions);
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(*tileProvider) ||
sample01 == tileprovider::noDataValueAsFloat(*tileProvider) ||
sample10 == tileprovider::noDataValueAsFloat(*tileProvider) ||
sample11 == tileprovider::noDataValueAsFloat(*tileProvider);
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 const double KM_TO_M = 1000.0;
ghoul_assert(
!_ellipsoid.shadowConfigurationArray().empty(),
"Needs to have eclipse shadows enabled"
);
// Shadow calculations..
std::vector<ShadowRenderingStruct> shadowDataArray;
std::vector<Ellipsoid::ShadowConfiguration> shadowConfArray =
_ellipsoid.shadowConfigurationArray();
shadowDataArray.reserve(shadowConfArray.size());
double lt;
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,
"SUN",
"GALACTIC",
{},
data.time.j2000Seconds(),
lt
);
sourcePos *= KM_TO_M; // converting to meters
glm::dvec3 casterPos = SpiceManager::ref().targetPosition(
shadowConf.caster.first,
"SUN",
"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);
const double sourceRadiusScale = std::max(
glm::compMax(sourceNode->scale()),
1.0
);
const double casterRadiusScale = std::max(
glm::compMax(casterNode->scale()),
1.0
);
if ((sourceNode == nullptr) || (casterNode == nullptr)) {
LERRORC(
"Renderableglobe",
"Invalid scenegraph node for the shadow's caster or shadow's receiver."
);
return;
}
// 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",
"SUN",
"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 const char* NameIsShadowing = "shadowDataArray[{}].isShadowing";
constexpr const char* NameXp = "shadowDataArray[{}].xp";
constexpr const char* NameXu = "shadowDataArray[{}].xu";
constexpr const char* NameRc = "shadowDataArray[{}].rc";
constexpr const char* NameSource = "shadowDataArray[{}].sourceCasterVec";
constexpr const char* NamePos = "shadowDataArray[{}].casterPositionVec";
programObject.setUniform(
fmt::format(NameIsShadowing, counter), sd.isShadowing
);
if (sd.isShadowing) {
programObject.setUniform(fmt::format(NameXp, counter), sd.xp);
programObject.setUniform(fmt::format(NameXu, counter), sd.xu);
programObject.setUniform(fmt::format(NameRc, counter), sd.rc);
programObject.setUniform(
fmt::format(NameSource, counter), sd.sourceCasterVec
);
programObject.setUniform(
fmt::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",
_generalProperties.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 = _generalProperties.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 = _generalProperties.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 (size_t i = 0; i < layergroupid::NUM_LAYER_GROUPS; ++i) {
for (Layer* layer :
_layerManager.layerGroup(layergroupid::GroupID(i)).activeLayers())
{
Tile::Status status = layer->tileStatus(chunk.tileIndex);
if (status == Tile::Status::OK) {
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); // In model space it is 0
const double minimumGlobeRadius = _ellipsoid.minimumRadius();
const glm::dvec3 cameraPos = glm::dvec3(
_cachedInverseModelTransform * glm::dvec4(renderData.camera.positionVec3(), 1)
);
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 = pow(length(objectPos - globePos), 2);
const double horizonP = pow(minimumGlobeRadius - maxHeight, 2);
if (objectP < horizonP) {
return false;
}
const double cameraP = pow(length(cameraPos - globePos), 2);
const double minR = pow(minimumGlobeRadius, 2);
if (cameraP < minR) {
return false;
}
const double minimumAllowedDistanceToObjectFromHorizon = sqrt(objectP - horizonP);
const double distanceToHorizon = sqrt(cameraP - minR);
// Minimum allowed for the object to be occluded
const double minimumAllowedDistanceToObjectSquared =
pow(distanceToHorizon + minimumAllowedDistanceToObjectFromHorizon, 2) +
pow(maxHeight, 2);
const double distanceToObjectSquared = pow(
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