mirror of
https://github.com/OpenSpace/OpenSpace.git
synced 2026-01-08 04:31:08 -06:00
1400 lines
52 KiB
C++
1400 lines
52 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/gaia/rendering/octreemanager.h>
|
|
|
|
#include <modules/gaia/rendering/octreeculler.h>
|
|
#include <openspace/util/distanceconstants.h>
|
|
#include <ghoul/fmt.h>
|
|
#include <ghoul/glm.h>
|
|
#include <ghoul/logging/logmanager.h>
|
|
#include <fstream>
|
|
#include <thread>
|
|
|
|
namespace {
|
|
constexpr const char* _loggerCat = "OctreeManager";
|
|
} // namespace
|
|
|
|
namespace openspace {
|
|
|
|
void OctreeManager::initOctree(long long cpuRamBudget, int maxDist, int maxStarsPerNode) {
|
|
if (_root) {
|
|
LDEBUG("Clear existing Octree");
|
|
clearAllData();
|
|
}
|
|
|
|
LDEBUG("Initializing new Octree");
|
|
_root = std::make_shared<OctreeNode>();
|
|
_root->octreePositionIndex = 8;
|
|
|
|
// Initialize the culler. The NDC.z of the comparing corners are always -1 or 1.
|
|
globebrowsing::AABB3 box;
|
|
box.min = glm::vec3(-1.f, -1.f, 0.f);
|
|
box.max = glm::vec3(1.f, 1.f, 100.f);
|
|
_culler = std::make_unique<OctreeCuller>(box);
|
|
_removedKeysInPrevCall = std::set<int>();
|
|
_leastRecentlyFetchedNodes = std::queue<unsigned long long>();
|
|
|
|
// Reset default values when rebuilding the Octree during runtime.
|
|
_numInnerNodes = 0;
|
|
_numLeafNodes = 0;
|
|
_totalDepth = 0;
|
|
_valuesPerStar = POS_SIZE + COL_SIZE + VEL_SIZE;
|
|
_maxCpuRamBudget = cpuRamBudget;
|
|
_cpuRamBudget = cpuRamBudget;
|
|
_parentNodeOfCamera = 8;
|
|
|
|
if (maxDist > 0) {
|
|
MAX_DIST = static_cast<size_t>(maxDist);
|
|
}
|
|
if (maxStarsPerNode > 0) {
|
|
MAX_STARS_PER_NODE = static_cast<size_t>(maxStarsPerNode);
|
|
}
|
|
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
_numLeafNodes++;
|
|
_root->Children[i] = std::make_shared<OctreeNode>();
|
|
_root->Children[i]->posData = std::vector<float>();
|
|
_root->Children[i]->colData = std::vector<float>();
|
|
_root->Children[i]->velData = std::vector<float>();
|
|
_root->Children[i]->magOrder = std::vector<std::pair<float, size_t>>();
|
|
_root->Children[i]->isLeaf = true;
|
|
_root->Children[i]->isLoaded = false;
|
|
_root->Children[i]->hasLoadedDescendant = false;
|
|
_root->Children[i]->bufferIndex = DEFAULT_INDEX;
|
|
_root->Children[i]->octreePositionIndex = 80 + i;
|
|
_root->Children[i]->numStars = 0;
|
|
_root->Children[i]->halfDimension = MAX_DIST / 2.f;
|
|
_root->Children[i]->originX = (i % 2 == 0) ?
|
|
_root->Children[i]->halfDimension :
|
|
-_root->Children[i]->halfDimension;
|
|
_root->Children[i]->originY = (i % 4 < 2) ?
|
|
_root->Children[i]->halfDimension :
|
|
-_root->Children[i]->halfDimension;
|
|
_root->Children[i]->originZ = (i < 4) ?
|
|
_root->Children[i]->halfDimension :
|
|
-_root->Children[i]->halfDimension;
|
|
}
|
|
}
|
|
|
|
void OctreeManager::initBufferIndexStack(long long maxNodes, bool useVBO,
|
|
bool datasetFitInMemory)
|
|
{
|
|
// Clear stack if we've used it before.
|
|
_biggestChunkIndexInUse = 0;
|
|
_freeSpotsInBuffer = std::stack<int>();
|
|
_rebuildBuffer = true;
|
|
_useVBO = useVBO;
|
|
_datasetFitInMemory = datasetFitInMemory;
|
|
|
|
// Build stack back-to-front.
|
|
for (long long idx = maxNodes - 1; idx >= 0; --idx) {
|
|
_freeSpotsInBuffer.push(static_cast<int>(idx));
|
|
}
|
|
_maxStackSize = _freeSpotsInBuffer.size();
|
|
LINFO("StackSize: " + std::to_string(maxNodes));
|
|
}
|
|
|
|
void OctreeManager::insert(const std::vector<float>& starValues) {
|
|
size_t index = getChildIndex(starValues[0], starValues[1], starValues[2]);
|
|
|
|
insertInNode(*_root->Children[index], starValues);
|
|
}
|
|
|
|
void OctreeManager::sliceLodData(size_t branchIndex) {
|
|
if (branchIndex != 8) {
|
|
sliceNodeLodCache(*_root->Children[branchIndex]);
|
|
}
|
|
else {
|
|
for (int i = 0; i < 7; ++i) {
|
|
sliceNodeLodCache(*_root->Children[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OctreeManager::printStarsPerNode() const {
|
|
auto accumulatedString = std::string();
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
std::string prefix = "{" + std::to_string(i);
|
|
accumulatedString += printStarsPerNode(*_root->Children[i], prefix);
|
|
}
|
|
LINFO(fmt::format("Number of stars per node: \n{}", accumulatedString));
|
|
LINFO(fmt::format("Number of leaf nodes: {}", std::to_string(_numLeafNodes)));
|
|
LINFO(fmt::format("Number of inner nodes: {}", std::to_string(_numInnerNodes)));
|
|
LINFO(fmt::format("Depth of tree: {}", std::to_string(_totalDepth)));
|
|
}
|
|
|
|
void OctreeManager::fetchSurroundingNodes(const glm::dvec3& cameraPos,
|
|
size_t chunkSizeInBytes,
|
|
const glm::ivec2& additionalNodes)
|
|
{
|
|
|
|
// If entire dataset fits in RAM then load the entire dataset asynchronously now.
|
|
// Nodes will be rendered when they've been made available.
|
|
if (_datasetFitInMemory) {
|
|
// Only traverse Octree once!
|
|
if (_parentNodeOfCamera == 8) {
|
|
// Fetch first layer of children
|
|
fetchChildrenNodes(*_root, 0);
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
// Check so branch doesn't have a single layer.
|
|
if (_root->Children[i]->isLeaf) {
|
|
continue;
|
|
}
|
|
|
|
// Use multithreading to load files and detach thread from main execution
|
|
// so it can execute independently. Thread will be destroyed when
|
|
// finished!
|
|
std::thread([this, n = _root->Children[i]]() {
|
|
fetchChildrenNodes(*n, -1);
|
|
}).detach();
|
|
}
|
|
_parentNodeOfCamera = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Get leaf node in which the camera resides.
|
|
glm::vec3 fCameraPos = static_cast<glm::vec3>(
|
|
cameraPos / (1000.0 * distanceconstants::Parsec)
|
|
);
|
|
size_t idx = getChildIndex(fCameraPos.x, fCameraPos.y, fCameraPos.z);
|
|
std::shared_ptr<OctreeNode> node = _root->Children[idx];
|
|
|
|
while (!node->isLeaf) {
|
|
idx = getChildIndex(
|
|
fCameraPos.x,
|
|
fCameraPos.y,
|
|
fCameraPos.z,
|
|
node->originX,
|
|
node->originY,
|
|
node->originZ
|
|
);
|
|
node = node->Children[idx];
|
|
}
|
|
unsigned long long leafId = node->octreePositionIndex;
|
|
unsigned long long firstParentId = leafId / 10;
|
|
|
|
// Return early if camera resides in the same first parent as before!
|
|
// Otherwise camera has moved and may need to load more nodes!
|
|
if (_parentNodeOfCamera == firstParentId) {
|
|
return;
|
|
}
|
|
_parentNodeOfCamera = firstParentId;
|
|
|
|
// Each parent level may be root, make sure to propagate it in that case!
|
|
unsigned long long secondParentId = (firstParentId == 8) ? 8 : leafId / 100;
|
|
unsigned long long thirdParentId = (secondParentId == 8) ? 8 : leafId / 1000;
|
|
unsigned long long fourthParentId = (thirdParentId == 8) ? 8 : leafId / 10000;
|
|
unsigned long long fifthParentId = (fourthParentId == 8) ? 8 : leafId / 100000;
|
|
|
|
// Get the number of levels to fetch from user input.
|
|
int additionalLevelsToFetch = additionalNodes.y;
|
|
|
|
// Get more descendants when closer to root.
|
|
if (_parentNodeOfCamera < 80000) {
|
|
additionalLevelsToFetch++;
|
|
}
|
|
|
|
// Get the 3^3 closest parents and load all their (eventual) children.
|
|
for (int x = -1; x <= 1; x += 1) {
|
|
for (int y = -2; y <= 2; y += 2) {
|
|
for (int z = -4; z <= 4; z += 4) {
|
|
// Fetch all stars the 216 closest leaf nodes.
|
|
findAndFetchNeighborNode(
|
|
firstParentId,
|
|
x,
|
|
y,
|
|
z,
|
|
additionalLevelsToFetch
|
|
);
|
|
// Fetch LOD stars from 208 parents one and two layer(s) up.
|
|
if (x != 0 || y != 0 || z != 0) {
|
|
if (additionalNodes.x > 0) {
|
|
findAndFetchNeighborNode(
|
|
secondParentId,
|
|
x,
|
|
y,
|
|
z,
|
|
additionalLevelsToFetch
|
|
);
|
|
}
|
|
if (additionalNodes.x > 1) {
|
|
findAndFetchNeighborNode(
|
|
thirdParentId,
|
|
x,
|
|
y,
|
|
z,
|
|
additionalLevelsToFetch
|
|
);
|
|
}
|
|
if (additionalNodes.x > 2) {
|
|
findAndFetchNeighborNode(
|
|
fourthParentId,
|
|
x,
|
|
y,
|
|
z,
|
|
additionalLevelsToFetch
|
|
);
|
|
}
|
|
if (additionalNodes.x > 3) {
|
|
findAndFetchNeighborNode(
|
|
fifthParentId,
|
|
x,
|
|
y,
|
|
z,
|
|
additionalLevelsToFetch
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we should remove any nodes from RAM.
|
|
long long tenthOfRamBudget = _maxCpuRamBudget / 10;
|
|
if (_cpuRamBudget < tenthOfRamBudget) {
|
|
long long bytesToTenthOfRam = tenthOfRamBudget - _cpuRamBudget;
|
|
size_t nNodesToRemove = static_cast<size_t>(bytesToTenthOfRam / chunkSizeInBytes);
|
|
std::vector<unsigned long long> nodesToRemove;
|
|
while (nNodesToRemove > 0) {
|
|
// Dequeue nodes that were least recently fetched by findAndFetchNeighborNode.
|
|
nodesToRemove.push_back(_leastRecentlyFetchedNodes.front());
|
|
_leastRecentlyFetchedNodes.pop();
|
|
nNodesToRemove--;
|
|
}
|
|
// Use asynchronous removal.
|
|
if (!nodesToRemove.empty()) {
|
|
std::thread(&OctreeManager::removeNodesFromRam, this, nodesToRemove).detach();
|
|
}
|
|
}
|
|
}
|
|
|
|
void OctreeManager::findAndFetchNeighborNode(unsigned long long firstParentId, int x,
|
|
int y, int z, int additionalLevelsToFetch)
|
|
{
|
|
unsigned long long parentId = firstParentId;
|
|
auto indexStack = std::stack<int>();
|
|
|
|
// Fetch first layer children if we're already at root.
|
|
if (parentId == 8) {
|
|
fetchChildrenNodes(*_root, 0);
|
|
return;
|
|
}
|
|
|
|
//----------------- Change first index -------------------//
|
|
int nodeIndex = parentId % 10;
|
|
|
|
int dx = (nodeIndex % 2 == 0) ? 1 : -1;
|
|
int dy = (nodeIndex % 4 < 2) ? 2 : -2;
|
|
int dz = (nodeIndex < 4) ? 4 : -4;
|
|
|
|
// Determine if we need to switch any side (find a higher common parent).
|
|
bool needToSwitchX = (x == dx);
|
|
bool needToSwitchY = (y == dy);
|
|
bool needToSwitchZ = (z == dz);
|
|
|
|
if (!needToSwitchX) {
|
|
x = -x;
|
|
}
|
|
if (!needToSwitchY) {
|
|
y = -y;
|
|
}
|
|
if (!needToSwitchZ) {
|
|
z = -z;
|
|
}
|
|
|
|
// Update node index and store it at the back of our stack.
|
|
nodeIndex += x + y + z;
|
|
indexStack.push(nodeIndex);
|
|
parentId /= 10;
|
|
|
|
//--------- Change all indices until we find a common parent --------------//
|
|
while (parentId != 8 && (needToSwitchX || needToSwitchY || needToSwitchZ) ) {
|
|
nodeIndex = parentId % 10;
|
|
|
|
dx = (nodeIndex % 2 == 0) ? 1 : -1;
|
|
dy = (nodeIndex % 4 < 2) ? 2 : -2;
|
|
dz = (nodeIndex < 4) ? 4 : -4;
|
|
|
|
if (needToSwitchX) {
|
|
if (x != dx) {
|
|
needToSwitchX = false;
|
|
}
|
|
nodeIndex += dx;
|
|
}
|
|
if (needToSwitchY) {
|
|
if (y != dy) {
|
|
needToSwitchY = false;
|
|
}
|
|
nodeIndex += dy;
|
|
}
|
|
if (needToSwitchZ) {
|
|
if (z != dz) {
|
|
needToSwitchZ = false;
|
|
}
|
|
nodeIndex += dz;
|
|
}
|
|
|
|
indexStack.push(nodeIndex);
|
|
parentId /= 10;
|
|
}
|
|
|
|
// Take care of edge cases. If we got to the root but still need to switch to a
|
|
// common parent then no neighbor exists in that direction.
|
|
if (needToSwitchX || needToSwitchY || needToSwitchZ) {
|
|
return;
|
|
}
|
|
|
|
// Continue to root if we didn't reach it.
|
|
while (parentId != 8) {
|
|
nodeIndex = parentId % 10;
|
|
indexStack.push(nodeIndex);
|
|
parentId /= 10;
|
|
}
|
|
|
|
// Traverse to that parent node (as long as such a child exists!).
|
|
std::shared_ptr<OctreeNode> node = _root;
|
|
while (!indexStack.empty() && !node->Children[indexStack.top()]->isLeaf) {
|
|
node = node->Children[indexStack.top()];
|
|
node->hasLoadedDescendant = true;
|
|
indexStack.pop();
|
|
}
|
|
|
|
// Fetch all children nodes from found parent. Use multithreading to load files
|
|
// asynchronously! Detach thread from main execution so it can execute independently.
|
|
// Thread will then be destroyed when it has finished!
|
|
std::thread([this, node, additionalLevelsToFetch]() {
|
|
fetchChildrenNodes(*node, additionalLevelsToFetch);
|
|
}).detach();
|
|
}
|
|
|
|
std::map<int, std::vector<float>> OctreeManager::traverseData(const glm::dmat4& mvp,
|
|
const glm::vec2& screenSize,
|
|
int& deltaStars,
|
|
gaia::RenderOption option,
|
|
float lodPixelThreshold)
|
|
{
|
|
auto renderData = std::map<int, std::vector<float>>();
|
|
bool innerRebuild = false;
|
|
_minTotalPixelsLod = lodPixelThreshold;
|
|
|
|
// Reclaim indices from previous render call.
|
|
for (auto removedKey = _removedKeysInPrevCall.rbegin();
|
|
removedKey != _removedKeysInPrevCall.rend(); ++removedKey) {
|
|
|
|
// Uses a reverse loop to try to decrease the biggest chunk.
|
|
if (*removedKey == static_cast<int>(_biggestChunkIndexInUse) - 1) {
|
|
_biggestChunkIndexInUse = *removedKey;
|
|
LDEBUG(fmt::format(
|
|
"Decreased size to: {} Free Spots in VBO: {}",
|
|
_biggestChunkIndexInUse, _freeSpotsInBuffer.size()
|
|
));
|
|
}
|
|
_freeSpotsInBuffer.push(*removedKey);
|
|
}
|
|
// Clear cache of removed keys before next render call.
|
|
if (!_removedKeysInPrevCall.empty()) {
|
|
_removedKeysInPrevCall.clear();
|
|
}
|
|
|
|
// Rebuild VBO from scratch if we're not using most of it but have a high max index.
|
|
if ((_biggestChunkIndexInUse > _maxStackSize * 4 / 5) &&
|
|
(_freeSpotsInBuffer.size() > _maxStackSize * 5 / 6))
|
|
{
|
|
LDEBUG(fmt::format(
|
|
"Rebuilding VBO. Biggest Chunk: {} 4/5: {} FreeSpotsInVBO: {} 5/6: {}",
|
|
_biggestChunkIndexInUse, _maxStackSize * 4 / 5, _freeSpotsInBuffer.size(),
|
|
_maxStackSize * 5 / 6
|
|
));
|
|
initBufferIndexStack(_maxStackSize, _useVBO, _datasetFitInMemory);
|
|
innerRebuild = true;
|
|
}
|
|
|
|
// Check if entire tree is too small to see, and if so remove it.
|
|
std::vector<glm::dvec4> corners(8);
|
|
float fMaxDist = static_cast<float>(MAX_DIST);
|
|
for (int i = 0; i < 8; ++i) {
|
|
float x = (i % 2 == 0) ? fMaxDist : -fMaxDist;
|
|
float y = (i % 4 < 2) ? fMaxDist : -fMaxDist;
|
|
float z = (i < 4) ? fMaxDist : -fMaxDist;
|
|
glm::dvec3 pos = glm::dvec3(x, y, z) * 1000.0 * distanceconstants::Parsec;
|
|
corners[i] = glm::dvec4(pos, 1.0);
|
|
}
|
|
if (!_culler->isVisible(corners, mvp)) {
|
|
return renderData;
|
|
}
|
|
glm::vec2 nodeSize = _culler->getNodeSizeInPixels(corners, mvp, screenSize);
|
|
float totalPixels = nodeSize.x * nodeSize.y;
|
|
if (totalPixels < _minTotalPixelsLod * 2) {
|
|
// Remove LOD from first layer of children.
|
|
for (int i = 0; i < 8; ++i) {
|
|
std::map<int, std::vector<float>> tmpData = removeNodeFromCache(
|
|
*_root->Children[i],
|
|
deltaStars
|
|
);
|
|
renderData.insert(tmpData.begin(), tmpData.end());
|
|
}
|
|
return renderData;
|
|
}
|
|
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
if (i < _traversedBranchesInRenderCall) {
|
|
continue;
|
|
}
|
|
|
|
std::map<int, std::vector<float>> tmpData = checkNodeIntersection(
|
|
*_root->Children[i],
|
|
mvp,
|
|
screenSize,
|
|
deltaStars,
|
|
option
|
|
);
|
|
|
|
// Avoid freezing when switching render mode for large datasets by only fetching
|
|
// one branch at a time when rebuilding buffer.
|
|
if (_rebuildBuffer) {
|
|
//renderData = std::move(tmpData);
|
|
_traversedBranchesInRenderCall++;
|
|
//break;
|
|
}
|
|
|
|
// Observe that if there exists identical keys in renderData then those values in
|
|
// tmpData will be ignored! Thus we store the removed keys until next render call!
|
|
renderData.insert(tmpData.begin(), tmpData.end());
|
|
}
|
|
|
|
if (_rebuildBuffer) {
|
|
if (_useVBO) {
|
|
// We need to overwrite bigger indices that had data before! No need for SSBO.
|
|
std::map<int, std::vector<float>> idxToRemove;
|
|
for (int idx : _removedKeysInPrevCall) {
|
|
idxToRemove[idx] = std::vector<float>();
|
|
}
|
|
|
|
// This will only insert indices that doesn't already exist in map
|
|
// (i.e. > biggestIdx).
|
|
renderData.insert(idxToRemove.begin(), idxToRemove.end());
|
|
}
|
|
if (innerRebuild) {
|
|
deltaStars = 0;
|
|
}
|
|
|
|
// Clear potential removed keys for both VBO and SSBO!
|
|
_removedKeysInPrevCall.clear();
|
|
|
|
LDEBUG(fmt::format(
|
|
"After rebuilding branch {} - Biggest chunk: {} Free spots in buffer: {}",
|
|
_traversedBranchesInRenderCall, _biggestChunkIndexInUse,
|
|
_freeSpotsInBuffer.size()
|
|
));
|
|
|
|
// End rebuild when all branches has been fetched.
|
|
if (_traversedBranchesInRenderCall == 8) {
|
|
_rebuildBuffer = false;
|
|
_traversedBranchesInRenderCall = 0;
|
|
}
|
|
}
|
|
return renderData;
|
|
}
|
|
|
|
std::vector<float> OctreeManager::getAllData(gaia::RenderOption option) {
|
|
std::vector<float> fullData;
|
|
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
std::vector<float> tmpData = getNodeData(*_root->Children[i], option);
|
|
fullData.insert(fullData.end(), tmpData.begin(), tmpData.end());
|
|
}
|
|
return fullData;
|
|
}
|
|
|
|
void OctreeManager::clearAllData(int branchIndex) {
|
|
// Don't clear everything if not needed.
|
|
if (branchIndex != -1) {
|
|
clearNodeData(*_root->Children[branchIndex]);
|
|
}
|
|
else {
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
clearNodeData(*_root->Children[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OctreeManager::writeToFile(std::ofstream& outFileStream, bool writeData) {
|
|
outFileStream.write(reinterpret_cast<const char*>(&_valuesPerStar), sizeof(int32_t));
|
|
outFileStream.write(
|
|
reinterpret_cast<const char*>(&MAX_STARS_PER_NODE),
|
|
sizeof(int32_t)
|
|
);
|
|
outFileStream.write(reinterpret_cast<const char*>(&MAX_DIST), sizeof(int32_t));
|
|
|
|
// Use pre-traversal (Morton code / Z-order).
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
writeNodeToFile(outFileStream, *_root->Children[i], writeData);
|
|
}
|
|
}
|
|
|
|
void OctreeManager::writeNodeToFile(std::ofstream& outFileStream, const OctreeNode& node,
|
|
bool writeData)
|
|
{
|
|
// Write node structure.
|
|
bool isLeaf = node.isLeaf;
|
|
int32_t numStars = static_cast<int32_t>(node.numStars);
|
|
outFileStream.write(reinterpret_cast<const char*>(&isLeaf), sizeof(bool));
|
|
outFileStream.write(reinterpret_cast<const char*>(&numStars), sizeof(int32_t));
|
|
|
|
// Write node data if specified
|
|
if (writeData) {
|
|
std::vector<float> nodeData = node.posData;
|
|
nodeData.insert(nodeData.end(), node.colData.begin(), node.colData.end());
|
|
nodeData.insert(nodeData.end(), node.velData.begin(), node.velData.end());
|
|
int32_t nDataSize = static_cast<int32_t>(nodeData.size());
|
|
size_t nBytes = nDataSize * sizeof(nodeData[0]);
|
|
|
|
outFileStream.write(reinterpret_cast<const char*>(&nDataSize), sizeof(int32_t));
|
|
if (nDataSize > 0) {
|
|
outFileStream.write(reinterpret_cast<const char*>(nodeData.data()), nBytes);
|
|
}
|
|
}
|
|
|
|
// Write children to file (in Morton order) if we're in an inner node.
|
|
if (!node.isLeaf) {
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
writeNodeToFile(outFileStream, *node.Children[i], writeData);
|
|
}
|
|
}
|
|
}
|
|
|
|
int OctreeManager::readFromFile(std::ifstream& inFileStream, bool readData,
|
|
const std::string& folderPath)
|
|
{
|
|
int nStarsRead = 0;
|
|
int oldMaxdist = static_cast<int>(MAX_DIST);
|
|
|
|
// If we're not reading data then we need to stream from files later on.
|
|
_streamOctree = !readData;
|
|
if (_streamOctree) {
|
|
_streamFolderPath = folderPath;
|
|
}
|
|
|
|
_valuesPerStar = 0;
|
|
inFileStream.read(reinterpret_cast<char*>(&_valuesPerStar), sizeof(int32_t));
|
|
inFileStream.read(reinterpret_cast<char*>(&MAX_STARS_PER_NODE), sizeof(int32_t));
|
|
inFileStream.read(reinterpret_cast<char*>(&MAX_DIST), sizeof(int32_t));
|
|
|
|
LDEBUG(fmt::format(
|
|
"Max stars per node in read Octree: {} - Radius of root layer: {}",
|
|
MAX_STARS_PER_NODE, MAX_DIST
|
|
));
|
|
|
|
// Octree Manager root halfDistance must be updated before any nodes are created!
|
|
if (static_cast<int>(MAX_DIST) != oldMaxdist) {
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
_root->Children[i]->halfDimension = MAX_DIST / 2.f;
|
|
_root->Children[i]->originX = (i % 2 == 0) ?
|
|
_root->Children[i]->halfDimension :
|
|
-_root->Children[i]->halfDimension;
|
|
_root->Children[i]->originY = (i % 4 < 2) ?
|
|
_root->Children[i]->halfDimension :
|
|
-_root->Children[i]->halfDimension;
|
|
_root->Children[i]->originZ = (i < 4) ?
|
|
_root->Children[i]->halfDimension :
|
|
-_root->Children[i]->halfDimension;
|
|
}
|
|
}
|
|
|
|
if (_valuesPerStar != (POS_SIZE + COL_SIZE + VEL_SIZE)) {
|
|
LERROR("Read file doesn't have the same structure of render parameters!");
|
|
}
|
|
|
|
// Use the same technique to construct octree from file.
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
nStarsRead += readNodeFromFile(inFileStream, *_root->Children[i], readData);
|
|
}
|
|
return nStarsRead;
|
|
}
|
|
|
|
int OctreeManager::readNodeFromFile(std::ifstream& inFileStream, OctreeNode& node,
|
|
bool readData)
|
|
{
|
|
// Read node structure.
|
|
bool isLeaf;
|
|
int32_t numStars = 0;
|
|
|
|
inFileStream.read(reinterpret_cast<char*>(&isLeaf), sizeof(bool));
|
|
inFileStream.read(reinterpret_cast<char*>(&numStars), sizeof(int32_t));
|
|
|
|
node.isLeaf = isLeaf;
|
|
node.numStars = numStars;
|
|
|
|
// Read node data if specified.
|
|
if (readData) {
|
|
int32_t nDataSize = 0;
|
|
inFileStream.read(reinterpret_cast<char*>(&nDataSize), sizeof(int32_t));
|
|
|
|
if (nDataSize > 0) {
|
|
std::vector<float> fetchedData(nDataSize, 0.0f);
|
|
size_t nBytes = nDataSize * sizeof(fetchedData[0]);
|
|
|
|
inFileStream.read(reinterpret_cast<char*>(&fetchedData[0]), nBytes);
|
|
|
|
int starsInNode = static_cast<int>(nDataSize / _valuesPerStar);
|
|
auto posEnd = fetchedData.begin() + (starsInNode * POS_SIZE);
|
|
auto colEnd = posEnd + (starsInNode * COL_SIZE);
|
|
auto velEnd = colEnd + (starsInNode * VEL_SIZE);
|
|
node.posData = std::vector<float>(fetchedData.begin(), posEnd);
|
|
node.colData = std::vector<float>(posEnd, colEnd);
|
|
node.velData = std::vector<float>(colEnd, velEnd);
|
|
}
|
|
}
|
|
|
|
// Create children if we're in an inner node and read from the corresponding nodes.
|
|
if (!node.isLeaf) {
|
|
numStars = 0;
|
|
createNodeChildren(node);
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
numStars += readNodeFromFile(inFileStream, *node.Children[i], readData);
|
|
}
|
|
}
|
|
|
|
// Return the number of stars in the entire branch, no need to check children.
|
|
return numStars;
|
|
}
|
|
|
|
void OctreeManager::writeToMultipleFiles(const std::string& outFolderPath,
|
|
size_t branchIndex)
|
|
{
|
|
// Write entire branch to disc, with one file per node.
|
|
std::string outFilePrefix = outFolderPath + std::to_string(branchIndex);
|
|
// More threads doesn't make it much faster, disk speed still the limiter.
|
|
writeNodeToMultipleFiles(outFilePrefix, *_root->Children[branchIndex], false);
|
|
|
|
// Clear all data in branch.
|
|
LINFO(fmt::format("Clear all data from branch {} in octree", branchIndex));
|
|
clearNodeData(*_root->Children[branchIndex]);
|
|
}
|
|
|
|
void OctreeManager::writeNodeToMultipleFiles(const std::string& outFilePrefix,
|
|
OctreeNode& node, bool threadWrites)
|
|
{
|
|
// Prepare node data, save nothing else.
|
|
std::vector<float> nodeData = node.posData;
|
|
nodeData.insert(nodeData.end(), node.colData.begin(), node.colData.end());
|
|
nodeData.insert(nodeData.end(), node.velData.begin(), node.velData.end());
|
|
int32_t nDataSize = static_cast<int32_t>(nodeData.size());
|
|
size_t nBytes = nDataSize * sizeof(nodeData[0]);
|
|
|
|
// Only open output stream if we have any values to write.
|
|
if (nDataSize > 0) {
|
|
// Use Morton code to name file (placement in Octree).
|
|
std::string outPath = outFilePrefix + BINARY_SUFFIX;
|
|
std::ofstream outFileStream(outPath, std::ofstream::binary);
|
|
if (outFileStream.good()) {
|
|
// LINFO("Write " + std::to_string(nDataSize) + " values to " + outPath);
|
|
outFileStream.write(
|
|
reinterpret_cast<const char*>(&nDataSize),
|
|
sizeof(int32_t)
|
|
);
|
|
outFileStream.write(reinterpret_cast<const char*>(nodeData.data()), nBytes);
|
|
|
|
outFileStream.close();
|
|
}
|
|
else {
|
|
LERROR(fmt::format("Error opening file: {} as output data file.", outPath));
|
|
}
|
|
}
|
|
|
|
// Recursively write children to file (in Morton order) if we're in an inner node.
|
|
if (!node.isLeaf) {
|
|
std::vector<std::thread> writeThreads(8);
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
std::string newOutFilePrefix = outFilePrefix + std::to_string(i);
|
|
if (threadWrites) {
|
|
// Divide writing to new threads to speed up the process.
|
|
std::thread t(
|
|
[this, newOutFilePrefix, n = node.Children[i]]() {
|
|
writeNodeToMultipleFiles(newOutFilePrefix, *n, false);
|
|
}
|
|
);
|
|
writeThreads[i] = std::move(t);
|
|
}
|
|
else {
|
|
writeNodeToMultipleFiles(newOutFilePrefix, *node.Children[i], false);
|
|
}
|
|
}
|
|
if (threadWrites) {
|
|
// Make sure all threads are done.
|
|
for (int thread = 0; thread < 8; ++thread) {
|
|
writeThreads[thread].join();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void OctreeManager::fetchChildrenNodes(OctreeNode& parentNode,
|
|
int additionalLevelsToFetch)
|
|
{
|
|
// Lock node to make sure nobody else are trying to load the same children.
|
|
std::lock_guard lock(parentNode.loadingLock);
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
// Fetch node data if we're streaming and it doesn't exist in RAM yet.
|
|
// (As long as there is any RAM budget left and node actually has any data!)
|
|
if (!parentNode.Children[i]->isLoaded &&
|
|
(parentNode.Children[i]->numStars > 0) &&
|
|
_cpuRamBudget > static_cast<long long>(parentNode.Children[i]->numStars
|
|
* (POS_SIZE + COL_SIZE + VEL_SIZE) * 4))
|
|
{
|
|
fetchNodeDataFromFile(*parentNode.Children[i]);
|
|
}
|
|
|
|
// Fetch all Children's Children if recursive is set to true!
|
|
if (additionalLevelsToFetch != 0 && !parentNode.Children[i]->isLeaf) {
|
|
fetchChildrenNodes(*parentNode.Children[i], --additionalLevelsToFetch);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OctreeManager::fetchNodeDataFromFile(OctreeNode& node) {
|
|
// Remove root ID ("8") from index before loading file.
|
|
std::string posId = std::to_string(node.octreePositionIndex);
|
|
posId.erase(posId.begin());
|
|
|
|
std::string inFilePath = _streamFolderPath + posId + BINARY_SUFFIX;
|
|
std::ifstream inFileStream(inFilePath, std::ifstream::binary);
|
|
// LINFO("Fetch node data file: " + inFilePath);
|
|
|
|
if (inFileStream.good()) {
|
|
// Read node data.
|
|
int32_t nDataSize = 0;
|
|
|
|
// Octree knows if we have any data in this node = it exists.
|
|
// Otherwise don't call this function!
|
|
inFileStream.read(reinterpret_cast<char*>(&nDataSize), sizeof(int32_t));
|
|
|
|
std::vector<float> readData(nDataSize, 0.f);
|
|
int nBytes = nDataSize * sizeof(readData[0]);
|
|
if (nDataSize > 0) {
|
|
inFileStream.read(reinterpret_cast<char*>(&readData[0]), nBytes);
|
|
}
|
|
|
|
int starsInNode = static_cast<int>(nDataSize / _valuesPerStar);
|
|
auto posEnd = readData.begin() + (starsInNode * POS_SIZE);
|
|
auto colEnd = posEnd + (starsInNode * COL_SIZE);
|
|
auto velEnd = colEnd + (starsInNode * VEL_SIZE);
|
|
node.posData = std::vector<float>(readData.begin(), posEnd);
|
|
node.colData = std::vector<float>(posEnd, colEnd);
|
|
node.velData = std::vector<float>(colEnd, velEnd);
|
|
|
|
// Keep track of nodes that are loaded and update CPU RAM budget.
|
|
node.isLoaded = true;
|
|
if (!_datasetFitInMemory) {
|
|
std::lock_guard g(_leastRecentlyFetchedNodesMutex);
|
|
_leastRecentlyFetchedNodes.push(node.octreePositionIndex);
|
|
}
|
|
_cpuRamBudget -= nBytes;
|
|
}
|
|
else {
|
|
LERROR("Error opening node data file: " + inFilePath);
|
|
}
|
|
}
|
|
|
|
void OctreeManager::removeNodesFromRam(
|
|
const std::vector<unsigned long long>& nodesToRemove)
|
|
{
|
|
// LINFO("Removed " + std::to_string(nodesToRemove.size()) + " nodes from RAM.");
|
|
|
|
for (unsigned long long nodePosIndex : nodesToRemove) {
|
|
std::stack<int> indexStack;
|
|
while (nodePosIndex != 8) {
|
|
int nodeIndex = nodePosIndex % 10;
|
|
indexStack.push(nodeIndex);
|
|
nodePosIndex /= 10;
|
|
}
|
|
|
|
// Traverse to node and remove it.
|
|
std::shared_ptr<OctreeNode> node = _root;
|
|
std::vector<std::shared_ptr<OctreeNode>> ancestors;
|
|
while (!indexStack.empty()) {
|
|
ancestors.push_back(node);
|
|
node = node->Children[indexStack.top()];
|
|
indexStack.pop();
|
|
}
|
|
removeNode(*node);
|
|
|
|
propagateUnloadedNodes(ancestors);
|
|
}
|
|
}
|
|
|
|
void OctreeManager::removeNode(OctreeNode& node) {
|
|
// Lock node to make sure nobody else is trying to access it while removing.
|
|
std::lock_guard lock(node.loadingLock);
|
|
|
|
int nBytes = static_cast<int>(
|
|
node.numStars * _valuesPerStar * sizeof(node.posData[0])
|
|
);
|
|
// Keep track of which nodes that are loaded and update CPU RAM budget.
|
|
node.isLoaded = false;
|
|
_cpuRamBudget += nBytes;
|
|
|
|
// Clear data
|
|
node.posData.clear();
|
|
node.posData.shrink_to_fit();
|
|
node.colData.clear();
|
|
node.colData.shrink_to_fit();
|
|
node.velData.clear();
|
|
node.velData.shrink_to_fit();
|
|
}
|
|
|
|
void OctreeManager::propagateUnloadedNodes(
|
|
std::vector<std::shared_ptr<OctreeNode>> ancestorNodes)
|
|
{
|
|
std::shared_ptr<OctreeNode> parentNode = ancestorNodes.back();
|
|
while (parentNode->octreePositionIndex != 8) {
|
|
// Check if any children of inner node is still loaded, or has loaded descendants.
|
|
if (parentNode->Children[0]->isLoaded || parentNode->Children[1]->isLoaded ||
|
|
parentNode->Children[2]->isLoaded || parentNode->Children[3]->isLoaded ||
|
|
parentNode->Children[4]->isLoaded || parentNode->Children[5]->isLoaded ||
|
|
parentNode->Children[6]->isLoaded || parentNode->Children[7]->isLoaded ||
|
|
parentNode->Children[0]->hasLoadedDescendant ||
|
|
parentNode->Children[1]->hasLoadedDescendant ||
|
|
parentNode->Children[2]->hasLoadedDescendant ||
|
|
parentNode->Children[3]->hasLoadedDescendant ||
|
|
parentNode->Children[4]->hasLoadedDescendant ||
|
|
parentNode->Children[5]->hasLoadedDescendant ||
|
|
parentNode->Children[6]->hasLoadedDescendant ||
|
|
parentNode->Children[7]->hasLoadedDescendant)
|
|
{
|
|
return;
|
|
}
|
|
// Else all children has been unloaded and we can update parent flag.
|
|
parentNode->hasLoadedDescendant = false;
|
|
// LINFO("Removed ancestor: " + std::to_string(parentNode->octreePositionIndex));
|
|
|
|
// Propagate change upwards.
|
|
ancestorNodes.pop_back();
|
|
parentNode = ancestorNodes.back();
|
|
}
|
|
}
|
|
|
|
size_t OctreeManager::numLeafNodes() const {
|
|
return _numLeafNodes;
|
|
}
|
|
|
|
size_t OctreeManager::numInnerNodes() const {
|
|
return _numInnerNodes;
|
|
}
|
|
|
|
size_t OctreeManager::totalNodes() const {
|
|
return _numLeafNodes + _numInnerNodes;
|
|
}
|
|
|
|
size_t OctreeManager::totalDepth() const {
|
|
return _totalDepth;
|
|
}
|
|
|
|
size_t OctreeManager::maxDist() const {
|
|
return MAX_DIST;
|
|
}
|
|
|
|
size_t OctreeManager::maxStarsPerNode() const {
|
|
return MAX_STARS_PER_NODE;
|
|
}
|
|
|
|
size_t OctreeManager::biggestChunkIndexInUse() const {
|
|
return _biggestChunkIndexInUse;
|
|
}
|
|
|
|
size_t OctreeManager::numFreeSpotsInBuffer() const {
|
|
return _freeSpotsInBuffer.size();
|
|
}
|
|
|
|
long long OctreeManager::cpuRamBudget() const {
|
|
return _cpuRamBudget;
|
|
}
|
|
|
|
bool OctreeManager::isRebuildOngoing() const {
|
|
return _rebuildBuffer;
|
|
}
|
|
|
|
size_t OctreeManager::getChildIndex(float posX, float posY, float posZ, float origX,
|
|
float origY, float origZ)
|
|
{
|
|
size_t index = 0;
|
|
if (posX < origX) {
|
|
index += 1;
|
|
}
|
|
if (posY < origY) {
|
|
index += 2;
|
|
}
|
|
if (posZ < origZ) {
|
|
index += 4;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
bool OctreeManager::insertInNode(OctreeNode& node, const std::vector<float>& starValues,
|
|
int depth)
|
|
{
|
|
if (node.isLeaf && node.numStars < MAX_STARS_PER_NODE) {
|
|
// Node is a leaf and it's not yet full -> insert star.
|
|
storeStarData(node, starValues);
|
|
|
|
if (depth > static_cast<int>(_totalDepth)) {
|
|
_totalDepth = depth;
|
|
}
|
|
return true;
|
|
}
|
|
else if (node.isLeaf) {
|
|
// Too many stars in leaf node, subdivide into 8 new nodes.
|
|
// Create children and clean up parent.
|
|
createNodeChildren(node);
|
|
|
|
// Distribute stars from parent node into children.
|
|
for (size_t n = 0; n < MAX_STARS_PER_NODE; ++n) {
|
|
// Position data.
|
|
auto posBegin = node.posData.begin() + n * POS_SIZE;
|
|
auto posEnd = posBegin + POS_SIZE;
|
|
std::vector<float> tmpValues(posBegin, posEnd);
|
|
// Color data.
|
|
auto colBegin = node.colData.begin() + n * COL_SIZE;
|
|
auto colEnd = colBegin + COL_SIZE;
|
|
tmpValues.insert(tmpValues.end(), colBegin, colEnd);
|
|
// Velocity data.
|
|
auto velBegin = node.velData.begin() + n * VEL_SIZE;
|
|
auto velEnd = velBegin + VEL_SIZE;
|
|
tmpValues.insert(tmpValues.end(), velBegin, velEnd);
|
|
|
|
// Find out which child that will inherit the data and store it.
|
|
size_t index = getChildIndex(
|
|
tmpValues[0],
|
|
tmpValues[1],
|
|
tmpValues[2],
|
|
node.originX,
|
|
node.originY,
|
|
node.originZ
|
|
);
|
|
insertInNode(*node.Children[index], tmpValues, depth);
|
|
}
|
|
|
|
// Sort magnitudes in inner node.
|
|
// (The last value will be used as comparison for what to store in LOD cache.)
|
|
std::sort(node.magOrder.begin(), node.magOrder.end());
|
|
}
|
|
|
|
// Node is an inner node, keep recursion going.
|
|
// This will also take care of the new star when a subdivision has taken place.
|
|
size_t index = getChildIndex(
|
|
starValues[0],
|
|
starValues[1],
|
|
starValues[2],
|
|
node.originX,
|
|
node.originY,
|
|
node.originZ
|
|
);
|
|
|
|
// Determine if new star should be kept in our LOD cache.
|
|
// Keeps track of the brightest nodes in children.
|
|
if (starValues[POS_SIZE] < node.magOrder[MAX_STARS_PER_NODE - 1].first) {
|
|
storeStarData(node, starValues);
|
|
}
|
|
|
|
return insertInNode(*node.Children[index], starValues, ++depth);
|
|
}
|
|
|
|
void OctreeManager::sliceNodeLodCache(OctreeNode& node) {
|
|
// Slice stored LOD data in inner nodes.
|
|
if (!node.isLeaf) {
|
|
// Sort by magnitude. Inverse relation (i.e. a lower magnitude means a brighter
|
|
// star!)
|
|
std::sort(node.magOrder.begin(), node.magOrder.end());
|
|
node.magOrder.resize(MAX_STARS_PER_NODE);
|
|
|
|
std::vector<float> tmpPos;
|
|
std::vector<float> tmpCol;
|
|
std::vector<float> tmpVel;
|
|
// Ordered map contain the MAX_STARS_PER_NODE brightest stars in all children!
|
|
for (auto const &[absMag, placement] : node.magOrder) {
|
|
auto posBegin = node.posData.begin() + placement * POS_SIZE;
|
|
auto colBegin = node.colData.begin() + placement * COL_SIZE;
|
|
auto velBegin = node.velData.begin() + placement * VEL_SIZE;
|
|
tmpPos.insert(tmpPos.end(), posBegin, posBegin + POS_SIZE);
|
|
tmpCol.insert(tmpCol.end(), colBegin, colBegin + COL_SIZE);
|
|
tmpVel.insert(tmpVel.end(), velBegin, velBegin + VEL_SIZE);
|
|
}
|
|
node.posData = std::move(tmpPos);
|
|
node.colData = std::move(tmpCol);
|
|
node.velData = std::move(tmpVel);
|
|
node.numStars = node.magOrder.size(); // = MAX_STARS_PER_NODE
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
sliceNodeLodCache(*node.Children[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OctreeManager::storeStarData(OctreeNode& node, const std::vector<float>& starValues)
|
|
{
|
|
// Insert star data at the back of vectors and store a vector with pairs consisting of
|
|
// star magnitude and insert index for later sorting and slicing of LOD cache.
|
|
float mag = starValues[POS_SIZE];
|
|
node.magOrder.insert(node.magOrder.end(), std::make_pair(mag, node.numStars));
|
|
node.numStars++;
|
|
|
|
// If LOD is growing too large then sort it and resize to [chunk size] to avoid too
|
|
// much RAM usage and increase threshold for adding new stars.
|
|
if (node.magOrder.size() > MAX_STARS_PER_NODE * 2) {
|
|
std::sort(node.magOrder.begin(), node.magOrder.end());
|
|
node.magOrder.resize(MAX_STARS_PER_NODE);
|
|
}
|
|
|
|
auto posEnd = starValues.begin() + POS_SIZE;
|
|
auto colEnd = posEnd + COL_SIZE;
|
|
node.posData.insert(node.posData.end(), starValues.begin(), posEnd);
|
|
node.colData.insert(node.colData.end(), posEnd, colEnd);
|
|
node.velData.insert(node.velData.end(), colEnd, starValues.end());
|
|
}
|
|
|
|
std::string OctreeManager::printStarsPerNode(const OctreeNode& node,
|
|
const std::string& prefix) const
|
|
{
|
|
|
|
// Print both inner and leaf nodes.
|
|
auto str = prefix + "} : " + std::to_string(node.numStars);
|
|
|
|
if (node.isLeaf) {
|
|
return str + " - [Leaf] \n";
|
|
}
|
|
else {
|
|
str += fmt::format("LOD: {} - [Parent]\n", node.posData.size() / POS_SIZE);
|
|
for (int i = 0; i < 8; ++i) {
|
|
auto pref = prefix + "->" + std::to_string(i);
|
|
str += printStarsPerNode(*node.Children[i], pref);
|
|
}
|
|
return str;
|
|
}
|
|
}
|
|
|
|
std::map<int, std::vector<float>> OctreeManager::checkNodeIntersection(OctreeNode& node,
|
|
const glm::dmat4& mvp,
|
|
const glm::vec2& screenSize,
|
|
int& deltaStars,
|
|
gaia::RenderOption option)
|
|
{
|
|
std::map<int, std::vector<float>> fetchedData;
|
|
//int depth = static_cast<int>(log2( MAX_DIST / node->halfDimension ));
|
|
|
|
// Calculate the corners of the node.
|
|
std::vector<glm::dvec4> corners(8);
|
|
for (int i = 0; i < 8; ++i) {
|
|
const float x = (i % 2 == 0) ?
|
|
node.originX + node.halfDimension :
|
|
node.originX - node.halfDimension;
|
|
const float y = (i % 4 < 2) ?
|
|
node.originY + node.halfDimension :
|
|
node.originY - node.halfDimension;
|
|
const float z = (i < 4) ?
|
|
node.originZ + node.halfDimension :
|
|
node.originZ - node.halfDimension;
|
|
glm::dvec3 pos = glm::dvec3(x, y, z) * 1000.0 * distanceconstants::Parsec;
|
|
corners[i] = glm::dvec4(pos, 1.0);
|
|
}
|
|
|
|
// Check if node is visible from camera. If not then return early.
|
|
if (!(_culler->isVisible(corners, mvp))) {
|
|
// Check if this node or any of its children existed in cache previously.
|
|
// If so, then remove them from cache and add those indices to stack.
|
|
fetchedData = removeNodeFromCache(node, deltaStars);
|
|
return fetchedData;
|
|
}
|
|
|
|
// Remove node if it has been unloaded while still in view.
|
|
// (While streaming big datasets.)
|
|
if (node.bufferIndex != DEFAULT_INDEX && !node.isLoaded && _streamOctree &&
|
|
!_datasetFitInMemory)
|
|
{
|
|
fetchedData = removeNodeFromCache(node, deltaStars);
|
|
return fetchedData;
|
|
}
|
|
|
|
// Take care of inner nodes.
|
|
if (!(node.isLeaf)) {
|
|
glm::vec2 nodeSize = _culler->getNodeSizeInPixels(corners, mvp, screenSize);
|
|
float totalPixels = nodeSize.x * nodeSize.y;
|
|
|
|
// Check if we should return any LOD cache data. If we're streaming a big dataset
|
|
// from files and inner node is visible and loaded, then it should be rendered
|
|
// (as long as it doesn't have loaded children because then we should traverse to
|
|
// lowest loaded level and render it instead)!
|
|
if ((totalPixels < _minTotalPixelsLod) || (_streamOctree &&
|
|
!_datasetFitInMemory && node.isLoaded && !node.hasLoadedDescendant))
|
|
{
|
|
// Get correct insert index from stack if node didn't exist already. Otherwise
|
|
// we will overwrite the old data. Key merging is not a problem here.
|
|
if ((node.bufferIndex == DEFAULT_INDEX) || _rebuildBuffer) {
|
|
// Return empty if we couldn't claim a buffer stream index.
|
|
if (!updateBufferIndex(node)) {
|
|
return fetchedData;
|
|
}
|
|
|
|
// We're in an inner node, remove indices from potential children in cache
|
|
for (int i = 0; i < 8; ++i) {
|
|
std::map<int, std::vector<float>> tmpData = removeNodeFromCache(
|
|
*node.Children[i],
|
|
deltaStars
|
|
);
|
|
fetchedData.insert(tmpData.begin(), tmpData.end());
|
|
}
|
|
|
|
// Insert data and adjust stars added in this frame.
|
|
fetchedData[node.bufferIndex] = constructInsertData(
|
|
node,
|
|
option,
|
|
deltaStars
|
|
);
|
|
}
|
|
return fetchedData;
|
|
}
|
|
}
|
|
// Return node data if node is a leaf.
|
|
else {
|
|
// If node already is in cache then skip it, otherwise store it.
|
|
if ((node.bufferIndex == DEFAULT_INDEX) || _rebuildBuffer) {
|
|
// Return empty if we couldn't claim a buffer stream index.
|
|
if (!updateBufferIndex(node)) {
|
|
return fetchedData;
|
|
}
|
|
|
|
// Insert data and adjust stars added in this frame.
|
|
fetchedData[node.bufferIndex] = constructInsertData(
|
|
node,
|
|
option,
|
|
deltaStars
|
|
);
|
|
}
|
|
return fetchedData;
|
|
}
|
|
|
|
// We're in a big, visible inner node -> remove it from cache if it existed.
|
|
// But not its children -> set recursive check to false.
|
|
fetchedData = removeNodeFromCache(node, deltaStars, false);
|
|
|
|
// Recursively check if children should be rendered.
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
// Observe that if there exists identical keys in fetchedData then those values in
|
|
// tmpData will be ignored! Thus we store the removed keys until next render call!
|
|
std::map<int, std::vector<float>> tmpData = checkNodeIntersection(
|
|
*node.Children[i],
|
|
mvp,
|
|
screenSize,
|
|
deltaStars,
|
|
option
|
|
);
|
|
fetchedData.insert(tmpData.begin(), tmpData.end());
|
|
}
|
|
return fetchedData;
|
|
}
|
|
|
|
std::map<int, std::vector<float>> OctreeManager::removeNodeFromCache(OctreeNode& node,
|
|
int& deltaStars,
|
|
bool recursive)
|
|
{
|
|
std::map<int, std::vector<float>> keysToRemove;
|
|
|
|
// If we're in rebuilding mode then there is no need to remove any nodes.
|
|
//if (_rebuildBuffer) return keysToRemove;
|
|
|
|
// Check if this node was rendered == had a specified index.
|
|
if (node.bufferIndex != DEFAULT_INDEX) {
|
|
|
|
// Reclaim that index. We need to wait until next render call to use it again!
|
|
_removedKeysInPrevCall.insert(node.bufferIndex);
|
|
|
|
// Insert dummy node at offset index that should be removed from render.
|
|
keysToRemove[node.bufferIndex] = std::vector<float>();
|
|
|
|
// Reset index and adjust stars removed this frame.
|
|
node.bufferIndex = DEFAULT_INDEX;
|
|
deltaStars -= static_cast<int>(node.numStars);
|
|
}
|
|
|
|
// Check children recursively if we're in an inner node.
|
|
if (!(node.isLeaf) && recursive) {
|
|
for (int i = 0; i < 8; ++i) {
|
|
std::map<int, std::vector<float>> tmpData = removeNodeFromCache(
|
|
*node.Children[i],
|
|
deltaStars
|
|
);
|
|
keysToRemove.insert(tmpData.begin(), tmpData.end());
|
|
}
|
|
}
|
|
return keysToRemove;
|
|
}
|
|
|
|
std::vector<float> OctreeManager::getNodeData(const OctreeNode& node,
|
|
gaia::RenderOption option)
|
|
{
|
|
// Return node data if node is a leaf.
|
|
if (node.isLeaf) {
|
|
int dStars = 0;
|
|
return constructInsertData(node, option, dStars);
|
|
}
|
|
|
|
// If we're not in a leaf, get data from all children recursively.
|
|
auto nodeData = std::vector<float>();
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
std::vector<float> tmpData = getNodeData(*node.Children[i], option);
|
|
nodeData.insert(nodeData.end(), tmpData.begin(), tmpData.end());
|
|
}
|
|
return nodeData;
|
|
}
|
|
|
|
void OctreeManager::clearNodeData(OctreeNode& node) {
|
|
// Clear data and its allocated memory.
|
|
node.posData.clear();
|
|
node.posData.shrink_to_fit();
|
|
node.colData.clear();
|
|
node.colData.shrink_to_fit();
|
|
node.velData.clear();
|
|
node.velData.shrink_to_fit();
|
|
|
|
// Clear magnitudes as well!
|
|
//std::vector<std::pair<float, size_t>>().swap(node->magOrder);
|
|
node.magOrder.clear();
|
|
|
|
if (!node.isLeaf) {
|
|
// Remove data from all children recursively.
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
clearNodeData(*node.Children[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OctreeManager::createNodeChildren(OctreeNode& node) {
|
|
for (size_t i = 0; i < 8; ++i) {
|
|
_numLeafNodes++;
|
|
node.Children[i] = std::make_shared<OctreeNode>();
|
|
node.Children[i]->isLeaf = true;
|
|
node.Children[i]->isLoaded = false;
|
|
node.Children[i]->hasLoadedDescendant = false;
|
|
node.Children[i]->bufferIndex = DEFAULT_INDEX;
|
|
node.Children[i]->octreePositionIndex = (node.octreePositionIndex * 10) + i;
|
|
node.Children[i]->numStars = 0;
|
|
node.Children[i]->posData = std::vector<float>();
|
|
node.Children[i]->colData = std::vector<float>();
|
|
node.Children[i]->velData = std::vector<float>();
|
|
node.Children[i]->magOrder = std::vector<std::pair<float, size_t>>();
|
|
node.Children[i]->halfDimension = node.halfDimension / 2.f;
|
|
|
|
// Calculate new origin.
|
|
node.Children[i]->originX = node.originX;
|
|
node.Children[i]->originX += (i % 2 == 0) ?
|
|
node.Children[i]->halfDimension :
|
|
-node.Children[i]->halfDimension;
|
|
node.Children[i]->originY = node.originY;
|
|
node.Children[i]->originY += (i % 4 < 2) ?
|
|
node.Children[i]->halfDimension :
|
|
-node.Children[i]->halfDimension;
|
|
node.Children[i]->originZ = node.originZ;
|
|
node.Children[i]->originZ += (i < 4) ?
|
|
node.Children[i]->halfDimension :
|
|
-node.Children[i]->halfDimension;
|
|
}
|
|
|
|
// Clean up parent.
|
|
node.isLeaf = false;
|
|
_numLeafNodes--;
|
|
_numInnerNodes++;
|
|
}
|
|
|
|
bool OctreeManager::updateBufferIndex(OctreeNode& node) {
|
|
if (node.bufferIndex != DEFAULT_INDEX) {
|
|
// If we're rebuilding Buffer Index Cache then store indices to overwrite later.
|
|
_removedKeysInPrevCall.insert(node.bufferIndex);
|
|
}
|
|
|
|
// Make sure node isn't loading/unloading as we're checking isLoaded flag.
|
|
std::lock_guard lock(node.loadingLock);
|
|
|
|
// Return false if there are no more spots in our buffer, or if we're streaming and
|
|
// node isn't loaded yet, or if node doesn't have any stars.
|
|
if (_freeSpotsInBuffer.empty() || (_streamOctree && !node.isLoaded) ||
|
|
node.numStars == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Get correct insert index from stack.
|
|
node.bufferIndex = _freeSpotsInBuffer.top();
|
|
_freeSpotsInBuffer.pop();
|
|
|
|
// Keep track of how many chunks are in use (ceiling).
|
|
if (_freeSpotsInBuffer.empty()) {
|
|
_biggestChunkIndexInUse++;
|
|
}
|
|
else if (_freeSpotsInBuffer.top() > static_cast<int>(_biggestChunkIndexInUse)) {
|
|
_biggestChunkIndexInUse = _freeSpotsInBuffer.top();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::vector<float> OctreeManager::constructInsertData(const OctreeNode& node,
|
|
gaia::RenderOption option,
|
|
int& deltaStars)
|
|
{
|
|
// Return early if node doesn't contain any stars!
|
|
if (node.numStars == 0) {
|
|
return std::vector<float>();
|
|
}
|
|
|
|
// Fill chunk by appending zeroes to data so we overwrite possible earlier values.
|
|
// And more importantly so our attribute pointers knows where to read!
|
|
auto insertData = std::vector<float>(node.posData.begin(), node.posData.end());
|
|
if (_useVBO) {
|
|
insertData.resize(POS_SIZE * MAX_STARS_PER_NODE, 0.f);
|
|
}
|
|
if (option != gaia::RenderOption::Static) {
|
|
insertData.insert(insertData.end(), node.colData.begin(), node.colData.end());
|
|
if (_useVBO) {
|
|
insertData.resize((POS_SIZE + COL_SIZE) * MAX_STARS_PER_NODE, 0.f);
|
|
}
|
|
if (option == gaia::RenderOption::Motion) {
|
|
insertData.insert(insertData.end(), node.velData.begin(), node.velData.end());
|
|
if (_useVBO) {
|
|
insertData.resize(
|
|
(POS_SIZE + COL_SIZE + VEL_SIZE) * MAX_STARS_PER_NODE, 0.f
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update deltaStars.
|
|
deltaStars += static_cast<int>(node.numStars);
|
|
return insertData;
|
|
}
|
|
|
|
} // namespace openspace
|