Cogs.Core
OGC3DTilesSystem.cpp
1#include "OGC3DTilesSystem.h"
2#include "OGC3DTilesBounds.h"
3
4#include "../OGC3DTilesUtils.h"
5#include "../OGC3DTilesAccessParser.h"
6#include "../OGC3DTilesAssetsParser.h"
7#include "../OGC3DTilesTileset.h"
8#include "../OGC3DTilesSubtree.h"
9#include "../Components/OGC3DTilesComponent.h"
10
11#include "Context.h"
12#include "ViewContext.h"
13#include "EntityStore.h"
14#include "ExtensionRegistry.h"
15
16#include "Services/Time.h"
17#include "Services/QualityService.h"
18#include "Services/Variables.h"
19
20#include "Components/Core/ModelComponent.h"
21#include "Components/Core/MeshComponent.h"
22#include "Components/Core/SceneComponent.h"
23#include "Components/Core/SubMeshRenderComponent.h"
24#include "Components/Appearance/MaterialComponent.h"
25
26#include "Systems/Core/CameraSystem.h"
27#include "Systems/Core/TransformSystem.h"
28
29#include "Resources/MeshManager.h"
30#include "Resources/ModelManager.h"
31#include "Resources/VertexFormats.h"
32#include "Resources/MaterialManager.h"
33#include "Resources/MaterialInstance.h"
34#include "Resources/DefaultMaterial.h"
35#include "Resources/Texture.h"
36
37#include "Renderer/RenderResource.h"
38
39#include "Foundation/Logging/Logger.h"
40#include "Foundation/HashFunctions.h"
41
42#include <glm/gtc/matrix_transform.hpp>
43#include <glm/gtx/quaternion.hpp>
44
45#include <chrono>
46#include <algorithm>
47#include <vector>
48#include <bitset>
49#include <string>
50
51using namespace Cogs::Core;
52
53constexpr int MAX_NUM_ACTIVE_MODEL_REQUESTS = 64; // Max number of active model requests in total
54constexpr int MAX_NUM_ACTIVE_SUBTREES_REQUESTS = 8;
55constexpr int MAX_NUM_ACTIVE_SUBTILESET_REQUESTS = 8;
56constexpr int MAX_NUM_MODEL_REQUESTS_PER_FRAME = 24; // Max number of new model req. per frame
57constexpr float DEFAULT_GRACE_PERIOD_BEFORE_TREE_TRAVERSAL = 0.0;
58constexpr float DEFAULT_GRACE_PERIOD_BEFORE_TILE_REMOVAL = 2.0;
59
60// The 'formulae' for the base-color of the bboxes. The 'r' variable will be randomized from <0 .. 1.0>.
61#define BBOX_BASE_COLOR_FORMAT glm::vec4(r, 1.0, r, 0.5)
62
63// Adds the full URL of the tile/model to the name. Used when debugging
64#define DEBUG_ADD_URL_TO_MODEL_NAMES false
65// Check all incoming models to see if they already exists
66#define DEBUG_CHECK_IF_INCOMING_MODEL_EXISTS false
67
68namespace {
69 // (un)freezing tree-traversal (bool)
70 const Cogs::StringView lodFreezeName = "OGC3DTiles.lodFreeze";
71 // Color each tile according to its depth in the tree
72 const Cogs::StringView colorTilesAccordingToTreeDepthName = "OGC3DTiles.debug.colorTilesAccordingToTreeDepth";
73 // Max number of active http(s) requests (integer)
74 const Cogs::StringView maxNumActiveModelRequestsName = "OGC3DTiles.requests.maxConcurrent";
75 // Max number of http(s) requests to be scheduled per frame (integer)
76 const Cogs::StringView maxNumModelRequestsPerFrameName = "OGC3DTiles.requests.maxPerFrame";
77 // Grace period in seconds before tree-traversal starts
87 const Cogs::StringView gracePeriodBeforeTreeTraversalName = "OGC3DTiles.timing.gracePeriodBeforeTreeTraversal";
88 // Grace period before tiles outside view will be deleted
89 const Cogs::StringView gracePeriodBeforeTileRemovalName = "OGC3DTiles.timing.gracePeriodBeforeTileRemoval";
90
91 Cogs::Logging::Log logger = Cogs::Logging::getLogger("OGC3DTilesSystem");
92
93 uint32_t createUniqueComponentId() {
94 static uint32_t idCounter = 1;
95 if (idCounter == 0) idCounter++;
96 return idCounter++;
97 }
98}
99
100OGC3DTilesSystem::OGC3DTilesSystem(Cogs::Memory::Allocator* allocator, Cogs::ComponentIndex capacity)
101 : ComponentSystemWithDataPool(allocator, capacity)
102{
103}
104
105OGC3DTilesSystem::~OGC3DTilesSystem() = default;
106
107void
109{
111
112 if (!context->variables->exist(maxNumActiveModelRequestsName)) {
113 context->variables->set(maxNumActiveModelRequestsName, MAX_NUM_ACTIVE_MODEL_REQUESTS);
114 }
115
116 if (!context->variables->exist(maxNumModelRequestsPerFrameName)) {
117 context->variables->set(maxNumModelRequestsPerFrameName, MAX_NUM_MODEL_REQUESTS_PER_FRAME);
118 }
119
120 if (!context->variables->exist(gracePeriodBeforeTreeTraversalName)) {
121 context->variables->set(gracePeriodBeforeTreeTraversalName, DEFAULT_GRACE_PERIOD_BEFORE_TREE_TRAVERSAL);
122 }
123
124 if (!context->variables->exist(gracePeriodBeforeTileRemovalName)) {
125 context->variables->set(gracePeriodBeforeTileRemovalName, DEFAULT_GRACE_PERIOD_BEFORE_TILE_REMOVAL);
126 }
127
128 if (!context->variables->exist(lodFreezeName)) {
129 context->variables->set(lodFreezeName, false);
130 }
131
132 if (!context->variables->exist(colorTilesAccordingToTreeDepthName)) {
133 context->variables->set(colorTilesAccordingToTreeDepthName, false);
134 }
135
136 // Build a couple of the permutations of the custom debug material for coloring
137 // tiles according to tree-depth.
138 Cogs::Core::MaterialHandle baseMaterialHandle = context->materialManager->loadMaterial("Materials/OGC3DTilesDebugMaterial.material");
139 context->materialManager->processLoading();
140
141 for (int i = 1; i < 8; ++i) {
142 glm::vec4 color = glm::vec4(i & 0b1 ? 1.0 : 0.0, i & 0b10 ? 1.0 : 0.0, i & 0b100 ? 1.0 : 0.0, 1.0);
143 MaterialInstanceHandle materialInstance = context->materialInstanceManager->createMaterialInstance(baseMaterialHandle);
144 materialInstance->setPermutation("FlatShaded");
145 if (VariableKey key = materialInstance->material->getVec4Key("diffuseColor"); key != NoProperty) {
146 materialInstance->setVec4Property(key, color);
147 }
148
149 debugTileTreeDepthColorMaterials.push_back(materialInstance);
150 }
151}
152
158bool
160{
161 OGC3DTilesSystem * system = ExtensionRegistry::getExtensionSystem<OGC3DTilesSystem>(context);
162
163 for (auto& comp : system->pool) {
164 OGC3DTilesDataHolder& h = system->getData(&comp);
165 OGC3DTilesData* componentState = h.data.get();
166 if (componentState->uniqueComponentId == id) {
167 return false;
168 }
169 }
170
171 return true;
172}
173
174std::string
175OGC3DTilesSystem::compileURL(const std::string& baseURL, const std::string& tilesetPath, const std::string& modelPath)
176{
177 return baseURL + (baseURL.empty() ? "" : "/") +
178 tilesetPath + (tilesetPath.empty() ? "" : "/") +
179 modelPath;
180}
181
182void
183OGC3DTilesSystem::printDebugStats(const OGC3DTilesData* componentState)
184{
185 LOG_DEBUG(logger, "== OGC3DTiles debug stats ['%s'] ==", componentState->baseURL.c_str());
186
187 int totalNumTiles = 0;
188 int totalNumRequests = 0;
189 for (auto const& [tileset, state] : componentState->states) {
190 totalNumTiles += (int)state.tileCache.size();
191 totalNumRequests += (int)state.modelRequests.size();
192 }
193
194 LOG_DEBUG(logger, " Total num tiles in cache(s): %d", totalNumTiles);
195 LOG_DEBUG(logger, " Num models in request-queue: %d", totalNumRequests);
196 LOG_DEBUG(logger, " Max traversal depth: %d, Num canceled model requests: %d",
197 componentState->statistics.traversedDepth, componentState->statistics.numberOfCancelledRequests);
198}
199
200void
201OGC3DTilesSystem::applyMaterialToAllModels(const OGC3DTilesData* componentState, MaterialInstanceHandle materialHandle)
202{
203 for (auto tileset : componentState->tilesets) {
204 for (auto const& [tileId, models] : componentState->states.at(tileset).tileCache) {
205 for (const LoadedModel& model : models) {
206 applyMaterialToModel(model, materialHandle);
207 }
208 }
209 }
210}
211
212void
213OGC3DTilesSystem::applyMaterialToModel(const OGC3DTilesSystem::LoadedModel& model, MaterialInstanceHandle materialHandle)
214{
215 EntityPtr entity = this->context->store->getEntity(model.uniqueName, false);
216 if (entity) {
217 ModelComponent* modelComponent = entity->getComponent<ModelComponent>();
218 modelComponent->inheritMaterial = materialHandle ? true : false;
219 modelComponent->materialInstance = materialHandle;
220 modelComponent->setChanged();
221 }
222}
223
224void
226{
227 CpuInstrumentationScope(SCOPE_OGC3DTILES, "OGC3DTilesSystem::update");
228
229 // Skip updating if we are lod-freezing
230 if (context->variables->get(lodFreezeName, false)) {
231 return;
232 }
233
234 // Has the camera moved?
235 const CameraData& cameraData = this->context->cameraSystem->getMainCameraData();
236 // FIXME: Should we add some kind of tolerance to the camera-movement check? The camera in VR is
237 // constantly moving which will always trigger a tree-traversal. Maybe we should be able to adjust
238 // the slack?
239 bool cameraMoved = cameraData.rawViewProjection != this->lastCameraViewProjection;
240
241 for (OGC3DTilesComponent& component : this->pool) {
242 OGC3DTilesDataHolder& h = this->getData(&component);
243 OGC3DTilesData* componentState = h.data.get();
244
245 if (componentState->overrideMaterial != component.overrideMaterial) {
246 // The override material has changed. We need to apply this to all loaded models.
247 applyMaterialToAllModels(componentState, component.overrideMaterial);
248 componentState->overrideMaterial = component.overrideMaterial;
249 }
250
251 if (componentState->detailFactor < 0.0) {
252 componentState->detailFactor = 0.0;
253 }
254
255 if (component.baseURL != componentState->baseURL && componentState->tilesets.empty()) { // Populate component values
256 componentState->parentEntity = component.getContainer();
257 initializeTileset(&component, componentState);
258 }
259
260 if (cameraMoved) { // Camera moved? Update the componentState
261 if (componentState->showTileBBoxes && !component.showTileBBoxes) {
262 // Remove all bbox indicators
263 for (auto tileset : componentState->tilesets) {
264 for (auto const& [tileId, entity] : componentState->states[tileset].bboxIndicatorCache) {
265 this->context->store->removeChild(componentState->parentEntity, entity.get());
266 this->context->store->destroyEntity(entity->getId());
267 }
268 componentState->states[tileset].bboxIndicatorCache.clear();
269 }
270 }
271
272 componentState->showTileBBoxes = component.showTileBBoxes;
273 componentState->neverDiscardRootTiles = component.neverDiscardRootTiles;
274 componentState->detailFactor = component.detailFactor;
275 componentState->waitForTileSiblings = component.waitForTileSiblings;
276 componentState->enableCaching = component.enableCaching;
277 componentState->colorTileAccordingToTreeDepth = context->variables->get(colorTilesAccordingToTreeDepthName, false);
278 componentState->dontRenderLeaves = component.dontRenderLeaves;
279 componentState->loadAllSiblings = component.loadAllSiblings;
280 }
281
282 const double now = this->context->time->getAnimationTime();
283
284 if (!componentState->tilesets.empty()) { // Are we ready yet?
285 float gracePeriodBeforeTreeTraversal = context->variables->get(gracePeriodBeforeTreeTraversalName, DEFAULT_GRACE_PERIOD_BEFORE_TREE_TRAVERSAL);
286
287 bool needsTraversal = false;
288 if (gracePeriodBeforeTreeTraversal > 0) { // Delayed traversal?
289 const double delta = now - this->timestampOfLastCameraMovement;
290 needsTraversal = delta > gracePeriodBeforeTreeTraversal;
291 if (needsTraversal) {
292 cameraMoved = true; // Fake a camera movement to ensure timestamps gets updated
293 }
294 }
295 else { // No delayed traversal. Always traverse if camera moved.
296 needsTraversal = cameraMoved;
297 }
298
299 if (needsTraversal || hasPendingRequests(componentState) || componentState->requiresTraversal) {
300 float gracePeriodBeforeTileRemoval = context->variables->get(gracePeriodBeforeTileRemovalName, DEFAULT_GRACE_PERIOD_BEFORE_TILE_REMOVAL);
301
302 componentState->statistics.traversedDepth = 0;
303 componentState->statistics.numberOfCancelledRequests = 0;
304 componentState->statistics.numberOfPendingRequests = 0;
305 componentState->statistics.numberOfVisitedSubTilesets = 0;
306 componentState->numberOfModelsBeingPrepared = 0;
307 componentState->requiresTraversal = false;
308
309 // Tileset #0 in the tileset-list is always the main tileset
310 traverseTileset(&component, componentState, componentState->tilesets[0], gracePeriodBeforeTileRemoval);
311
312 // Check all tilesets (except the main tileset) and their timestamp to see if they recently fell
313 // out of view and needs to be traversed (ie. pruned).
314 for (auto const& tileset : componentState->tilesets) {
315 double timestamp = componentState->states[tileset].timestampOfLastTraversal;
316 if (tileset != componentState->tilesets[0] &&
317 componentState->states[tileset].candidateForDelayedTraversal &&
318 (now - timestamp) >= gracePeriodBeforeTileRemoval) {
319 traverseTileset(&component, componentState, tileset, gracePeriodBeforeTileRemoval);
320 componentState->states[tileset].candidateForDelayedTraversal = false;
321 }
322 }
323 }
324
325 this->processSubTilesetRequests(&component, componentState);
326 }
327
328#if 0 // For debugging
329 LOG_DEBUG(logger, " Max traversal depth: %d, Num requests: %d, Num cancelled: %d, Num pending: %d",
330 componentState->statistics.traversedDepth,
331 componentState->modelRequests.size(),
332 componentState->statistics.numberOfCancelledRequests,
333 componentState->statistics.numberOfPendingRequests);
334#endif
335
336 if (cameraMoved) {
337 this->timestampOfLastCameraMovement = now;
338 this->lastCameraViewProjection = cameraData.rawViewProjection;
339 }
340
341 // Is there still work to be done? If so, notify the engine. This is required as
342 // the "update()" method is not necessarily called every frame.
343 if (this->hasPendingRequests(componentState)) {
344 this->context->engine->setDirty();
345 }
346 }
347}
348
349void
350OGC3DTilesSystem::cancelStaleModelRequests(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, const Node& node) const
351{
352 // Check the list of models currently in view against the model-request queue.
353 // If a model is in the queue but not in the view -- try to cancel the request.
354
355 std::vector<std::string> toBeCancelled;
356 toBeCancelled.reserve(node.tile.URLs.size());
357 OGC3DTilesData::State* state = &componentState->states[tileset];
358
359 for (auto const& url : node.tile.URLs) {
360 if (!state->modelRequests.contains(url)) {
361 bool found = false;
362 for (auto const& model : state->tileCache[node.tile.tileId]) {
363 if (model.URL == url) {
364 found = true;
365 break;
366 }
367 }
368
369 if (!found) {
370 this->context->modelManager->cancelModelLoad(state->modelRequests[url].second);
371 componentState->statistics.numberOfCancelledRequests += 1;
372 toBeCancelled.emplace_back(url);
373 }
374 }
375 }
376
377 for (auto const& url : toBeCancelled) {
378 state->modelRequests.erase(url);
379 }
380
381 for (auto const& [tileId, child] : node.children) {
382 cancelStaleModelRequests(componentState, tileset, child); // ** Recursive **
383 }
384}
385
391void
393{
394 OGC3DTilesData::State * state = &componentState->states[tileset];
395 const std::unordered_map<std::string, OGC3DTiles::Coord> subtreeRequestsCopy = state->subtreeRequests;
396
397 for (const auto& [subtreeURL, coord] : subtreeRequestsCopy) {
398 const uint64_t key = OGC3DTilesSystem::makeCacheKey(coord);
399 if (state->subtreeCache.contains(coord.level) && state->subtreeCache[coord.level].contains(key) &&
400 state->subtreeCache[coord.level][key]->pendingRequests == 0) {
401 const uint64_t subtreeGUID = Cogs::hash(subtreeURL);
402 state->subtreeRequests.erase(subtreeURL);
403 state->subtreeFetchIds.erase(subtreeGUID);
404 }
405 }
406}
407
408bool
409OGC3DTilesSystem::hasPendingRequests(const OGC3DTilesData* componentState) const
410{
411 const bool subTilesetsState = !componentState->subTilesetRequests.empty();
412 const bool modelsPreparedState = componentState->numberOfModelsBeingPrepared > 0;
413 const bool tilesetFetches = !componentState->tilesetFetchIds.empty();
414
415 if (subTilesetsState || modelsPreparedState || tilesetFetches) {
416 return true;
417 }
418
419 for (auto tileset : componentState->tilesets) {
420 const bool modelRequestsState = !componentState->states.at(tileset).pendingModelRequests.empty() || !componentState->states.at(tileset).modelRequests.empty();
421 const bool subtreeRequestsState = !componentState->states.at(tileset).subtreeRequests.empty();
422
423 if (modelRequestsState || subtreeRequestsState) {
424 return true;
425 }
426 }
427
428 return false;
429}
430
431void
432OGC3DTilesSystem::traverseTileset(const OGC3DTilesComponent* component, OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, float gracePeriodBeforeTileRemoval)
433{
434 std::unordered_set<const OGC3DTilesTileset::Tileset*> visitedTilesets;
435
436 Node rootNode;
437 rootNode.tile.depth = 0;
438
439 componentState->states[tileset].pendingModelRequests.clear();
440
441 const TransformComponent* componentTransform = componentState->parentEntity->getComponent<TransformComponent>();
442 const glm::mat4 globalTransform = this->context->transformSystem->getLocalToWorld(componentTransform);
443
444 // FIXME: We are only checking the root-node to see if the tileset is in "implicit tiling"-mode.
445 // It is not clear if the 3DTiles-standard allows for subsections of the tree to be in
446 // other modes.
447 if (tileset->root.useImplicitTiling) { // Implicit tree?
448 // Quad- or Oct-tree mode?
449 OGC3DTiles::Coord rootCoord{ 0, 0, 0, tileset->root.implicitTiling.isOctTree ? 0 : -1 };
450 OGC3DTilesSubtree::Subtree const* rootSubtree = OGC3DTilesSystem::getSubtreeForTileCoord(componentState, tileset, rootCoord);
451 if (rootSubtree) { // Is the root subtree loaded and available?
452 rootNode = buildImplicitTree(componentState, tileset, rootCoord, globalTransform);
453 }
454 }
455 else { // Each tile has an explicit BBOX defined
456 rootNode = buildExplicitTree(componentState, tileset, &tileset->root, "", tileset->root.transform, globalTransform, visitedTilesets);
457 }
458
459 if (rootNode.valid) {
460 std::unordered_map<uint64_t, OGC3DTilesSystem::TileCandidate> tilesInView;
461 collectTileCandidates(componentState, &rootNode, tilesInView);
462
463#if 0
464 LOG_DEBUG(logger, "Tiles in view: %d\n", (int)tilesInView.size());
465 OGC3DTilesSystem::printDebugStats(componentState);
466#endif
467
468 // Add models which are in view but not yet loaded to the request queue.
469 loadMissingModels(componentState, tileset, tilesInView);
470
471 // Set visibility for already loaded models
472 calculateTileVisibilities(componentState, tileset, &rootNode);
473 }
474
475 // Cancel model-requests which are no longer valid (ie. models not currently in
476 // the viewport but has pending requests).
477 cancelStaleModelRequests(componentState, tileset, rootNode);
478
479 const size_t numReleased = pruneTileCache(componentState, tileset, gracePeriodBeforeTileRemoval);
480#if 0
481 if (numReleased != 0) {
482 LOG_DEBUG(logger, "Released %zu model(s) (frame #%d)", numReleased, context->time->getFrame());
483 }
484#else
485 (void)numReleased;
486#endif
487
488 processModelRequests(componentState, tileset, context->time->getFrame());
489
490 componentState->states[tileset].timestampOfLastTraversal = this->context->time->getAnimationTime();
491 componentState->states[tileset].candidateForDelayedTraversal = true;
492
493 if (tileset->root.useImplicitTiling) {
494 processSubtreeRequests(component, componentState, tileset);
495 cleanupPendingSubtreeRequests(componentState, tileset);
496 }
497
498 for (auto subTileset : visitedTilesets) {
499 assert(subTileset != componentState->tilesets[0] && "Don't traverse the main tileset twice!");
500 traverseTileset(component, componentState, subTileset, gracePeriodBeforeTileRemoval);
501 }
502}
503
507bool
508OGC3DTilesSystem::extractAndStoreOptionalSessionKeys(OGC3DTilesData* componentState, const std::string& url) const
509{
510 const std::string_view sessionToken = "session=";
511 const std::string_view keyToken = "key=";
512
513 size_t sessionTokenPos = url.find(sessionToken);
514 size_t sessionTokenEndPos = url.find("&");
515 if (sessionTokenPos != std::string::npos) {
516 size_t len = sessionTokenEndPos == std::string::npos ? std::string::npos : (sessionTokenEndPos - (sessionTokenPos + sessionToken.size()));
517 const std::string session = url.substr(sessionTokenPos + sessionToken.size(), len);
518
519 if (!componentState->optionalSessionKey.empty() && componentState->optionalSessionKey != session) {
520 LOG_WARNING(logger, "Multiple session keys ('session=') for one domain is not supported.");
521 }
522 componentState->optionalSessionKey = "session="+session;
523 LOG_DEBUG(logger, "Found session key '%s' in URL", session.c_str());
524 return true;
525 }
526 else {
527 LOG_DEBUG(logger, "No optional session key in URL '%s'", url.c_str());
528 }
529
530 // Sometimes the access "key=" key to use for tiles and other sub-tilesets are found in the URL of the main dataset.
531 // This is the case for Google datasets via Cesium f.ex. Confusing...
532 size_t keyTokenPos = url.find(keyToken);
533 size_t keyTokenEndPos = url.find("&");
534 if (keyTokenPos != std::string::npos) {
535 size_t len = keyTokenEndPos == std::string::npos ? std::string::npos : (keyTokenEndPos - (keyTokenPos + keyToken.size()));
536 const std::string key = url.substr(keyTokenPos + keyToken.size(), len);
537
538 if (!componentState->optionalAccessKey.empty() && componentState->optionalAccessKey != key) {
539 LOG_WARNING(logger, "Multiple access-keys ('key=') for one domain is not supported.");
540 }
541 componentState->optionalAccessKey = "key="+key;
542 LOG_DEBUG(logger, "Found access key '%s' in URL", key.c_str());
543 return true;
544 }
545 else {
546 LOG_DEBUG(logger, "No optional access key in URL '%s'", url.c_str());
547 }
548
549 return false;
550}
551
552std::string
553OGC3DTilesSystem::addOptionalURLParameters(const OGC3DTilesData* componentState, const std::string& url) const
554{
555 std::string result = url;
556 bool hasParams = url.find("?") != std::string::npos;
557
558 // Append optional session key
559 if (result.find(componentState->optionalSessionKey) == std::string::npos) {
560 result += hasParams ? "&" : "?";
561 result += componentState->optionalSessionKey;
562 hasParams = true;
563 }
564
565 // Some servers requires an alternative way to authenticate (e.g. tile.googleapis.com)
566 if (result.find(componentState->optionalAccessKey) == std::string::npos) {
567 result += hasParams ? "&" : "?";
568 result += componentState->optionalAccessKey;
569 hasParams = true;
570 }
571
572 if (!componentState->additionalURLParameters.empty()) {
573 result += hasParams ? "&" : "?";
574 result += componentState->additionalURLParameters;
575 }
576
577 return result;
578}
579
580void
581OGC3DTilesSystem::initializeTileset(OGC3DTilesComponent* component, Cogs::Core::OGC3DTilesData* componentState)
582{
583 LOG_INFO(logger, "Initializing a OGC3DTilesComponent with baseURL='%s'.", component->baseURL.c_str());
584 componentState->baseURL = component->baseURL;
585 componentState->additionalURLParameters = component->additionalURLParameters;
586
587 // Notify the users that we are currently initializing the tileset.
588 context->engine->invokeComponentNotifyCallback(*component, (int)OGC3DTilesNotification::TilesetInitializing, nullptr, 0);
589
590 componentState->assetsFetchId = Cogs::Core::DataFetcherManager::NoFetchId;
591 if (component->assetId >= 0) { // Fetch tileset.json via an assets-lookup (most likely cesium.com)
592 LOG_DEBUG(logger, "Fetching assets:\n URL='%s'\n accessToken='%s'\n AssetID=%d", component->baseURL.c_str(), component->accessToken.c_str(), component->assetId);
593 componentState->assetsFetchId = fetchAndInitializeAsset(component, componentState->uniqueComponentId);
594 }
595 else { // Fetch the tileset.json directly
596 std::string tilesetURL = component->baseURL + "/tileset.json";
597 tilesetURL = addOptionalURLParameters(componentState, tilesetURL);
598 const uint64_t tilesetGUID = Cogs::hash(tilesetURL);
599
600 if (componentState->tilesetFetchIds.contains(tilesetGUID)) {
601 LOG_ERROR(logger, "Tileset '%s' is already being fetched [%lu]", tilesetURL.c_str(), componentState->tilesetFetchIds[tilesetGUID]);
602 return;
603 }
604 tilesetURL = this->addOptionalURLParameters(componentState, tilesetURL);
605 componentState->tileBaseURL = component->baseURL; // Default tilebase URL
606 componentState->tilesetFetchIds[tilesetGUID] = fetchAndInitializeTileset(tilesetGUID, tilesetURL, component, componentState->uniqueComponentId);
607 }
608}
609
610Cogs::Core::DataFetcherManager::FetchId
611OGC3DTilesSystem::fetchAndInitializeAsset(OGC3DTilesComponent * component, uint32_t uniqueComponentId)
612{
613 return Cogs::Core::OGC3DTilesAssetsParser::fetch(this->context, component->baseURL, component->accessToken, component->assetId,
614 [thisp = this, component, uniqueComponentId](const OGC3DTilesAssetsParser::TilesAsset* tilesAsset) {
615 if (OGC3DTilesSystem::componentIsStale(thisp->context, uniqueComponentId)) { // Has component been destructed in the meanwhile?
616 return;
617 }
618
619 OGC3DTilesDataHolder& h = thisp->getData(component);
620 OGC3DTilesData* componentState = h.data.get();
621
622 // The 'tileset.json' name should be fetched from the assets.json file -- if not we'll use the std URL.
623 std::string tilesetURL = component->baseURL + "/tileset.json";
624 componentState->tileBaseURL = component->baseURL; // Default tilebase URL
625
626 if (tilesAsset != nullptr) {
627 LOG_DEBUG(logger, "Got asset data URL: '%s'", tilesAsset->url.c_str());
628 tilesetURL = tilesAsset->url;
629
630 thisp->extractAndStoreOptionalSessionKeys(componentState, tilesetURL);
631
632 // Extract the base-URL/domain
633 size_t pos = tilesetURL.find_first_of('/', std::string("https://").size()+1);
634 if (pos == std::string::npos) {
635 pos = tilesetURL.size();
636 }
637 componentState->tileBaseURL = tilesetURL.substr(0, pos);
638
639 // Choose between regular bearer access-token or optional access-key.
640 if (!tilesAsset->bearerToken.empty()) {
641 // This is the standard alternative to setting 'Authentication: Bearer <token>' in the HTTP-header of each request.
642 LOG_DEBUG(logger, "Appending bearer-token auth to tile requests URLs ('%s')", tilesAsset->bearerToken.c_str());
643 componentState->optionalAccessKey = "access_token=" + tilesAsset->bearerToken;
644 }
645
646 LOG_DEBUG(logger, "Tiles URL base is: '%s'", componentState->tileBaseURL.c_str());
647 }
648 else {
649 if (component->assetId >= 0) {
650 LOG_ERROR(logger, "Could not fetch asset data from '%s'\n", component->baseURL.c_str());
651 }
652 }
653
654 tilesetURL = thisp->addOptionalURLParameters(componentState, tilesetURL);
655
656 //
657 // Fetch the main tileset JSON
658 //
659 const uint64_t tilesetGUID = Cogs::hash(tilesetURL);
660 if (componentState->tilesetFetchIds.contains(tilesetGUID)) {
661 LOG_ERROR(logger, "Tileset '%s' is already being fetched [%lu]", tilesetURL.c_str(), componentState->tilesetFetchIds[tilesetGUID]);
662 return;
663 }
664
665 componentState->tilesetFetchIds[tilesetGUID] = thisp->fetchAndInitializeTileset(tilesetGUID, tilesetURL, component, uniqueComponentId);
666 componentState->assetsFetchId = Cogs::Core::DataFetcherManager::NoFetchId;
667 });
668}
669
670uint64_t
671OGC3DTilesSystem::fetchAndInitializeTileset(const uint64_t tilesetGUID, const std::string & tilesetURL, OGC3DTilesComponent * component, uint32_t uniqueComponentId)
672{
673 return Cogs::Core::OGC3DTilesTileset::fetch(this->context, tilesetURL,
674 [thisp=this, component, tilesetGUID, uniqueComponentId](OGC3DTilesTileset::Tileset* mainTileset) {
675 if (OGC3DTilesSystem::componentIsStale(thisp->context, uniqueComponentId)) { // Has component been destructed in the meanwhile?
676 return;
677 }
678
679 OGC3DTilesDataHolder& h = thisp->getData(component);
680 OGC3DTilesData* componentState = h.data.get();
681
682 if (mainTileset != nullptr) {
683 componentState->states[mainTileset] = OGC3DTilesData::State();
684 componentState->originalRootTransform = mainTileset->root.transform;
685 mainTileset->root.transform = glm::mat4(1.0);
686 LOG_INFO(logger, "Loaded tileset is of type '%s' [%p]", mainTileset->root.useImplicitTiling ? "IMPLICIT" : "EXPLICIT", mainTileset);
687
688 Cogs::Geometry::DBoundingBox bbox =
689 OGC3DTilesUtils::boundingVolumeConverter(mainTileset->root.boundingVolume, mainTileset->root.transform);
690 glm::vec3 center = (bbox.max - bbox.min) * 0.5 + bbox.min;
691
692 LOG_INFO(logger, "Tileset center: <%.2f, %.2f, %.2f> [%p]", center.x, center.y, center.z, mainTileset);
693 LOG_INFO(logger, "Tileset bbox: <%.2f, %.2f, %.2f>|<%.2f, %.2f, %.2f> [%p]", bbox.min.x, bbox.min.y, bbox.min.z, bbox.max.x, bbox.max.y, bbox.max.z, mainTileset);
694 LOG_INFO(logger, "Tileset bounding volume: %s", mainTileset->root.boundingVolume.toString().c_str());
695
696#if 1
697 // If BBOX'es are switched on we'll add the global bbox right away. This makes it way easier to locate
698 // the dataset if the offsets are large. NOTE: This needs to be done before the tileset-offset is set.
699 if (component->showTileBBoxes) {
700 Cogs::Geometry::DBoundingBox globalBBox =
701 OGC3DTilesUtils::boundingVolumeConverter(mainTileset->root.boundingVolume, mainTileset->root.transform);
702 thisp->addBBoxIndicatorToScene(componentState, mainTileset, 0, globalBBox);
703 }
704#endif
705
706 // If the tileset is in sparse/implicit-mode, we'll always load and cache the root subtree-file.
707 if (mainTileset->root.useImplicitTiling) {
708 LOG_INFO(logger, " Tree-structure is of type '%s' [%p]", mainTileset->root.implicitTiling.isOctTree ? "OCT" : "QUAD", mainTileset);
709 OGC3DTiles::Coord rootCoord = OGC3DTiles::Coord{ 0, 0, 0, mainTileset->root.implicitTiling.isOctTree ? 0 : -1 };
710 thisp->requestSubtree(componentState, mainTileset, rootCoord);
711 }
712
713 componentState->tilesets.push_back(mainTileset);
714
715 // Notify the users that the tileset has finished setting up.
716 thisp->context->engine->invokeComponentNotifyCallback(*component,
718 reinterpret_cast<const double *>(glm::value_ptr(componentState->originalRootTransform)),
719 16*sizeof(double));
720 }
721 else {
722 LOG_ERROR(logger, "Failed to initialize OGC3DTilesComponent (baseURL='%s')", component->baseURL.c_str());
723 }
724
725 componentState->tilesetFetchIds.erase(tilesetGUID);
726
727 LOG_DEBUG(logger, "Tileset fetched, initialized and ready");
728 thisp->context->engine->setDirty();
729 componentState->requiresTraversal = true;
730 });
731}
732
742size_t
743OGC3DTilesSystem::pruneTileCache(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, float gracePeriodBeforeTileRemoval) const
744{
745 // Do we need to perform any pruning?
746 const float qualityDelta = componentState->enableCaching ?
747 this->context->qualityService->ogc3DTilesSystemCacheControl - this->context->qualityService->ogc3DTilesSystemToleranceScale :
748 -1.0f;
749 if (qualityDelta >= 0.0) {
750 return 0;
751 }
752
753 // Add a tiny amount of slack in case the user sets the grace period to <= 0.0.
754 gracePeriodBeforeTileRemoval = std::max(2.0f/60.0f, gracePeriodBeforeTileRemoval);
755
756 std::set<std::pair<double, uint64_t>> toBeRemoved; // set<pair<timestamp, tileId>>
757 const double now = this->context->time->getAnimationTime();
758
759 // Collect tileId candidates
760 bool tileHidden = false;
761 for (const auto& [tileId, loadedModels] : componentState->states[tileset].tileCache) {
762 if (componentState->neverDiscardRootTiles &&
763 tileset == componentState->tilesets[0] &&
764 tileId == 0) { // Only applies to the main tileset
765 continue;
766 }
767
768 // Any models in the tile too old?
769 for (const LoadedModel& lm : loadedModels) {
770 if (lm.timestampLastUsed + gracePeriodBeforeTileRemoval < now) {
771 toBeRemoved.emplace(std::make_pair(lm.timestampLastUsed, tileId));
772 break;
773 }
774 }
775
776 // Hide all models which might be deleted later.
777 for (const LoadedModel& lm : loadedModels) {
778 if (lm.timestampLastUsed < now) {
779 EntityPtr entity = this->context->store->getEntity(lm.uniqueName, false);
780 if (entity) {
781 SceneComponent* sceneComponent = entity->getComponent<SceneComponent>();
782 sceneComponent->visible = false;
783 tileHidden = true;
784 sceneComponent->setChanged();
785 }
786 }
787 }
788 }
789
790 if (tileHidden) {
791 this->context->engine->setDirty();
792 }
793
794 if (toBeRemoved.empty()) {
795 return 0; // Nothing to do
796 }
797
798 std::list<std::pair<double, uint64_t>> sortedByTimestamp(toBeRemoved.begin(), toBeRemoved.end());
799 sortedByTimestamp.sort([](const std::pair<double, uint64_t>& a, const std::pair<double, uint64_t>& b) { return a.first > b.first; });
800
801 // Perform the removal from the scene
802 size_t numRemoved = 0;
803 // Try to cap the number of tiles to be removed based on how "bad" the current quality situation is.
804 const size_t numToBeRemoved = sortedByTimestamp.size() * size_t(-qualityDelta);
805
806 for (const auto& [timestamp, tileId] : sortedByTimestamp) {
807 removeTileFromScene(componentState, tileset, tileId);
808 numRemoved++;
809
810 if (numRemoved >= numToBeRemoved) { // Enough removed?
811 break;
812 }
813 }
814
815 return numRemoved;
816}
817
818void
819OGC3DTilesSystem::setVisibilityForAllTiles(const OGC3DTilesData* componentState, bool onoff) const
820{
821 // Set all tiles to visible.
822 for (auto tileset : componentState->tilesets) {
823 for (const auto& [tileId, loadedModels] : componentState->states.at(tileset).tileCache) {
824 setTileVisibility(componentState, tileset, tileId, onoff);
825 }
826 }
827}
828
829void
830OGC3DTilesSystem::setTileVisibility(const OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, uint64_t tileId, bool onoff) const
831{
832 if (!componentState->states.at(tileset).tileCache.contains(tileId)) {
833 //LOG_WARNING(logger, "Tried to change visibility of a tile not loaded to '%s': %s", onoff ? "TRUE" : "FALSE", tileId.c_str());
834 return;
835 }
836
837 const std::vector<LoadedModel>& loadedModels = componentState->states.at(tileset).tileCache.at(tileId);
838 for (const LoadedModel& lm : loadedModels) {
839 if (!modelIsReady(lm.modelHandle)) {
840 continue;
841 }
842
843 if (!lm.isValid) {
844 continue; // Node/model is registered but has no content.
845 }
846
847 EntityPtr entity = this->context->store->getEntity(lm.uniqueName, false);
848 if (entity) {
849 SceneComponent* sceneComponent = entity->getComponent<SceneComponent>();
850 if (sceneComponent->visible != onoff) {
851 sceneComponent->setChanged();
852 }
853 sceneComponent->visible = onoff;
854 }
855 else {
856 LOG_WARNING(logger, "Could not find '%s' for visibility toggling (tileset %p)", lm.URL.c_str(), tileset);
857 }
858 }
859}
860
861bool
862OGC3DTilesSystem::modelIsReady(const ModelHandle& modelhandle) const
863{
864 if (!modelhandle->isResident()) {
865 return false;
866 }
867
868 // Check mesh/geometry
869 for (const auto& mesh : modelhandle->meshes) {
870 if (!mesh->isActive()) {
871 return false;
872 }
873
874 if (mesh->hasAttachedResource()) {
875 const auto r = mesh->getAttachedResource();
876 if (!r->isActive()) {
877 return false;
878 }
879 }
880 }
881
882 // Check materials/textures
883 for (const auto& material : modelhandle->materials) {
884 if (!material->isResident()) {
885 return false;
886 }
887
888 for (const auto& tv : material->textureVariables) {
889 if (!tv.texture.handle->isResident()) {
890 return false;
891 }
892 }
893 }
894
895 return true;
896}
897
898void
899OGC3DTilesSystem::removeTileFromScene(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, uint64_t tileId) const
900{
901 const std::vector<LoadedModel>& loadedModels = componentState->states[tileset].tileCache[tileId];
902 for (const LoadedModel& lm : loadedModels) {
903 if (!lm.isValid) {
904 continue; // Node/Model is registeres but has no content
905 }
906
907 EntityPtr entity = this->context->store->getEntity(lm.uniqueName, false);
908 if (entity) {
909 this->context->store->removeChild(componentState->parentEntity, entity.get());
910 this->context->store->destroyEntity(entity->getId());
911 }
912 else {
913 // This might happen if a node was requested and registered but removed before it could be properly added to
914 // the scene.
915 LOG_WARNING(logger, "removeTileFromCache(): Entity '%s' (%s) not found.", lm.URL.c_str(), lm.uniqueName.c_str());
916 }
917 }
918
919 if (componentState->showTileBBoxes) {
920 removeBBoxIndicatorFromScene(componentState, tileset, tileId);
921 }
922
923 componentState->states[tileset].tileCache.erase(tileId);
924}
925
926void
927OGC3DTilesSystem::processModelRequests(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, uint32_t frameNumber) const
928{
929 std::vector<const std::string*> finished;
930 const double now = this->context->time->getAnimationTime();
931
932 for (const auto& [URL, pair] : componentState->states[tileset].modelRequests) {
933 TileCandidate tileCandidate = pair.first;
934 ModelHandle modelHandle = pair.second;
935 if (this->modelIsReady(modelHandle)) {
936 finished.emplace_back(&URL);
937
938#if DEBUG_CHECK_IF_INCOMING_MODEL_EXISTS // Sanity check while developing
939 const std::vector<LoadedModel>& loadedModels = componentState->tileCache[tileCandidate.tileId];
940 bool alreadyLoaded = std::any_of(loadedModels.begin(), loadedModels.end(), [&url=URL](const LoadedModel & item) { return item.URL == url; });
941 if (alreadyLoaded) {
942 LOG_ERROR(logger, "Incoming model '%s' is already loaded", URL.c_str());
943 assert(alreadyLoaded && "Incoming model is already loaded for this tile!");
944 continue;
945 }
946#endif
947
948#if DEBUG_ADD_URL_TO_MODEL_NAMES
949 // A unique name based on TileID + Full URL + ptr-value of tileset.
950 const std::string uniqueName = (tileset->root.useImplicitTiling ? OGC3DTiles::Coord::fromHash(tileCandidate.tileId).toString() : std::to_string(tileCandidate.tileId)) + " - " + URL;
951#else
952 // A unique name based on TileID + ptr-value of tileset.
953 std::string uniqueName = tileset->root.useImplicitTiling ? OGC3DTiles::Coord::fromHash(tileCandidate.tileId).toString() : std::to_string(tileCandidate.tileId);
954 uniqueName += " - ";
955 uniqueName += std::to_string(uint64_t(tileset));
956#endif
957
958 if (this->context->store->findEntity(uniqueName)) {
959 // The model was already added. This can happen if a tile goes in and out of view, gets
960 // in the "might get deleted" phase -- but then suddenly get back into view and
961 // therefore rescheduled.
962 continue;
963 }
964
965 LoadedModel loadedModel;
966 loadedModel.modelHandle = modelHandle;
967 loadedModel.timestampLastUsed = now;
968 loadedModel.URL = URL;
969 loadedModel.uniqueName = uniqueName;
970 loadedModel.frameNumberAtArrival = frameNumber;
971
972 if (!OGC3DTilesSystem::modelIsEmpty(modelHandle)) {
973 addModelToScene(loadedModel.uniqueName, tileCandidate, modelHandle, componentState);
974 loadedModel.isValid = true;
975 componentState->requiresTraversal = true; // Ensure re-traversal to recalc visibility for all existing nodes wrt new nodes.
976 this->context->engine->setDirty();
977 }
978 componentState->states[tileset].tileCache[tileCandidate.tileId].emplace_back(loadedModel);
979 }
980 else if (modelHandle->hasFailedLoad()) {
981 LOG_ERROR(logger, "processModelRequests(): Could not load '%s'", URL.c_str());
982 finished.emplace_back(&URL);
983 }
984 else if (modelHandle->hasFailedActivation()) {
985 // FIXME: What does "failed activation" actually mean?
986 LOG_ERROR(logger, "processModelRequests(): Model failed to activate!");
987 // FIXME: We used to assert here, but that will crash the app. Too strict?
988 finished.emplace_back(&URL);
989 }
990 }
991
992 for (const std::string* URL : finished) {
993 componentState->states[tileset].modelRequests.erase(*URL);
994 }
995}
996
1002bool
1003OGC3DTilesSystem::requestModel(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, const TileCandidate& tile, const std::string URL) const
1004{
1005#if 0 // Sanity check
1006 // Sometimes a content-URL is not a model but a completely new tileset json-file. This should have been
1007 // handled earlier, not here!
1008 if (URL.find(".json") != std::string::npos) {
1009 LOG_ERROR(logger, "Internal error: A sub-tileset was requested as model; '%s'.", URL.c_str());
1010 assert(false && "Internal error");
1011 return false;
1012 }
1013#endif
1014
1015 if (!componentState->states[tileset].modelRequests.contains(URL)) {
1016 const std::vector<LoadedModel>& loadedModels = componentState->states[tileset].tileCache[tile.tileId];
1017 bool alreadyLoaded = std::ranges::any_of(loadedModels.cbegin(), loadedModels.cend(), [&URL](LoadedModel const& item) { return item.URL == URL; });
1018 if (!alreadyLoaded) {
1019 ModelHandle handle = context->modelManager->loadModel(URL, NoResourceId, ModelLoadFlags::None);
1020 componentState->states[tileset].modelRequests[URL] = std::pair(tile, handle);
1021 return true;
1022 }
1023 else {
1024 //LOG_DEBUG(logger, "Tile '%s' (%s) has already been loaded.\n", URL.c_str(), tile.tileId.c_str());
1025 }
1026 }
1027 else {
1028 //LOG_DEBUG(logger, "Tile '%s' (%s) has already been requested.\n", URL.c_str(), tile.tileId.c_str());
1029 return true;
1030 }
1031 return false;
1032}
1033
1039void
1040OGC3DTilesSystem::loadMissingModels(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, const std::unordered_map<uint64_t, TileCandidate>& tilesInView) const
1041{
1042 std::vector<const TileCandidate*> sortedTilesInView;
1043 sortedTilesInView.reserve(tilesInView.size());
1044 for (auto it = tilesInView.cbegin(); it != tilesInView.cend(); ++it) {
1045 sortedTilesInView.emplace_back(&it->second);
1046 }
1047
1048#if 0
1049 // Sort tilesInView according to distance from camera instead of the default "level"-sorting.
1050 const glm::vec3 cameraPos = getCurrentCameraPosition();
1051 std::ranges::sort(sortedTilesInView.begin(), sortedTilesInView.end(),
1052 [cameraPos](TileCandidate const* a, TileCandidate const* b) {
1053 const double distA = OGC3DTilesUtils::distanceFromBBox(cameraPos, a->bbox);
1054 const double distB = OGC3DTilesUtils::distanceFromBBox(cameraPos, b->bbox);
1055 return distA < distB;
1056 });
1057#else
1058 // Sort according to tile-depth
1059 std::ranges::sort(sortedTilesInView.begin(), sortedTilesInView.end(),
1060 [](const TileCandidate* a, const TileCandidate* b) {
1061 return a->depth < b->depth;
1062 });
1063#endif
1064
1065 const double now = this->context->time->getAnimationTime();
1066 int numScheduled = 0;
1067
1068 int maxNumRequestsPerFrame = context->variables->get(maxNumModelRequestsPerFrameName, MAX_NUM_MODEL_REQUESTS_PER_FRAME);
1069 if (maxNumRequestsPerFrame <= 0) {
1070 LOG_WARNING_ONCE(logger, "Variable '%s' is <= 0. Resetting to %d.",
1071 maxNumModelRequestsPerFrameName.to_string().c_str(), MAX_NUM_MODEL_REQUESTS_PER_FRAME);
1072 maxNumRequestsPerFrame = MAX_NUM_MODEL_REQUESTS_PER_FRAME;
1073 context->variables->set(maxNumModelRequestsPerFrameName, MAX_NUM_MODEL_REQUESTS_PER_FRAME);
1074 }
1075
1076 int maxNumActiveRequests = context->variables->get(maxNumActiveModelRequestsName, MAX_NUM_ACTIVE_MODEL_REQUESTS);
1077 if (maxNumActiveRequests <= 0) {
1078 LOG_WARNING_ONCE(logger, "Variable '%s' is <= 0. Resetting to %d.",
1079 maxNumActiveModelRequestsName.to_string().c_str(), MAX_NUM_ACTIVE_MODEL_REQUESTS);
1080 maxNumActiveRequests = MAX_NUM_ACTIVE_MODEL_REQUESTS;
1081 context->variables->set(maxNumActiveModelRequestsName, MAX_NUM_ACTIVE_MODEL_REQUESTS);
1082 }
1083
1084 OGC3DTilesData::State* state = &componentState->states[tileset];
1085 state->pendingModelRequests.reserve(sortedTilesInView.size());
1086
1087 for (const auto* tilecandidate : sortedTilesInView) {
1088 for (std::string URL : tilecandidate->URLs) {
1089 // Is the limit for total number of active requests reached?
1090 if (state->modelRequests.size() >= size_t(maxNumActiveRequests)) {
1091 // Register as pending
1092 state->pendingModelRequests.emplace(tilecandidate->tileId);
1093 }
1094 else { // Start a new request
1095 if (numScheduled < maxNumRequestsPerFrame) {
1096 if (requestModel(componentState, tileset, *tilecandidate, addOptionalURLParameters(componentState, URL))) {
1097 numScheduled++;
1098 if (componentState->showTileBBoxes) {
1099 addBBoxIndicatorToScene(componentState, tileset, tilecandidate->tileId, tilecandidate->bbox);
1100 }
1101 }
1102 }
1103 else {
1104 // We need to register all requests that are still pending.
1105 state->pendingModelRequests.emplace(tilecandidate->tileId);
1106 }
1107 }
1108 }
1109
1110 // Model already loaded? Update timestamp to prevent culling.
1111 if (state->tileCache.contains(tilecandidate->tileId)) {
1112 std::vector<LoadedModel>& loadedModels = state->tileCache[tilecandidate->tileId];
1113 for (LoadedModel& m : loadedModels) {
1114 m.timestampLastUsed = now;
1115 }
1116 }
1117 }
1118
1119 componentState->statistics.numberOfPendingRequests = (int) componentState->states[tileset].pendingModelRequests.size();
1120}
1121
1122// Only relevant for IMPLICIT trees
1123std::string
1124OGC3DTilesSystem::buildSubtreeURL(const OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, const OGC3DTiles::Coord& coord)
1125{
1126 std::string path = std::string(tileset->root.implicitTiling.subTreeURLScheme);
1127 path = path.replace(path.find("{level}"), 7, std::to_string(coord.level));
1128 path = path.replace(path.find("{x}"), 3, std::to_string(coord.x));
1129 path = path.replace(path.find("{y}"), 3, std::to_string(coord.y));
1130 if (coord.z >= 0) {
1131 path = path.replace(path.find("{z}"), 3, std::to_string(coord.z));
1132 }
1133
1134 return componentState->tileBaseURL + "/" + path;
1135}
1136
1143uint64_t
1145{
1146 assert(coord.level <= 0xFFFF && coord.x <= 0xFFFF && coord.y <= 0xFFFF && "Coord values out of the 16bit bounds");
1147 uint64_t hash = coord.level + (uint64_t(coord.x) << 16) + (uint64_t(coord.y) << 32);
1148
1149 if (coord.z >= 0) {
1150 assert(coord.z <= 0xFFFF && "Coord.z value out of the 16bit bounds");
1151 hash += uint64_t(coord.z) << 48;
1152 }
1153 return hash;
1154}
1155
1159bool
1160OGC3DTilesSystem::tileHasReadyContent(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, uint64_t tileId) const
1161{
1162 // Any pending dependencies?
1163 if (tileHasModelRequests(componentState, tileset, tileId) ||
1164 tileHasSubTilesetRequests(componentState, tileId)) {
1165 return false;
1166 }
1167
1168 assert(componentState->states[tileset].tileCache.contains(tileId) && "Tile not in cache");
1169
1170 const std::vector<LoadedModel>& models = componentState->states[tileset].tileCache[tileId];
1171 size_t numReadyModels = 0;
1172 for (const auto& lm : models) {
1173 if (!lm.isValid) {
1174 numReadyModels += 1;
1175 continue; // Node/tile is registered but has no content
1176 }
1177
1178 if (modelIsReady(lm.modelHandle)) {
1179 numReadyModels += 1;
1180 }
1181 else {
1182 componentState->numberOfModelsBeingPrepared += 1;
1183 }
1184 }
1185
1186 return models.size() == numReadyModels;
1187}
1188
1189/*
1190* Return TRUE if a tile has ongoing model requests or pending requests (requests not started yet due to throttling).
1191*/
1192bool
1193OGC3DTilesSystem::tileHasModelRequests(const OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, uint64_t tileId) const
1194{
1195 for (const auto& [URL, pair] : componentState->states.at(tileset).modelRequests) {
1196 const TileCandidate& tileCandidate = pair.first;
1197 if (tileCandidate.tileId == tileId) {
1198 return true;
1199 }
1200 }
1201
1202 if (componentState->states.at(tileset).pendingModelRequests.contains(tileId)) {
1203 return true;
1204 }
1205
1206 return false;
1207}
1208
1209bool
1210OGC3DTilesSystem::tileHasSubTilesetRequests(const OGC3DTilesData* componentState, uint64_t tileId) const
1211{
1212 for (const auto& [URL, pair] : componentState->subTilesetRequests) {
1213 if (pair.first == tileId) {
1214 return true;
1215 }
1216 }
1217 return false;
1218}
1219
1224bool
1226{
1227 size_t numReadyChildren = 0;
1228 for (auto const& [childTileId, child] : node->children) {
1229 if (child.tile.URLs.empty()) { // Child has no content. Go down a level?
1230 if (allChildrenAreReady(componentState, tileset, &child)) {
1231 numReadyChildren += 1;
1232 }
1233 }
1234 else {
1235 if (tileHasReadyContent(componentState, tileset, childTileId)) {
1236 numReadyChildren += 1;
1237 }
1238 else {
1239 return false;
1240 }
1241 }
1242 }
1243
1244 return node->children.size() == numReadyChildren;
1245}
1246
1250void
1251OGC3DTilesSystem::collectTileCandidates(OGC3DTilesData* componentState, const Node* node, std::unordered_map<uint64_t, TileCandidate>& target) const
1252{
1253#if 0 // Sanity check
1254 if (target.contains(node->tile.tileId)) {
1255 assert(false && "Node has already been visited. Internal error.");
1256 }
1257#endif
1258
1259 if (!node->tile.URLs.empty()) {
1260 target[node->tile.tileId] = node->tile;
1261 }
1262
1263 for (auto const& [childTileId, child] : node->children) {
1264 collectTileCandidates(componentState, &child, target);
1265 }
1266}
1267
1272void
1274{
1275 if (node->replaceRefine) {
1276 if (!componentState->waitForTileSiblings) {
1277 if (node->children.empty()) { // Always show leaf nodes (if ready)
1278 setTileVisibility(componentState, tileset, node->tile.tileId, true);
1279 }
1280 else if (allChildrenAreReady(componentState, tileset, node)) {
1281 setTileVisibility(componentState, tileset, node->tile.tileId, false);
1282 }
1283 }
1284 else {
1285 if (componentState->states[tileset].tileCache.contains(node->tile.tileId)) {
1286 bool showNode = true;
1287 if (!node->children.empty() &&
1288 tileHasReadyContent(componentState, tileset, node->tile.tileId) &&
1289 allChildrenAreReady(componentState, tileset, node)) {
1290 // All children are ready. Hide this node.
1291
1292 showNode = false;
1293 }
1294 setTileVisibility(componentState, tileset, node->tile.tileId, showNode);
1295 }
1296 }
1297 }
1298
1299 for (auto const& [childTileId, child] : node->children) {
1300 calculateTileVisibilities(componentState, tileset, &child); // Recursive
1301 }
1302}
1303
1312 const OGC3DTilesTileset::Tileset* tileset,
1313 const OGC3DTiles::Coord& coord,
1314 const glm::mat4& globalTransform) const
1315{
1316 Node node;
1317 node.valid = false;
1318 node.replaceRefine = tileset->root.refine == OGC3DTilesTileset::RefineType::REPLACE;
1319
1320 componentState->statistics.traversedDepth = std::max(componentState->statistics.traversedDepth, coord.level);
1321
1322 OGC3DTiles::BoundingVolume tileBoundingVolume =
1323 OGC3DTilesUtils::getBoundingVolumeFromCoord(coord, tileset->root.boundingVolume);
1324 Cogs::Geometry::DBoundingBox tileBBox =
1325 OGC3DTilesUtils::boundingVolumeConverter(tileBoundingVolume, tileset->root.transform);
1326 Cogs::Geometry::DBoundingBox transformedTileBBox(tileBBox);
1327
1328 OGC3DTilesUtils::transformBBox(transformedTileBBox, globalTransform);
1329
1330 const bool isInside = isInsideFrustum(transformedTileBBox);
1331
1332 if (!isInside && !componentState->loadAllSiblings) {
1333 return node; // Don't dig deeper
1334 }
1335
1336 OGC3DTilesSubtree::Subtree const* subtree =
1337 OGC3DTilesSystem::getSubtreeForTileCoord(componentState, tileset, coord);
1338 if (!subtree) {
1339 return node;
1340 }
1341
1342 const double distance = OGC3DTilesUtils::distanceFromBBox(getCurrentCameraPosition(), transformedTileBBox);
1343
1344 //
1345 // Shall we dig deeper or use content at this level?
1346 //
1347 const double geometricError = tileset->geometricError / OGC3DTilesUtils::fastPow2(coord.level);
1348 const double factor = componentState->detailFactor * geometricError;
1349 const bool hasContent = OGC3DTilesSubtree::hasContent(subtree, coord);
1350
1351 if ((distance < factor || !hasContent) && isInside) {
1352 // Collect all child nodes to "coord" and evaluate each one using recursion
1353 std::vector<OGC3DTiles::Coord> children =
1354 getTileCoordChildren(componentState, tileset, coord);
1355
1356 bool stop = false;
1357 if (componentState->dontRenderLeaves) {
1358 // If any of the children is a leaf node we'll have to stop as we
1359 // are instructed to never render leaf nodes.
1360 for (const OGC3DTiles::Coord& child : children) {
1361 if (getTileCoordChildren(componentState, tileset, child).empty()) {
1362 stop = true;
1363 break;
1364 }
1365 }
1366 }
1367
1368 if (!stop) {
1369 for (const OGC3DTiles::Coord& childCoord : children) {
1370 assert(coord != childCoord && "Internal error");
1371 assert(childCoord.level != 0 && "Coord with level=0 cannot be a child");
1372
1373 // *** Recursion ***
1374 const Node child = buildImplicitTree(componentState, tileset, childCoord, globalTransform);
1375 if (child.valid) {
1376 node.children[child.tile.tileId] = child;
1377 }
1378 }
1379 }
1380 }
1381 else {
1382 // Node is to far away (or not inside frustum and we are loading siblings). Don't dig any deeper.
1383 }
1384
1385 //
1386 // Add contents/geometry for 'coord' if there is any
1387 //
1388 if (hasContent) {
1389 assert(tileset->root.contents.size() > 0 && "No content!");
1390 std::string path = tileset->root.contents[0].URI;
1391 path = path.replace(path.find("{level}"), 7, std::to_string(coord.level));
1392 path = path.replace(path.find("{x}"), 3, std::to_string(coord.x));
1393 path = path.replace(path.find("{y}"), 3, std::to_string(coord.y));
1394 if (coord.z >= 0) {
1395 path = path.replace(path.find("{z}"), 3, std::to_string(coord.z));
1396 }
1397
1398 node.tile.URLs.emplace_back(OGC3DTilesSystem::compileURL(componentState->tileBaseURL, tileset->baseURL, path));
1399 node.tile.transform = tileset->root.transform;
1400 }
1401
1402 node.tile.tileId = coord.toHash();
1403 node.tile.depth = coord.level;
1404 node.tile.bbox = tileBBox;
1405 node.valid = true;
1406 return node;
1407}
1408
1417 const OGC3DTilesTileset::Tileset* tileset,
1418 const OGC3DTilesTileset::Tile* tile,
1419 const std::string& tileIdStr,
1420 const glm::mat4& currentTransform,
1421 const glm::mat4& globalTransform,
1422 std::unordered_set<const OGC3DTilesTileset::Tileset*> & visitedTilesets) const
1423{
1424 Node node;
1425 node.valid = false;
1426 node.replaceRefine = tile->refine == OGC3DTilesTileset::RefineType::REPLACE;
1427 componentState->statistics.traversedDepth = std::max(componentState->statistics.traversedDepth, (int)tileIdStr.size());
1428
1429 glm::mat4 transform = currentTransform;
1430 Cogs::Geometry::DBoundingBox bbox = OGC3DTilesUtils::boundingVolumeConverter(tile->boundingVolume, transform);
1431 Cogs::Geometry::DBoundingBox transformedBBox(bbox);
1432 OGC3DTilesUtils::transformBBox(transformedBBox, globalTransform);
1433
1434 const bool isInside = isInsideFrustum(transformedBBox);
1435
1436 if (!isInside && !componentState->loadAllSiblings) {
1437 return node; // Node is not visible. No need to continue.
1438 }
1439
1440 const double distance = OGC3DTilesUtils::distanceFromBBox(getCurrentCameraPosition(), transformedBBox);
1441 const double factor = (node.tile.depth + 1) * std::pow(componentState->detailFactor, 2.0) * tile->geometricError;
1442 const bool hasContent = !tile->contents.empty();
1443
1444 if ((distance < factor || !hasContent) && isInside) {
1445 bool stop = false;
1446 if (componentState->dontRenderLeaves) {
1447 // If any of the children is a leaf node we'll have to stop as we
1448 // are instructed to never render leaf nodes.
1449 for (size_t i=0; i < tile->children.size(); ++i) {
1450 const OGC3DTilesTileset::Tile* childTile = &tile->children.at(i);
1451 if (childTile->children.empty()) {
1452 stop = true;
1453 break;
1454 }
1455 }
1456 }
1457
1458 if (!stop) {
1459 for (size_t i = 0; i < tile->children.size(); ++i) {
1460 const OGC3DTilesTileset::Tile* childTile = &tile->children.at(i);
1461 assert(!childTile->useImplicitTiling && "Internal error");
1462
1463 // ** Recursion **
1464 Node childNode = buildExplicitTree(componentState, tileset, childTile, tileIdStr + std::to_string(i), transform, globalTransform, visitedTilesets);
1465
1466 if (childNode.valid) {
1467 assert(node.children.find(childNode.tile.tileId) == node.children.end() && "Child node has already been added");
1468 node.children[childNode.tile.tileId] = childNode;
1469 }
1470 }
1471 }
1472 }
1473
1474 //
1475 // Add any content available on the current level
1476 //
1477 node.tile.tileId = Cogs::hash(tileIdStr);
1478 node.tile.depth = (uint32_t)tileIdStr.length();
1479 node.tile.bbox = bbox;
1480 node.tile.transform = transform;
1481 node.tile.URLs.reserve(tile->contents.size());
1482 node.valid = true;
1483
1484 for (const OGC3DTilesTileset::Content& c : tile->contents) {
1485 const std::string url = OGC3DTilesSystem::compileURL(componentState->tileBaseURL, tileset->baseURL, c.URI);
1486 // Sub-tileset?
1487 if (c.URI.find(".json") != std::string::npos || c.URI.find(".JSON") != std::string::npos) {
1488 const std::string tilesetURL = addOptionalURLParameters(componentState, url);
1489 if (componentState->states[tileset].subTilesets.contains(tilesetURL)) {
1490 visitedTilesets.insert(componentState->states[tileset].subTilesets[tilesetURL]);
1491 componentState->states[tileset].candidateForDelayedTraversal = true;
1492 componentState->statistics.numberOfVisitedSubTilesets += 1;
1493 }
1494 else {
1495 // We have encountered a new sub-tileset. We'll load and register it in ::processSubTilesetsRequests()
1496 requestSubTileset(componentState, tilesetURL, node.tile.tileId, tileset);
1497 }
1498 }
1499 else {
1500 node.tile.URLs.emplace_back(url);
1501 }
1502 }
1503
1504 return node;
1505}
1506
1507// Only relevant for EXPLICIT trees
1508void
1509OGC3DTilesSystem::requestSubTileset(OGC3DTilesData* componentState, const std::string& URL, uint64_t tileId, const OGC3DTilesTileset::Tileset* parentTileset) const
1510{
1511 if (componentState->subTilesetRequests.size() > MAX_NUM_ACTIVE_SUBTILESET_REQUESTS) {
1512 return;
1513 }
1514
1515 if (componentState->loadedSubTilesets.contains(URL) ||
1516 componentState->subTilesetRequests.contains(URL)) {
1517 return;
1518 }
1519
1520 componentState->subTilesetRequests[URL] = std::make_pair(tileId, parentTileset);
1521}
1522
1526void
1527OGC3DTilesSystem::addModelToScene(const std::string& uniqueName, TileCandidate tile, ModelHandle handle, const OGC3DTilesData* componentState) const
1528{
1529 assert(!this->context->store->findEntity(uniqueName) && "Model already exists!");
1530
1531 EntityPtr entity = this->context->store->createEntity(uniqueName, "ModelEntity");
1532 this->context->store->addChild(componentState->parentEntity, entity);
1533
1534 ModelComponent* modelComponent = entity->getComponent<ModelComponent>();
1535
1536 if (componentState->colorTileAccordingToTreeDepth) {
1537 modelComponent->inheritMaterial = true;
1538 modelComponent->materialInstance = debugTileTreeDepthColorMaterials[tile.depth % debugTileTreeDepthColorMaterials.size()];
1539 }
1540 else if (componentState->overrideMaterial) {
1541 modelComponent->inheritMaterial = true;
1542 modelComponent->materialInstance = componentState->overrideMaterial;
1543 }
1544
1545 modelComponent->model = handle;
1546
1547 // Never cast shadows.
1548 // FIXME: If we ever want to support shadows we can expose this option as a flag in
1549 // OGC3DTilesComponent and let the user decide.
1550 RenderComponent* renderComponent = entity->getComponent<RenderComponent>();
1551 renderComponent->setRenderFlag(RenderFlags::CastShadows, false);
1552 MaterialComponent* materialComponent = entity->getComponent<MaterialComponent>();
1553 materialComponent->shadowReceiver = false;
1554
1555 TransformComponent* transformComponent = entity->getComponent<TransformComponent>();
1556 transformComponent->transform.matrix = tile.transform;
1557
1558 transformComponent->transformFlags = 1; // use the matrix -- not position, rotation & scale
1559 transformComponent->setChanged();
1560}
1561
1562void
1563OGC3DTilesSystem::addBBoxIndicatorToScene(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, uint64_t tileId, Cogs::Geometry::DBoundingBox box) const
1564{
1565 assert(componentState->states.contains(tileset) && "Internal error");
1566 if (!componentState->states[tileset].bboxIndicatorCache.contains(tileId)) {
1567 std::string title = "BBox ";
1568 title += tileset->root.useImplicitTiling ? OGC3DTiles::Coord::fromHash(tileId).toString() : std::to_string(tileId);
1569 title += " - ";
1570 title += std::to_string(uint64_t(tileset));
1571 EntityPtr bboxEntity = this->context->store->createEntity(title, "SubMeshPart");
1572 this->context->store->addChild(componentState->parentEntity, bboxEntity);
1573
1574 componentState->states[tileset].bboxIndicatorCache[tileId] = bboxEntity;
1575
1576 auto createBBoxTask = [ctx = this->context, bboxEntity, box]() {
1577 MeshComponent* meshComponent = bboxEntity->getComponent<MeshComponent>();
1578
1579 MaterialInstanceHandle material = ctx->materialInstanceManager->createMaterialInstance(ctx->materialManager->getDefaultMaterial());
1580 material->setPermutation("Line");
1581 material->setBoolProperty(DefaultMaterial::EnableLighting, false);
1582 const float r = (rand() % 255) / 255.0f;
1583 material->setVec4Property(DefaultMaterial::DiffuseColor, BBOX_BASE_COLOR_FORMAT);
1584 material->options.depthBiasEnabled = true;
1585 material->options.depthBias.constant = -1.f;
1586 material->options.depthBias.slope = -1.f;
1587 material->options.depthBias.clamp = 100.f;
1588
1589 // We'll translate the BBox to origo and let the OGC3DTilesEntity's transform handle the global offset.
1590 std::array<PositionVertex, 8> vertices = {
1591 PositionVertex{box.min},
1592 PositionVertex{glm::dvec3(box.min.x, box.max.y, box.min.z)},
1593 PositionVertex{glm::dvec3(box.max.x, box.max.y, box.min.z)},
1594 PositionVertex{glm::dvec3(box.max.x, box.min.y, box.min.z)},
1595 PositionVertex{box.max},
1596 PositionVertex{glm::dvec3(box.min.x, box.max.y, box.max.z)},
1597 PositionVertex{glm::dvec3(box.min.x, box.min.y, box.max.z)},
1598 PositionVertex{glm::dvec3(box.max.x, box.min.y, box.max.z)},
1599 };
1600
1601 std::array<unsigned int, 16> lineStripIndices = {
1602 0, 1, 2, 3, 0, // Bottom + Edge
1603 6, 7, 4, 5, 6, // Top
1604 7, 3, // Edge
1605 2, 4, // Edge
1606 5, 1 // Edge
1607 };
1608
1609 SubMeshRenderComponent* meshRenderComponent = bboxEntity->getComponent<SubMeshRenderComponent>();
1610 meshRenderComponent->setMaterial(material);
1611 meshRenderComponent->setRenderFlag(RenderFlags::CastShadows, false);
1612
1613 MeshHandle meshHandle = ctx->meshManager->create();
1614 meshComponent->meshHandle = meshHandle;
1615
1616 Cogs::Geometry::BoundingBox fbox;
1617 fbox += box.min;
1618 fbox += box.max;
1619 meshHandle->setBounds(fbox);
1620
1621 Mesh* mesh = ctx->meshManager->get(meshHandle);
1622 assert(mesh && "Internal error");
1623 mesh->setVertexData(vertices.data(), vertices.size());
1624 mesh->addSubMesh(std::span(lineStripIndices), PrimitiveType::LineStrip);
1625
1626 meshRenderComponent->setChanged();
1627 meshComponent->setChanged();
1628 mesh->setChanged();
1629 };
1630
1631 this->context->taskManager->enqueue(TaskManager::GlobalQueue, createBBoxTask);
1632 }
1633}
1634
1635void
1636OGC3DTilesSystem::removeBBoxIndicatorFromScene(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, uint64_t tileId) const
1637{
1638 auto it = componentState->states[tileset].bboxIndicatorCache.find(tileId);
1639 if (it != componentState->states[tileset].bboxIndicatorCache.end()) {
1640 EntityPtr entity = it->second;
1641 this->context->store->removeChild(componentState->parentEntity, entity.get());
1642 this->context->store->destroyEntity(entity->getId());
1643 componentState->states[tileset].bboxIndicatorCache.erase(tileId);
1644 }
1645 else {
1646 if (tileset->root.useImplicitTiling) {
1647 LOG_ERROR(logger, "Instructed to remove BBox '%s', but it does not exist.", OGC3DTiles::Coord::fromHash(tileId).toString().c_str());
1648 }
1649 else {
1650 LOG_ERROR(logger, "Instructed to remove BBox '%llu', but it does not exist.", tileId);
1651 }
1652 }
1653}
1654
1657{
1659 OGC3DTilesComponent* comp = static_cast<OGC3DTilesComponent*>(handle.resolve());
1660 OGC3DTilesDataHolder& h = this->getData(comp);
1661 h.data = std::make_unique<OGC3DTilesData>();
1662
1663 OGC3DTilesData* data = h.data.get();
1664 data->uniqueComponentId = createUniqueComponentId();
1665
1666 if (this->pool.size() >= 1 && this->bounds == nullptr) {
1667 this->bounds = new OGC3DTilesBounds(this);
1668 this->context->bounds->addBoundsExtension(this->bounds);
1669 }
1670
1671 return handle;
1672}
1673
1674void
1676{
1677 OGC3DTilesComponent* comp = static_cast<OGC3DTilesComponent*>(componenthandle.resolve());
1678 LOG_DEBUG(logger, "Destroying OGC3DTilesComponent (baseURL=%s)", comp->baseURL.c_str());
1679
1680 OGC3DTilesDataHolder& h = this->getData(comp);
1681 OGC3DTilesData* componentState = h.data.get();
1682
1683 if (componentState->assetsFetchId != Cogs::Core::DataFetcherManager::NoFetchId) {
1684 Cogs::Core::DataFetcherManager::cancelAsyncFetch(this->context, componentState->assetsFetchId);
1685 }
1686
1687 for (auto & [guid, fetchId] : componentState->tilesetFetchIds) {
1688 Cogs::Core::DataFetcherManager::cancelAsyncFetch(this->context, fetchId);
1689 }
1690
1691 for (auto tileset : componentState->tilesets) {
1692 for (auto& [guid, fetchId] : componentState->states[tileset].subtreeFetchIds) {
1693 Cogs::Core::DataFetcherManager::cancelAsyncFetch(this->context, fetchId);
1694 }
1695
1696 for (const auto& [tileId, loadedModels] : componentState->states[tileset].tileCache) {
1697 for (const LoadedModel& lm : loadedModels) {
1698 if (!lm.isValid) {
1699 continue; // Node/model is registered but has no content
1700 }
1701 EntityPtr entity = this->context->store->getEntity(lm.uniqueName);
1702 if (entity) {
1703 this->context->store->destroyEntity(entity->getId());
1704 }
1705 else {
1706 LOG_ERROR(logger, "destroyComponent(): Could not find entity '%s' (%s)", lm.URL.c_str(), lm.uniqueName.c_str());
1707 }
1708 }
1709 }
1710
1711 // Clean subtreeCache
1712 for (auto& [id, subtreeMap] : componentState->states[tileset].subtreeCache) {
1713 for (auto& [key, val] : subtreeMap) {
1714 delete val;
1715 }
1716 subtreeMap.clear();
1717 }
1718
1719 // Clean bboxIndicatorCache
1720 for (auto& b : componentState->states[tileset].bboxIndicatorCache) {
1721 context->store->destroyEntity(b.second->getId());
1722 }
1723
1724 delete tileset;
1725 }
1726
1727 componentState->tilesets.clear();
1728 componentState->states.clear();
1729
1730 if (this->bounds != nullptr) {
1731 this->context->bounds->removeBoundsExtension(this->bounds);
1732 }
1733
1735}
1736
1737glm::dvec3
1738OGC3DTilesSystem::getCurrentCameraPosition() const
1739{
1740 return this->context->cameraSystem->getMainCameraData().inverseViewMatrix * glm::dvec4(0, 0, 0, 1.0);
1741}
1742
1743bool
1744OGC3DTilesSystem::isInsideFrustum(const Cogs::Geometry::DBoundingBox& box) const
1745{
1746#if 1
1747 const Cogs::Geometry::Frustum frustum = this->context->cameraSystem->getMainCameraData().frustum;
1748 Cogs::Geometry::BoundingBox fbox;
1749 fbox.min = box.min;
1750 fbox.max = box.max;
1751 const int result = Cogs::Geometry::contains(frustum, fbox);
1752 return result == 2 || result == 1; // Totally inside || partially inside
1753#else
1754 // Alternative method of checking if a bbox intersects the view frustum based on the
1755 // camera's view-projection matrix.
1756 // NOTE: This methods seems to fail on certain S100 datasets generated by ECC but not
1757 // on datasets generated by Cesium. We should probably investigate why.
1758 const CameraData& cameraData = this->context->cameraSystem->getMainCameraData();
1759 int result = OGC3DTilesUtils::bboxInsideViewFrustum(box, cameraData.rawViewProjection);
1760 return result != 0; // Totally inside || partially inside
1761#endif
1762}
1763
1764bool
1765OGC3DTilesSystem::modelIsEmpty(const ModelHandle handle)
1766{
1767 const size_t numParts = handle->parts.size();
1768 for (size_t i = 0; i < numParts; ++i) {
1769 const ModelPart& part = handle->parts[i];
1770 if (part.vertexCount != static_cast<uint32_t>(-1)) { // Part not initialized?
1771 return false;
1772 }
1773 }
1774 return true;
1775}
1776
1777std::vector<OGC3DTiles::Coord>
1778OGC3DTilesSystem::getTileCoordChildren(OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset, const OGC3DTiles::Coord& parent) const
1779{
1780 assert(tileset->root.useImplicitTiling && "Only IMPLICIT trees uses subtrees");
1781 std::vector<OGC3DTiles::Coord> children;
1782
1783 OGC3DTilesSubtree::Subtree const* subtree = getSubtreeForTileCoord(componentState, tileset, parent);
1784 if (!subtree) {
1785 LOG_ERROR(logger, "getTileCoordChildren(): No subtree found. Internal error");
1786 return children;
1787 }
1788
1789 // Do we have to look up in this subtree's subtree?
1790 if (OGC3DTilesSubtree::isLeafNodeInSubtree(subtree, parent) || // Is a leaf-node
1791 ((parent.level + 1) >= tileset->root.implicitTiling.availableLevels)) { // We have no more levels
1792 std::vector<OGC3DTiles::Coord> subtreeCoords = OGC3DTilesSubtree::getLeafNodesSubtreeCoords(subtree, parent);
1793 children.reserve(subtreeCoords.size());
1794
1795 for (auto const& subtreeCoord : subtreeCoords) {
1796 // FIXME: Should we check the BBox of the subtree against the view frustum before scheduling a request?
1797 if (subtreeCoord.level > tileset->root.implicitTiling.availableLevels) {
1798 LOG_WARNING(logger, "Requested subtree-level too deep: %d vs %d", subtreeCoord.level, tileset->root.implicitTiling.availableLevels);
1799 break;
1800 }
1801
1802 OGC3DTilesSubtree::Subtree const* subSubtree = getSubtreeForTileCoord(componentState, tileset, subtreeCoord);
1803 if (subSubtree && (subtree->tileAvailability.availableCount > 0 || subtree->tileAvailability.allIsAvailable)) {
1804 children.emplace_back(subtreeCoord);
1805 }
1806 }
1807 }
1808 else {
1809 if (subtree->tileAvailability.availableCount == 0 && !subtree->tileAvailability.allIsAvailable) {
1810 // There is nothing to add
1811 }
1812 else if (subtree->tileAvailability.allIsAvailable) {
1813 // Add all possible children-coords for a node/coord depending on tree-type (Oct/Quad)
1814 children = OGC3DTilesSubtree::getAllChildren(subtree, parent);
1815 }
1816 else {
1817 // Use the tile-availability buffer bitmap to determine which children exists
1818 children = OGC3DTilesSubtree::getChildren(subtree, parent);
1819 }
1820 }
1821
1822 return children;
1823}
1824
1834{
1835 assert(tileset->root.useImplicitTiling && "Only IMPLICIT trees uses subtrees");
1836
1837 const int levelsPerSubtree = tileset->root.implicitTiling.subtreeLevels;
1838 const OGC3DTiles::Coord & subtreeCoord = OGC3DTilesSubtree::getSubtreesRootCoord(levelsPerSubtree, coord);
1839
1840 if (subtreeCoord.level > tileset->root.implicitTiling.availableLevels) {
1841 LOG_ERROR(logger, "Subtree coord-depth > tree-depth (%d vs %d)", subtreeCoord.level, tileset->root.implicitTiling.availableLevels);
1842 return nullptr;
1843 }
1844
1845 // Is subtree already in cache?
1846 const uint64_t key = OGC3DTilesSystem::makeCacheKey(subtreeCoord);
1847
1848 if (!componentState->states[tileset].subtreeCache.contains(subtreeCoord.level) ||
1849 !componentState->states[tileset].subtreeCache[subtreeCoord.level].contains(key)) {
1850 requestSubtree(componentState, tileset, subtreeCoord);
1851 return nullptr;
1852 }
1853
1854 return componentState->states[tileset].subtreeCache[subtreeCoord.level][key];
1855}
1856
1857// Only relevant for EXPLICIT trees
1858void
1859OGC3DTilesSystem::processSubTilesetRequests(const OGC3DTilesComponent* component, OGC3DTilesData* componentState)
1860{
1861 // Pre-collect a list of URLs/keys so that we can modify the 'requestedSubtree' map as we go.
1862 std::vector<std::string> urls;
1863 urls.reserve(componentState->subTilesetRequests.size());
1864 for (const auto& [URL, tile] : componentState->subTilesetRequests) {
1865 const uint64_t tilesetGUID = Cogs::hash(URL);
1866 if (!componentState->tilesetFetchIds.contains(tilesetGUID)) {
1867 urls.emplace_back(URL);
1868 }
1869 }
1870
1871 for (size_t i = 0; i < urls.size(); ++i) {
1872 const std::string url = addOptionalURLParameters(componentState, urls[i]);
1873 const OGC3DTilesTileset::Tileset* parentTileset = componentState->subTilesetRequests[url].second;
1874
1875 extractAndStoreOptionalSessionKeys(componentState, url);
1876
1877 const uint64_t tilesetGUID = Cogs::hash(url);
1878 componentState->tilesetFetchIds[tilesetGUID] = Cogs::Core::OGC3DTilesTileset::fetch(context, url,
1879 [thisp=this, component, url, parentTileset, tilesetGUID, uniqueComponentId=componentState->uniqueComponentId](OGC3DTilesTileset::Tileset* tileset) {
1880 if (OGC3DTilesSystem::componentIsStale(thisp->context, uniqueComponentId)) { // Has component been destructed in the meanwhile?
1881 return;
1882 }
1883
1884 OGC3DTilesDataHolder& h = thisp->getData(component);
1885 OGC3DTilesData* componentState = h.data.get();
1886
1887 if (tileset) {
1888 componentState->tilesets.push_back(tileset);
1889 componentState->states[tileset] = OGC3DTilesData::State();
1890 componentState->states[parentTileset].subTilesets[url] = tileset;
1891
1892 // Now we need to set the baseURL for the new tileset so that its URLs will be relative to
1893 // the root-tileset's baseURL.
1894 size_t offset = componentState->baseURL.size() + 1;
1895 tileset->baseURL = url.substr(offset, url.find_last_of("/") - offset);
1896
1897 if (tileset->root.useImplicitTiling) {
1898 tileset->root.implicitTiling.subTreeURLScheme = tileset->baseURL + "/" + tileset->root.implicitTiling.subTreeURLScheme;
1899 }
1900
1901 // Transform the sub-tileset with the master-tileset's transform matrix.
1902 tileset->root.transform = tileset->root.transform * parentTileset->root.transform;
1903
1904 // If the tileset is in sparse/implicit-mode, we'll always load and cache the root subtree-file.
1905 if (tileset->root.useImplicitTiling) {
1906 LOG_DEBUG(logger, " Sub-tileset tree-structure is of type '%s'", tileset->root.implicitTiling.isOctTree ? "OCT" : "QUAD");
1907 OGC3DTiles::Coord rootCoord = OGC3DTiles::Coord{ 0, 0, 0, tileset->root.implicitTiling.isOctTree ? 0 : -1 };
1908 thisp->requestSubtree(componentState, tileset, rootCoord);
1909 }
1910
1911 componentState->loadedSubTilesets.insert(url);
1912 }
1913 else {
1914 LOG_ERROR(logger, "Could not parse incoming Sub-tileset from '%s'", url.c_str());
1915 }
1916
1917 componentState->subTilesetRequests.erase(url);
1918 componentState->tilesetFetchIds.erase(tilesetGUID);
1919 });
1920 }
1921}
1922
1923// Only relevant for IMPLICIT trees
1924void
1925OGC3DTilesSystem::processSubtreeRequests(const OGC3DTilesComponent* component, OGC3DTilesData* componentState, const OGC3DTilesTileset::Tileset* tileset)
1926{
1927 assert(tileset->root.useImplicitTiling && "Only IMPLICIT trees uses subtrees");
1928
1929 // Pre-collect a list of URLs/keys so that we can modify the 'requestedSubtree' map as we go.
1930 std::vector<std::string> urls;
1931 urls.reserve(componentState->states[tileset].subtreeRequests.size());
1932 for (const auto& [subtreeURL, coord] : componentState->states[tileset].subtreeRequests) {
1933 const uint64_t subtreeGUID = Cogs::hash(subtreeURL);
1934 if (!componentState->states[tileset].subtreeFetchIds.contains(subtreeGUID)) {
1935 urls.emplace_back(subtreeURL);
1936 }
1937 }
1938
1939 for (size_t i=0; i<urls.size(); ++i) {
1940 const std::string subtreeURL = urls[i];
1941 const OGC3DTiles::Coord subtreeCoord = componentState->states[tileset].subtreeRequests[subtreeURL];
1942
1943 const uint64_t subtreeGUID = Cogs::hash(subtreeURL);
1944
1945 componentState->states[tileset].subtreeFetchIds[subtreeGUID] = OGC3DTilesSubtree::fetchSubtree(this->context, subtreeURL, componentState->uniqueComponentId,
1946 [thisp=this, component, subtreeCoord, subtreeURL, tileset, uniqueComponentId = componentState->uniqueComponentId, subtreeGUID](OGC3DTilesSubtree::Subtree* subtree) {
1947 if (OGC3DTilesSystem::componentIsStale(thisp->context, uniqueComponentId)) { // Has component been destructed in the meanwhile?
1948 return;
1949 }
1950
1951 OGC3DTilesDataHolder& h = thisp->getData(component);
1952 OGC3DTilesData* componentState = h.data.get();
1953
1954 assert(componentState->states.contains(tileset) && "The tileset is not a part of the component state!");
1955 OGC3DTilesData::State* state = &componentState->states[tileset];
1956
1957 if (subtree != nullptr) {
1958 subtree->globalCoord = subtreeCoord;
1959 subtree->levelsPerSubtree = tileset->root.implicitTiling.subtreeLevels;
1960 const uint64_t key = OGC3DTilesSystem::makeCacheKey(subtreeCoord);
1961
1962 if (state->subtreeCache[subtree->globalCoord.level].contains(key)) {
1963 LOG_WARNING(logger, "Subtree '%s' (%s) is already in the cache.", subtreeCoord.toString().c_str(), subtreeURL.c_str());
1964 return;
1965 }
1966 else {
1967 state->subtreeCache[subtree->globalCoord.level][key] = subtree;
1968 }
1969 }
1970 else {
1971 LOG_ERROR(logger, "Failed to load subtree '%s'", subtreeCoord.toString().c_str());
1972 // Remove the failed request from the register or else the max limit of pending requests will be
1973 // reached blocking future subtree-requests.
1974 state->subtreeRequests.erase(subtreeURL);
1975 state->subtreeFetchIds.erase(subtreeGUID);
1976 }
1977
1978 // The registered requests in the component's state will be cleared in "cleanupPendingSubtreeRequests()" in the main
1979 // thread when the tree has been properly traversed.
1980
1981 thisp->context->engine->setDirty();
1982 });
1983 }
1984}
1985
1991void
1993{
1994 if (componentState->states[tileset].subtreeRequests.size() > MAX_NUM_ACTIVE_SUBTREES_REQUESTS) {
1995 return;
1996 }
1997
1998 const std::string subtreeURL = OGC3DTilesSystem::buildSubtreeURL(componentState, tileset, coord);
1999 const std::string fullURL = this->addOptionalURLParameters(componentState, subtreeURL);
2000 const uint64_t key = OGC3DTilesSystem::makeCacheKey(coord);
2001
2002 if (!componentState->states[tileset].subtreeCache[coord.level].contains(key) && // Not loaded or available?
2003 !componentState->states[tileset].subtreeRequests.contains(fullURL)) { // Not already requested?
2004 componentState->states[tileset].subtreeRequests[fullURL] = coord;
2005 }
2006}
void setChanged()
Sets the component to the ComponentFlags::Changed state with carry.
Definition: Component.h:202
ComponentType * getComponent() const
Definition: Component.h:159
T * getComponent() const
Get a pointer to the first component implementing the given type in the entity.
Definition: Entity.h:35
virtual ComponentHandle createComponent()
Create a new component instance.
Context * context
Pointer to the Context instance the system lives in.
virtual void initialize(Context *context)
Initialize the system.
virtual void destroyComponent(ComponentHandle)
Destroy the component held by the given handle.
void update()
Updates the system state to that of the current frame.
Component system with parallel data per component stored in a pool similar to how the components them...
size_t size()
Returns the number of active components.
ComponentPool< ComponentType > pool
Pool of components managed by the system.
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
std::unique_ptr< class Bounds > bounds
Bounds service instance.
Definition: Context.h:216
std::unique_ptr< class QualityService > qualityService
Quality service instance.
Definition: Context.h:201
class EntityStore * store
Entity store.
Definition: Context.h:231
std::unique_ptr< class TaskManager > taskManager
TaskManager service instance.
Definition: Context.h:186
std::unique_ptr< class Variables > variables
Variables service instance.
Definition: Context.h:180
std::unique_ptr< class Time > time
Time service instance.
Definition: Context.h:198
std::unique_ptr< class Engine > engine
Engine instance.
Definition: Context.h:222
EntityPtr getEntity(const StringView &name, bool logIfNotFound=true) const
Retrieve a reference to the shared entity pointer to the Entity with the given name.
void addChild(ComponentModel::Entity *parent, const EntityPtr &entity)
Add a child to the given parent.
void destroyEntity(const EntityId id)
Destroy the entity with the given id.
EntityPtr createEntity(const StringView &name, const StringView &type, bool storeOwnership=true)
Create a new Entity.
void removeChild(ComponentModel::Entity *parent, const ComponentModel::Entity *entity)
Remove the parent-child relationship between parent and entity.
EntityPtr findEntity(const StringView &name, const ComponentModel::Entity *root=nullptr, EntityFind findOptions=EntityFind::Default) const
Finds an entity with the given name.
Contains a handle to a Mesh resource to use when rendering using the MeshRenderComponent.
Definition: MeshComponent.h:15
MeshHandle meshHandle
Handle to a Mesh resource to use when rendering.
Definition: MeshComponent.h:29
Node buildImplicitTree(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, const OGC3DTiles::Coord &coord, const glm::mat4 &globalTransform) const
bool requestModel(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, const TileCandidate &tile, const std::string URL) const
static uint64_t makeCacheKey(const OGC3DTiles::Coord &coord)
bool allChildrenAreReady(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, const Node *node) const
void collectTileCandidates(OGC3DTilesData *componentState, const Node *node, std::unordered_map< uint64_t, TileCandidate > &target) const
static bool componentIsStale(Context *context, uint32_t id)
void loadMissingModels(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, const std::unordered_map< uint64_t, TileCandidate > &toBeLoaded) const
void requestSubtree(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, const OGC3DTiles::Coord &coord) const
Node buildExplicitTree(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, const OGC3DTilesTileset::Tile *tile, const std::string &tileIdStr, const glm::mat4 &currentTransform, const glm::mat4 &globalTransform, std::unordered_set< const OGC3DTilesTileset::Tileset * > &visitedTilesets) const
OGC3DTilesSubtree::Subtree * getSubtreeForTileCoord(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, const OGC3DTiles::Coord &coord) const
bool tileHasReadyContent(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, uint64_t tileId) const
void cleanupPendingSubtreeRequests(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset) const
bool extractAndStoreOptionalSessionKeys(OGC3DTilesData *componentState, const std::string &url) const
size_t pruneTileCache(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, float gracePeriodBeforeTileRemoval) const
ComponentHandle createComponent() override
void calculateTileVisibilities(OGC3DTilesData *componentState, const OGC3DTilesTileset::Tileset *tileset, const Node *node) const
void addModelToScene(const std::string &uniqueName, TileCandidate tile, ModelHandle handle, const OGC3DTilesData *componentState) const
void initialize(Context *context) override
Initialize the system.
void destroyComponent(ComponentHandle component) override
Base component for all rendering content.
constexpr void setRenderFlag(RenderFlags flag)
Sets the given flag.
Contains information on how the entity behaves in the scene.
bool visible
If the entity this component is a member of should be visible.
Renders a mesh with flexible submesh usage.
static constexpr TaskQueueId GlobalQueue
Global task queue.
Definition: TaskManager.h:224
Defines a 4x4 transformation matrix for the entity and a global offset for root entities.
uint32_t transformFlags
Transform flags.
Log implementation class.
Definition: LogManager.h:140
Base allocator implementation.
Definition: Allocator.h:30
Provides a weakly referenced view over the contents of a string.
Definition: StringView.h:50
std::string to_string() const
String conversion method.
Definition: StringView.cpp:9
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
std::shared_ptr< ComponentModel::Entity > EntityPtr
Smart pointer for Entity access.
Definition: EntityPtr.h:12
@ CastShadows
Casts shadows.
@ TilesetInitializing
The tileset is being initialized.
uint16_t VariableKey
Used to lookup material properties.
Definition: Resources.h:46
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:181
constexpr size_t hash() noexcept
Simple getter function that returns the initial value for fnv1a hashing.
Definition: HashFunctions.h:62
@ LineStrip
Line strip.
uint32_t ComponentIndex
Type used to track component indexes in pools.
Definition: Component.h:16
Handle to a Component instance.
Definition: Component.h:67
COGSFOUNDATION_API class Component * resolve() const
Resolve the handle, returning a pointer to the held Component instance.
Definition: Component.cpp:65
Contains data describing a Camera instance and its derived data structured such as matrix data and vi...
Definition: CameraSystem.h:67
Exposes material properties for legacy entities and code.
bool shadowReceiver
If false, object never gets shadowed.
void setVec4Property(const VariableKey key, glm::vec4 value)
Set the vec4 property with the given key to value.
Material * material
Material resource this MaterialInstance is created from.
Meshes contain streams of vertex data in addition to index data and options defining geometry used fo...
Definition: Mesh.h:265
void setBounds(Geometry::BoundingBox box)
Set custom bounds for the mesh.
Definition: Mesh.h:298
void addSubMesh(std::span< uint32_t > collection, PrimitiveType primitiveType)
Add a sub-mesh to the Mesh.
Definition: Mesh.cpp:189
void setVertexData(Element *elements, size_t count)
Set vertex data.
Definition: Mesh.h:754
Contains a model reference to instance as children to the entity the ModelComponent belongs to.
bool inheritMaterial
If the model should inherit the material from the base entity.
MaterialInstanceHandle materialInstance
Explicit material handle.
ModelHandle model
Handle to a model resource that will be instanced onto the entity this ModelComponent belongs to.
glm::mat4 matrix
Complete transformation.