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