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