From d683e4d0d8fa52abb91d000966aa44c3bb160f20 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 20 Feb 2018 16:54:21 -0500 Subject: [PATCH 01/53] Change request queue from a heap to a sorted array --- Source/Core/RequestQueue.js | 186 ++++++++++++++++++++++++++++ Source/Core/RequestScheduler.js | 82 ++++++------- Specs/Core/HeapSpec.js | 174 -------------------------- Specs/Core/RequestQueueSpec.js | 189 +++++++++++++++++++++++++++++ Specs/Core/RequestSchedulerSpec.js | 38 +++--- 5 files changed, 435 insertions(+), 234 deletions(-) create mode 100644 Source/Core/RequestQueue.js delete mode 100644 Specs/Core/HeapSpec.js create mode 100644 Specs/Core/RequestQueueSpec.js diff --git a/Source/Core/RequestQueue.js b/Source/Core/RequestQueue.js new file mode 100644 index 000000000000..169483d3881c --- /dev/null +++ b/Source/Core/RequestQueue.js @@ -0,0 +1,186 @@ +define([ + './Check', + './defineProperties' + ], function( + Check, + defineProperties) { + 'use strict'; + + /** + * Priority queue for the {@link RequestScheduler} implemented as a sorted array. + * The request with the highest priority is placed at index 0 and the request + * with lowest priority is placed at index length - 1. + *

+ * A lower request.priority value indicates that the request has higher priority. See {@link Request#priority}. + *

+ * + * @alias RequestQueue + * @constructor + * @private + * + * @param {Number} maximumLength The maximum length of the queue. + */ + function RequestQueue(maximumLength) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('maximumLength', maximumLength); + //>>includeEnd('debug'); + + this._array = new Array(maximumLength); + this._length = 0; + this._maximumLength = maximumLength; + } + + defineProperties(RequestQueue.prototype, { + /** + * Gets the length of the queue. + * + * @memberof RequestQueue.prototype + * + * @type {Number} + * @readonly + */ + length : { + get : function() { + return this._length; + } + } + }); + + /** + * Get the request at the given index. + * + * @param {Number} index The index of the request. + * + * @return {Request} The request at the given index. + */ + RequestQueue.prototype.get = function(index) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.greaterThanOrEquals('index', index, 0); + Check.typeOf.number.lessThan('index', index, this._length); + //>>includeEnd('debug'); + return this._array[index]; + }; + + /** + * Insert a request into the queue. If the length would grow greater than the maximum length + * of the queue, the lowest priority request is removed and returned. + * + * @param {Request} request The request to insert. + * + * @return {Request|undefined} The request that was removed from the queue if the queue is at full capacity. + */ + RequestQueue.prototype.insert = function(request) { + //>>includeStart('debug', pragmas.debug); + Check.defined('request', request); + //>>includeEnd('debug'); + + var array = this._array; + var previousLength = this._length; + var length = this._length; + var maximumLength = this._maximumLength; + + if (length < maximumLength) + { + ++this._length; + } + + if (previousLength === 0) + { + array[0] = request; + return; + } + + var removedRequest; + var lastIndex = previousLength - 1; + + if (previousLength === maximumLength) { + var lastRequest = array[lastIndex]; + if (request.priority >= lastRequest.priority) { + // The array is full and the priority value of this request is too high to be inserted. + return request; + } + // The array is full and the inserted request pushes off the last request + removedRequest = lastRequest; + --lastIndex; + } + + while (lastIndex >= 0 && request.priority < array[lastIndex].priority) { + array[lastIndex + 1] = array[lastIndex]; // Shift element to the right + --lastIndex; + } + array[lastIndex + 1] = request; + + return removedRequest; + }; + + /** + * Call the given function for each request in the queue. + * + * @type {RequestQueue~ForEachCallback} The function to call. + */ + RequestQueue.prototype.forEach = function(callback) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.func('callback', callback); + //>>includeEnd('debug'); + + var array = this._array; + var length = this._length; + for (var i = 0; i < length; ++i) { + callback(array[i]); + } + }; + + /** + * Sorts the queue. + */ + RequestQueue.prototype.sort = function() { + var array = this._array; + var length = this._length; + + // Use insertion sort since our array is small and likely to be mostly sorted already. + // Additionally length may be smaller than the array's actual length, so calling array.sort will lead to incorrect results for uninitialized values. + for (var i = 1; i < length; ++i) { + var j = i; + while ((j > 0) && (array[j - 1].priority > array[j].priority)) { + var temp = array[j - 1]; + array[j - 1] = array[j]; + array[j] = temp; + --j; + } + } + }; + + /** + * Remove length number of requests from the top of the queue. + * + * @param {Number} length The number of requests to remove. + */ + RequestQueue.prototype.remove = function(length) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number.greaterThanOrEquals('length', length, 0); + Check.typeOf.number.lessThanOrEquals('length', length, this._length); + //>>includeEnd('debug'); + if (length === 0) { + return; + } + if (length === this._length) { + this._length = 0; + return; + } + + // Shift remaining requests back to the left + var array = this._array; + for (var i = length; i < this._length; ++i) { + array[i - length] = array[i]; + } + this._length -= length; + }; + + /** + * The callback to use in forEach. + * @callback RequestQueue~ForEachCallback + * @param {Request} request The request. + */ + + return RequestQueue; +}); diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index 10ccb51b9a0e..2f9320f899fa 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -5,9 +5,9 @@ define([ './defined', './defineProperties', './Event', - './Heap', './isBlobUri', './isDataUri', + './RequestQueue', './RequestState' ], function( Uri, @@ -16,16 +16,12 @@ define([ defined, defineProperties, Event, - Heap, isBlobUri, isDataUri, + RequestQueue, RequestState) { 'use strict'; - function sortRequests(a, b) { - return a.priority - b.priority; - } - var statistics = { numberOfAttemptedRequests : 0, numberOfActiveRequests : 0, @@ -35,12 +31,8 @@ define([ numberOfActiveRequestsEver : 0 }; - var priorityHeapLength = 20; - var requestHeap = new Heap({ - comparator : sortRequests - }); - requestHeap.maximumLength = priorityHeapLength; - requestHeap.reserve(priorityHeapLength); + var requestQueueLength = 20; + var requestQueue = new RequestQueue(requestQueueLength); var activeRequests = []; var numberOfActiveRequestsByServer = {}; @@ -112,29 +104,28 @@ define([ }, /** - * The maximum size of the priority heap. This limits the number of requests that are sorted by priority. Only applies to requests that are not yet active. + * The maximum length of the request queue. This limits the number of requests that are sorted by priority. Only applies to requests that are not yet active. * * @memberof RequestScheduler * * @type {Number} * @default 20 + * + * @private */ - priorityHeapLength : { + requestQueueLength : { get : function() { - return priorityHeapLength; + return requestQueueLength; }, set : function(value) { - // If the new length shrinks the heap, need to cancel some of the requests. - // Since this value is not intended to be tweaked regularly it is fine to just cancel the high priority requests. - if (value < priorityHeapLength) { - while (requestHeap.length > value) { - var request = requestHeap.pop(); - cancelRequest(request); - } + // Cancel all requests and resize the queue + var length = requestQueue.length; + for (var i = 0; i < length; ++i) { + var request = requestQueue.get(i); + cancelRequest(request); } - priorityHeapLength = value; - requestHeap.maximumLength = value; - requestHeap.reserve(value); + requestQueue = new RequestQueue(value); + RequestScheduler.requestQueue = requestQueue; } } }); @@ -242,21 +233,19 @@ define([ } activeRequests.length -= removeCount; - // Update priority of issued requests and resort the heap - var issuedRequests = requestHeap.internalArray; - var issuedLength = requestHeap.length; - for (i = 0; i < issuedLength; ++i) { - updatePriority(issuedRequests[i]); - } - requestHeap.resort(); + // Update priority of issued requests and resort the queue + requestQueue.forEach(updatePriority); + requestQueue.sort(); // Get the number of open slots and fill with the highest priority requests. // Un-throttled requests are automatically added to activeRequests, so activeRequests.length may exceed maximumRequests var openSlots = Math.max(RequestScheduler.maximumRequests - activeRequests.length, 0); var filledSlots = 0; - while (filledSlots < openSlots && requestHeap.length > 0) { - // Loop until all open slots are filled or the heap becomes empty - request = requestHeap.pop(); + var processedRequests = 0; + var totalRequests = requestQueue.length; + while (filledSlots < openSlots && processedRequests < totalRequests) { + // Loop until all open slots are filled or the queue becomes empty + request = requestQueue.get(processedRequests++); if (request.cancelled) { // Request was explicitly cancelled cancelRequest(request); @@ -272,6 +261,7 @@ define([ startRequest(request); ++filledSlots; } + requestQueue.remove(processedRequests); updateStatistics(); }; @@ -344,10 +334,9 @@ define([ return undefined; } - // Insert into the priority heap and see if a request was bumped off. If this request is the lowest - // priority it will be returned. + // Insert into the priority queue and see if a request was bumped off. If this request is the lowest priority it will be returned. updatePriority(request); - var removedRequest = requestHeap.insert(request); + var removedRequest = requestQueue.insert(request); if (defined(removedRequest)) { if (removedRequest === request) { @@ -397,12 +386,19 @@ define([ * @private */ RequestScheduler.clearForSpecs = function() { - while (requestHeap.length > 0) { - var request = requestHeap.pop(); + var request; + var length; + var i; + + length = requestQueue.length; + for (i = 0; i < length; ++i) { + request = requestQueue.get(i); cancelRequest(request); } - var length = activeRequests.length; - for (var i = 0; i < length; ++i) { + requestQueue.remove(length); + + length = activeRequests.length; + for (i = 0; i < length; ++i) { cancelRequest(activeRequests[i]); } activeRequests.length = 0; @@ -431,7 +427,7 @@ define([ * * @private */ - RequestScheduler.requestHeap = requestHeap; + RequestScheduler.requestQueue = requestQueue; return RequestScheduler; }); diff --git a/Specs/Core/HeapSpec.js b/Specs/Core/HeapSpec.js deleted file mode 100644 index 1b77e269652e..000000000000 --- a/Specs/Core/HeapSpec.js +++ /dev/null @@ -1,174 +0,0 @@ -defineSuite([ - 'Core/Heap' - ], function( - Heap) { - 'use strict'; - - var length = 100; - - function checkHeap(heap, comparator) { - var array = heap.internalArray; - var pass = true; - var length = heap.length; - for (var i = 0; i < length; ++i) { - var left = 2 * (i + 1) - 1; - var right = 2 * (i + 1); - if (left < heap.length) { - pass = pass && (comparator(array[i], array[left]) <= 0); - } - if (right < heap.length) { - pass = pass && (comparator(array[i], array[right]) <= 0); - } - } - return pass; - } - - // min heap - function comparator(a, b) { - return a - b; - } - - it('maintains heap property on insert', function() { - var heap = new Heap({ - comparator : comparator - }); - var pass = true; - for (var i = 0; i < length; ++i) { - heap.insert(Math.random()); - pass = pass && checkHeap(heap, comparator); - } - - expect(pass).toBe(true); - }); - - it('maintains heap property on pop', function() { - var heap = new Heap({ - comparator : comparator - }); - var i; - for (i = 0; i < length; ++i) { - heap.insert(Math.random()); - } - var pass = true; - for (i = 0; i < length; ++i) { - heap.pop(); - pass = pass && checkHeap(heap, comparator); - } - expect(pass).toBe(true); - }); - - it('limited by maximum length', function() { - var heap = new Heap({ - comparator : comparator - }); - heap.maximumLength = length / 2; - var pass = true; - for (var i = 0; i < length; ++i) { - heap.insert(Math.random()); - pass = pass && checkHeap(heap, comparator); - } - expect(pass).toBe(true); - expect(heap.length <= heap.maximumLength).toBe(true); - // allowed one extra slot for swapping - expect(heap.internalArray.length).toBeLessThanOrEqualTo(heap.maximumLength + 1); - }); - - it('pops in sorted order', function() { - var heap = new Heap({ - comparator : comparator - }); - var i; - for (i = 0; i < length; ++i) { - heap.insert(Math.random()); - } - var curr = heap.pop(); - var pass = true; - for (i = 0; i < length - 1; ++i) { - var next = heap.pop(); - pass = pass && (comparator(curr, next) <= 0); - curr = next; - } - expect(pass).toBe(true); - }); - - it('insert returns the removed element when maximumLength is set', function() { - var heap = new Heap({ - comparator : comparator - }); - heap.maximumLength = length; - - var i; - var max = 0.0; - var min = 1.0; - var values = new Array(length); - for (i = 0; i < length; ++i) { - var value = Math.random(); - max = Math.max(max, value); - min = Math.min(min, value); - values[i] = value; - } - - // Push 99 values - for (i = 0; i < length - 1; ++i) { - heap.insert(values[i]); - } - - // Push 100th, nothing is removed so it returns undefined - var removed = heap.insert(values[length - 1]); - expect(removed).toBeUndefined(); - - // Insert value, an element is removed - removed = heap.insert(max - 0.1); - expect(removed).toBeDefined(); - - // If this value is the least priority it will be returned - removed = heap.insert(max + 0.1); - expect(removed).toBe(max + 0.1); - }); - - it('resort', function() { - function comparator(a, b) { - return a.distance - b.distance; - } - - var i; - var heap = new Heap({ - comparator : comparator - }); - for (i = 0; i < length; ++i) { - heap.insert({ - distance : i / (length - 1), - id : i - }); - } - - // Check that elements are initially sorted - var element; - var elements = []; - var currentId = 0; - while (heap.length > 0) { - element = heap.pop(); - elements.push(element); - expect(element.id).toBeGreaterThanOrEqualTo(currentId); - currentId = element.id; - } - - // Add back into heap - for (i = 0; i < length; ++i) { - heap.insert(elements[i]); - } - - // Invert priority - for (i = 0; i < length; ++i) { - elements[i].distance = 1.0 - elements[i].distance; - } - - // Resort and check the the elements are popped in the opposite order now - heap.resort(); - while (heap.length > 0) { - element = heap.pop(); - expect(element.id).toBeLessThanOrEqualTo(currentId); - currentId = element.id; - } - }); -}); diff --git a/Specs/Core/RequestQueueSpec.js b/Specs/Core/RequestQueueSpec.js new file mode 100644 index 000000000000..a7bb51107e7e --- /dev/null +++ b/Specs/Core/RequestQueueSpec.js @@ -0,0 +1,189 @@ +defineSuite([ + 'Core/RequestQueue', + 'Core/Math', + 'Core/Request' + ], function( + RequestQueue, + CesiumMath, + Request) { + 'use strict'; + + var length = 20; + + function createRequest(distance) { + return new Request({ + priority : distance + }); + } + + function isSorted(queue) { + var distance = Number.NEGATIVE_INFINITY; + for (var i = 0; i < queue.length; ++i) { + var requestDistance = queue.get(i).priority; + if (requestDistance < distance) { + return false; + } + + distance = requestDistance; + } + return true; + } + + it('sets initial values', function() { + var queue = new RequestQueue(length); + expect(queue._array.length).toBe(length); + expect(queue._length).toBe(0); + expect(queue._maximumLength).toBe(length); + }); + + it('gets length', function() { + var queue = new RequestQueue(length); + expect(queue.length).toBe(0); + queue.insert(createRequest(1.0)); + expect(queue.length).toBe(1); + }); + + it('get', function() { + var queue = new RequestQueue(length); + queue.insert(createRequest(1)); + queue.insert(createRequest(0)); + expect(queue.get(0).priority).toBe(0); + expect(queue.get(1).priority).toBe(1); + }); + + it('insert', function() { + var removedRequest; + var request; + var i; + + CesiumMath.setRandomNumberSeed(0.0); + var distances = new Array(length); + for (i = 0; i < length; ++i) { + distances[i] = CesiumMath.nextRandomNumber(); + } + distances.sort(); + var lowestDistance = distances[0]; + var highestDistance = distances[distances.length - 1]; + + var queue = new RequestQueue(length); + for (i = 0; i < length; ++i) { + removedRequest = queue.insert(createRequest(distances[i])); + expect(removedRequest).toBeUndefined(); // Requests are not removed until the queue is full + } + + expect(isSorted(queue)).toBe(true); + + request = createRequest(highestDistance); + expect(queue.insert(request)).toBe(request); + + request = createRequest(highestDistance + 1.0); + expect(queue.insert(request)).toBe(request); + + request = createRequest(lowestDistance); + expect(queue.insert(request).priority).toBe(highestDistance); + expect(queue.get(0).priority).toBe(lowestDistance); + expect(queue.get(1).priority).toBe(lowestDistance); + expect(queue.get(2).priority).toBeGreaterThan(lowestDistance); + + expect(isSorted(queue)).toBe(true); + }); + + it('forEach', function() { + var total = 0; + var queue = new RequestQueue(length); + for (var i = 0; i < length; ++i) { + queue.insert(createRequest(1)); + } + queue.forEach(function(request) { + total += request.priority; + }); + expect(total).toBe(length); + }); + + it('sort', function() { + var i; + CesiumMath.setRandomNumberSeed(0.0); + var queue = new RequestQueue(length); + for (i = 0; i < length / 2; ++i) { + queue.insert(createRequest(CesiumMath.nextRandomNumber())); + } + queue.forEach(function(request) { + request.priority = CesiumMath.nextRandomNumber(); + }); + expect(isSorted(queue)).toBe(false); + queue.sort(); + expect(isSorted(queue)).toBe(true); + }); + + it('remove', function() { + var queue = new RequestQueue(length); + for (var i = 0; i < length; ++i) { + queue.insert(createRequest(i)); + } + queue.remove(0); + expect(queue.get(0).priority).toBe(0); + expect(queue.get(1).priority).toBe(1); + expect(queue.length).toBe(length); + + queue.remove(1); + expect(queue.get(0).priority).toBe(1); + expect(queue.get(1).priority).toBe(2); + expect(queue.length).toBe(length - 1); + + queue.remove(2); + expect(queue.get(0).priority).toBe(3); + expect(queue.get(1).priority).toBe(4); + expect(queue.length).toBe(length - 3); + + queue.remove(17); + expect(queue.length).toBe(0); + }); + + it('throws if maximumLength is undefined', function() { + expect(function() { + return new RequestQueue(); + }).toThrowDeveloperError(); + }); + + it('throws if get index is out of range', function() { + expect(function() { + var queue = new RequestQueue(length); + queue.get(0); + }).toThrowDeveloperError(); + + expect(function() { + var queue = new RequestQueue(length); + queue.insert(createRequest(0.0)); + queue.get(1); + }).toThrowDeveloperError(); + + expect(function() { + var queue = new RequestQueue(length); + queue.insert(createRequest(0.0)); + queue.get(-1); + }).toThrowDeveloperError(); + }); + + it('throws if forEach callback is not a function', function() { + expect(function() { + var queue = new RequestQueue(length); + queue.forEach(); + }).toThrowDeveloperError(); + expect(function() { + var queue = new RequestQueue(length); + queue.forEach(5); + }).toThrowDeveloperError(); + }); + + it('throws if remove length is out of range', function() { + expect(function() { + var queue = new RequestQueue(length); + queue.remove(1); + }).toThrowDeveloperError(); + + expect(function() { + var queue = new RequestQueue(length); + queue.remove(-1); + }).toThrowDeveloperError(); + }); +}); diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js index fbdf872694c6..92c9ef7aafec 100644 --- a/Specs/Core/RequestSchedulerSpec.js +++ b/Specs/Core/RequestSchedulerSpec.js @@ -12,12 +12,12 @@ defineSuite([ var originalMaximumRequests; var originalMaximumRequestsPerServer; - var originalPriorityHeapLength; + var originalRequestQueueLength; beforeAll(function() { originalMaximumRequests = RequestScheduler.maximumRequests; originalMaximumRequestsPerServer = RequestScheduler.maximumRequestsPerServer; - originalPriorityHeapLength = RequestScheduler.priorityHeapLength; + originalRequestQueueLength = RequestScheduler.requestQueueLength; }); beforeEach(function() { @@ -27,7 +27,7 @@ defineSuite([ afterEach(function() { RequestScheduler.maximumRequests = originalMaximumRequests; RequestScheduler.maximumRequestsPerServer = originalMaximumRequestsPerServer; - RequestScheduler.priorityHeapLength = originalPriorityHeapLength; + RequestScheduler.requestQueueLength = originalRequestQueueLength; }); it('request throws when request is undefined', function() { @@ -204,7 +204,7 @@ defineSuite([ } }); - it('honors priorityHeapLength', function() { + it('honors requestQueue length', function() { var deferreds = []; var requests = []; @@ -225,22 +225,23 @@ defineSuite([ return request; } - RequestScheduler.priorityHeapLength = 1; + RequestScheduler.requestQueueLength = 1; var firstRequest = createRequest(0.0); var promise = RequestScheduler.request(firstRequest); expect(promise).toBeDefined(); promise = RequestScheduler.request(createRequest(1.0)); expect(promise).toBeUndefined(); - RequestScheduler.priorityHeapLength = 3; + RequestScheduler.requestQueueLength = 3; + promise = RequestScheduler.request(createRequest(1.0)); promise = RequestScheduler.request(createRequest(2.0)); promise = RequestScheduler.request(createRequest(3.0)); expect(promise).toBeDefined(); promise = RequestScheduler.request(createRequest(4.0)); expect(promise).toBeUndefined(); - // A request is cancelled to accommodate the new heap length - RequestScheduler.priorityHeapLength = 2; + // The requests are cancelled when the queue length changes + RequestScheduler.requestQueueLength = 2; expect(firstRequest.state).toBe(RequestState.CANCELLED); var length = deferreds.length; @@ -448,7 +449,7 @@ defineSuite([ }); } - var length = RequestScheduler.priorityHeapLength; + var length = RequestScheduler.requestQueueLength; for (var i = 0; i < length; ++i) { var priority = Math.random(); RequestScheduler.request(createRequest(priority)); @@ -485,7 +486,7 @@ defineSuite([ var i; var request; - var length = RequestScheduler.priorityHeapLength; + var length = RequestScheduler.requestQueueLength; for (i = 0; i < length; ++i) { var priority = i / (length - 1); request = createRequest(priority); @@ -497,28 +498,31 @@ defineSuite([ RequestScheduler.maximumRequests = 0; RequestScheduler.update(); - var requestHeap = RequestScheduler.requestHeap; + var requestQueue = RequestScheduler.requestQueue; var requests = []; var currentTestId = 0; - while (requestHeap.length > 0) { - request = requestHeap.pop(); + + for (i = 0; i < length; ++i) { + request = requestQueue.get(i); requests.push(request); expect(request.testId).toBeGreaterThanOrEqualTo(currentTestId); currentTestId = request.testId; } + requestQueue.remove(length); for (i = 0; i < length; ++i) { - requestHeap.insert(requests[i]); + requestQueue.insert(requests[i]); } invertPriority = true; RequestScheduler.update(); - while (requestHeap.length > 0) { - request = requestHeap.pop(); + for (i = 0; i < length; ++i) { + request = requestQueue.get(i); expect(request.testId).toBeLessThanOrEqualTo(currentTestId); currentTestId = request.testId; } + requestQueue.remove(length); }); it('handles low priority requests', function() { @@ -539,7 +543,7 @@ defineSuite([ var mediumPriority = 0.5; var lowPriority = 1.0; - var length = RequestScheduler.priorityHeapLength; + var length = RequestScheduler.requestQueueLength; for (var i = 0; i < length; ++i) { RequestScheduler.request(createRequest(mediumPriority)); } From 366a1d2af1c7fbce32c4b5a8ef280682b5f1a894 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 21 Feb 2018 13:57:58 -0500 Subject: [PATCH 02/53] Set highest priority for globe tiles that you are inside --- Source/Scene/GlobeSurfaceTile.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index 1026832675b8..e9b9e76d1216 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -7,6 +7,7 @@ define([ '../Core/defineProperties', '../Core/IntersectionTests', '../Core/PixelFormat', + '../Core/Rectangle', '../Renderer/PixelDatatype', '../Renderer/Sampler', '../Renderer/Texture', @@ -28,6 +29,7 @@ define([ defineProperties, IntersectionTests, PixelFormat, + Rectangle, PixelDatatype, Sampler, Texture, @@ -254,6 +256,10 @@ define([ function createPriorityFunction(surfaceTile, frameState) { return function() { + if (Rectangle.contains(surfaceTile.tileBoundingRegion.rectangle, frameState.camera.positionCartographic)) { + // If the camera is inside this tile's bounding region treat it as highest priority + return 0.0; + } return surfaceTile.tileBoundingRegion.distanceToCamera(frameState); }; } From 3f09e0cef6094bc9cc1b73b9dc289c29e078cf5c Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 22 Feb 2018 14:20:37 -0500 Subject: [PATCH 03/53] Move tileset cache to its own class --- Source/Scene/Cesium3DTile.js | 8 +-- Source/Scene/Cesium3DTileset.js | 66 +++++--------------- Source/Scene/Cesium3DTilesetCache.js | 79 ++++++++++++++++++++++++ Source/Scene/Cesium3DTilesetTraversal.js | 15 ++--- Specs/Scene/Cesium3DTilesetSpec.js | 8 +-- 5 files changed, 108 insertions(+), 68 deletions(-) create mode 100644 Source/Scene/Cesium3DTilesetCache.js diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 18101211d979..57e588774403 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -250,14 +250,16 @@ define([ this.hasTilesetContent = false; /** - * The corresponding node in the cache replacement list. + * The corresponding node in the cache. + * + * See {@link Cesium3DTilesetCache} * * @type {DoublyLinkedListNode} * @readonly * * @private */ - this.replacementNode = undefined; + this.cacheNode = undefined; var expire = header.expire; var expireDuration; @@ -732,8 +734,6 @@ define([ this._contentReadyToProcessPromise = undefined; this._contentReadyPromise = undefined; - this.replacementNode = undefined; - this.lastStyleTime = 0; this._debugColorizeTiles = false; diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index e4590a8d60e1..25ef90c9dff2 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -29,6 +29,7 @@ define([ './Cesium3DTile', './Cesium3DTileColorBlendMode', './Cesium3DTileOptimizations', + './Cesium3DTilesetCache', './Cesium3DTilesetStatistics', './Cesium3DTilesetTraversal', './Cesium3DTileStyleEngine', @@ -73,6 +74,7 @@ define([ Cesium3DTile, Cesium3DTileColorBlendMode, Cesium3DTileOptimizations, + Cesium3DTilesetCache, Cesium3DTilesetStatistics, Cesium3DTilesetTraversal, Cesium3DTileStyleEngine, @@ -175,6 +177,7 @@ define([ this._properties = undefined; // Metadata for per-model/point/etc properties this._geometricError = undefined; // Geometric error when the tree is not rendered at all this._gltfUpAxis = undefined; + this._cache = new Cesium3DTilesetCache(); this._processingQueue = []; this._selectedTiles = []; this._requestedTiles = []; @@ -183,14 +186,6 @@ define([ this._loadTimestamp = undefined; this._timeSinceLoad = 0.0; - var replacementList = new DoublyLinkedList(); - - // [head, sentinel) -> tiles that weren't selected this frame and may be replaced - // (sentinel, tail] -> tiles that were selected this frame - this._replacementList = replacementList; // Tiles with content loaded. For cache management. - this._replacementSentinel = replacementList.add(); - this._trimTiles = false; - this._cullWithChildrenBounds = defaultValue(options.cullWithChildrenBounds, true); this._hasMixedContent = false; @@ -1502,10 +1497,7 @@ define([ tileset._statistics.incrementLoadCounts(tile.content); ++tileset._statistics.numberOfTilesWithContentReady; - // Add to the tile cache. Previously expired tiles are already in the cache. - if (!defined(tile.replacementNode)) { - tile.replacementNode = tileset._replacementList.add(tile); - } + tileset._cache.add(tile); } }; } @@ -1745,52 +1737,27 @@ define([ stack.push(children[i]); } if (tile !== root) { - unloadTileFromCache(tileset, tile); - tile.destroy(); + destroyTile(tileset, tile); --statistics.numberOfTilesTotal; } } root.children = []; } - function unloadTileFromCache(tileset, tile) { - var node = tile.replacementNode; - if (!defined(node)) { - return; - } - - var statistics = tileset._statistics; - var replacementList = tileset._replacementList; - var tileUnload = tileset.tileUnload; + function unloadTile(tileset, tile) { + tileset.tileUnload.raiseEvent(tile); + tileset._statistics.decrementLoadCounts(tile.content); + --tileset._statistics.numberOfTilesWithContentReady; + tile.unloadContent(); + } - tileUnload.raiseEvent(tile); - replacementList.remove(node); - statistics.decrementLoadCounts(tile.content); - --statistics.numberOfTilesWithContentReady; + function destroyTile(tileset, tile) { + tileset._cache.unloadTile(tileset, tile, unloadTile); + tile.destroy(); } function unloadTiles(tileset) { - var trimTiles = tileset._trimTiles; - tileset._trimTiles = false; - - var replacementList = tileset._replacementList; - - var totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; - var maximumMemoryUsageInBytes = tileset._maximumMemoryUsage * 1024 * 1024; - - // Traverse the list only to the sentinel since tiles/nodes to the - // right of the sentinel were used this frame. - // - // The sub-list to the left of the sentinel is ordered from LRU to MRU. - var sentinel = tileset._replacementSentinel; - var node = replacementList.head; - while ((node !== sentinel) && ((totalMemoryUsageInBytes > maximumMemoryUsageInBytes) || trimTiles)) { - var tile = node.item; - node = node.next; - unloadTileFromCache(tileset, tile); - tile.unloadContent(); - totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; - } + tileset._cache.unloadTiles(tileset, unloadTile); } /** @@ -1803,8 +1770,7 @@ define([ *

*/ Cesium3DTileset.prototype.trimLoadedTiles = function() { - // Defer to next frame so WebGL delete calls happen inside the render loop - this._trimTiles = true; + this._cache.trim(); }; /////////////////////////////////////////////////////////////////////////// diff --git a/Source/Scene/Cesium3DTilesetCache.js b/Source/Scene/Cesium3DTilesetCache.js new file mode 100644 index 000000000000..848a41da2529 --- /dev/null +++ b/Source/Scene/Cesium3DTilesetCache.js @@ -0,0 +1,79 @@ +define([ + '../Core/defined', + '../Core/DoublyLinkedList' + ], function( + defined, + DoublyLinkedList) { + 'use strict'; + + /** + * Stores tiles with content loaded. + * + * @private + */ + function Cesium3DTilesetCache() { + // [head, sentinel) -> tiles that weren't selected this frame and may be removed from the cache + // (sentinel, tail] -> tiles that were selected this frame + this._list = new DoublyLinkedList(); + this._sentinel = this._list.add(); + this._trimTiles = false; + } + + Cesium3DTilesetCache.prototype.reset = function() { + // Move sentinel node to the tail so, at the start of the frame, all tiles + // may be potentially replaced. Tiles are moved to the right of the sentinel + // when they are selected so they will not be replaced. + this._list.splice(this._list.tail, this._sentinel); + }; + + Cesium3DTilesetCache.prototype.touch = function(tile) { + var node = tile.cacheNode; + if (defined(node)) { + this._list.splice(this._sentinel, node); + } + }; + + Cesium3DTilesetCache.prototype.add = function(tile) { + if (!defined(tile.cacheNode)) { + tile.cacheNode = this._list.add(tile); + } + }; + + Cesium3DTilesetCache.prototype.unloadTile = function(tileset, tile, unloadCallback) { + var node = tile.cacheNode; + if (!defined(node)) { + return; + } + + this._list.remove(node); + tile.cacheNode = undefined; + unloadCallback(tileset, tile); + }; + + Cesium3DTilesetCache.prototype.unloadTiles = function(tileset, unloadCallback) { + var trimTiles = this._trimTiles; + this._trimTiles = false; + + var list = this._list; + + var maximumMemoryUsageInBytes = tileset.maximumMemoryUsage * 1024 * 1024; + + // Traverse the list only to the sentinel since tiles/nodes to the + // right of the sentinel were used this frame. + // + // The sub-list to the left of the sentinel is ordered from LRU to MRU. + var sentinel = this._sentinel; + var node = list.head; + while ((node !== sentinel) && ((tileset.totalMemoryUsageInBytes > maximumMemoryUsageInBytes) || trimTiles)) { + var tile = node.item; + node = node.next; + this.unloadTile(tileset, tile, unloadCallback); + } + }; + + Cesium3DTilesetCache.prototype.trim = function() { + this._trimTiles = true; + }; + + return Cesium3DTilesetCache; +}); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 3052b00ddb79..3acb1a2a66a3 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -25,7 +25,9 @@ define([ /** * @private */ - var Cesium3DTilesetTraversal = {}; + function Cesium3DTilesetTraversal() { + } + function selectTiles(tileset, frameState, outOfCore) { if (tileset.debugFreezeFrame) { @@ -40,11 +42,7 @@ define([ tileset._selectedTilesToStyle.length = 0; tileset._hasMixedContent = false; - // Move sentinel node to the tail so, at the start of the frame, all tiles - // may be potentially replaced. Tiles are moved to the right of the sentinel - // when they are selected so they will not be replaced. - var replacementList = tileset._replacementList; - replacementList.splice(replacementList.tail, tileset._replacementSentinel); + tileset._cache.reset(); var root = tileset._root; root.updateTransform(tileset._modelMatrix); @@ -625,10 +623,7 @@ define([ if (!outOfCore) { return; } - var node = tile.replacementNode; - if (defined(node)) { - tileset._replacementList.splice(tileset._replacementSentinel, node); - } + tileset._cache.touch(tile); } function computeSSE(tile, frameState) { diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index ce9f61a122cb..1e0330376b61 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -2299,14 +2299,14 @@ defineSuite([ it('Unloads cached tiles in a tileset with external tileset.json using maximumMemoryUsage', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { var statistics = tileset._statistics; - var replacementList = tileset._replacementList; + var cacheList = tileset._cache._list; tileset.maximumMemoryUsage = 0.025; scene.renderForSpecs(); expect(statistics.numberOfCommands).toEqual(5); expect(statistics.numberOfTilesWithContentReady).toEqual(5); - expect(replacementList.length - 1).toEqual(5); // Only tiles with content are on the replacement list. -1 for sentinel. + expect(cacheList.length - 1).toEqual(5); // Only tiles with content are on the replacement list. -1 for sentinel. // Zoom out so only root tile is needed to meet SSE. This unloads // all tiles except the root and one of the b3dm children @@ -2315,7 +2315,7 @@ defineSuite([ expect(statistics.numberOfCommands).toEqual(1); expect(statistics.numberOfTilesWithContentReady).toEqual(2); - expect(replacementList.length - 1).toEqual(2); + expect(cacheList.length - 1).toEqual(2); // Reset camera so all tiles are reloaded viewAllTiles(); @@ -2324,7 +2324,7 @@ defineSuite([ expect(statistics.numberOfCommands).toEqual(5); expect(statistics.numberOfTilesWithContentReady).toEqual(5); - expect(replacementList.length - 1).toEqual(5); + expect(cacheList.length - 1).toEqual(5); }); }); }); From 15700e49f2d03fac664a4e74096e2954e3261a59 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 27 Feb 2018 15:18:26 -0500 Subject: [PATCH 04/53] Temp --- Source/Scene/Cesium3DTile.js | 40 +- Source/Scene/Cesium3DTileBatchTable.js | 14 +- Source/Scene/Cesium3DTileStyleEngine.js | 2 +- Source/Scene/Cesium3DTileset.js | 81 +- Source/Scene/Cesium3DTilesetTraversal.js | 997 +++++++++-------------- Specs/Scene/Cesium3DTilesetSpec.js | 2 +- 6 files changed, 451 insertions(+), 685 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 57e588774403..84cb889323ac 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -252,8 +252,6 @@ define([ /** * The corresponding node in the cache. * - * See {@link Cesium3DTilesetCache} - * * @type {DoublyLinkedListNode} * @readonly * @@ -285,15 +283,6 @@ define([ */ this.expireDate = expireDate; - /** - * Marks if the tile is selected this frame. - * - * @type {Boolean} - * - * @private - */ - this.selected = false; - /** * The time when a style was last applied to this tile. * @@ -324,22 +313,21 @@ define([ // Members that are updated every frame for tree traversal and rendering optimizations: this._distanceToCamera = 0; - this._visibilityPlaneMask = 0; - this._childrenVisibility = Cesium3DTileChildrenVisibility.VISIBLE; - this._lastSelectedFrameNumber = -1; + this._centerZDepth = 0; // TODO : remove? this._screenSpaceError = 0; - this._screenSpaceErrorComputedFrame = -1; + this._visibilityPlaneMask = 0; + this._finalResolution = true; this._depth = 0; - this._centerZDepth = 0; this._stackLength = 0; - this._selectedFrame = -1; this._selectionDepth = 0; - this._lastSelectionDepth = undefined; - this._requestedFrame = undefined; - this._lastVisitedFrame = undefined; - this._ancestorWithContent = undefined; - this._ancestorWithLoadedContent = undefined; + + this._updatedVisibilityFrame = 0; + this._touchedFrame = 0; + this._selectedFrame = 0; + this._ancestorWithContentAvailable = undefined; + this._refines = false; + this._priority = 0.0; this._isClipped = true; this._clippingPlanesState = 0; // encapsulates (_isClipped, clippingPlanes.enabled) and number/function @@ -457,13 +445,13 @@ define([ */ contentAvailable : { get : function() { - return this.contentReady || (defined(this._expiredContent) && this._contentState !== Cesium3DTileContentState.FAILED); + return (this.contentReady && this.hasRenderableContent) || (defined(this._expiredContent) && this._contentState !== Cesium3DTileContentState.FAILED); } }, /** - * Determines if the tile is ready to render. true if the tile - * is ready to render; otherwise, false. + * Determines if the tile's content is ready. This is automatically true for + * tile's with empty content. * * @memberof Cesium3DTile.prototype * @@ -608,7 +596,7 @@ define([ function createPriorityFunction(tile) { return function() { - return tile._distanceToCamera; + return tile._priority; }; } diff --git a/Source/Scene/Cesium3DTileBatchTable.js b/Source/Scene/Cesium3DTileBatchTable.js index 42c32407f0c9..87982d59062c 100644 --- a/Source/Scene/Cesium3DTileBatchTable.js +++ b/Source/Scene/Cesium3DTileBatchTable.js @@ -1355,9 +1355,15 @@ define([ } tileset._backfaceCommands.push(derivedCommands.zback); } - if (!defined(derivedCommands.stencil) || tile._selectionDepth !== tile._lastSelectionDepth) { + if (!defined(derivedCommands.stencil) || tile._selectionDepth !== getLastSelectionDepth(derivedCommands.stencil)) { + // if (defined(derivedCommands.stencil)) { + // var currentSelectionDepth = tile._selectionDepth; + // var newSelectionDepth = getLastSelectionDepth(derivedCommands.stencil); + // console.log('My selection depth: ' + tile._selectionDepth); + // console.log('Previous selection depth: ' + (defined(derivedCommands.stencil) ? getLastSelectionDepth(derivedCommands.stencil) : 'None')); + // + // } derivedCommands.stencil = deriveStencilCommand(derivedCommands.originalCommand, tile._selectionDepth); - tile._lastSelectionDepth = tile._selectionDepth; } updateDerivedCommand(derivedCommands.stencil, command); } @@ -1482,6 +1488,10 @@ define([ return derivedCommand; } + function getLastSelectionDepth(stencilCommand) { + return stencilCommand.renderState.stencilTest.reference >>> 4; + } + function getTranslucentRenderState(renderState) { var rs = clone(renderState, true); rs.cull.enabled = false; diff --git a/Source/Scene/Cesium3DTileStyleEngine.js b/Source/Scene/Cesium3DTileStyleEngine.js index 07ac449b0e52..e9fa8ee4af4f 100644 --- a/Source/Scene/Cesium3DTileStyleEngine.js +++ b/Source/Scene/Cesium3DTileStyleEngine.js @@ -65,7 +65,7 @@ define([ var length = tiles.length; for (var i = 0; i < length; ++i) { var tile = tiles[i]; - if (tile.selected && (tile.lastStyleTime !== lastStyleTime)) { + if (tile.lastStyleTime !== lastStyleTime) { // Apply the style to this tile if it wasn't already applied because: // 1) the user assigned a new style to the tileset // 2) this tile is now visible, but it wasn't visible when the style was first assigned diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 25ef90c9dff2..310f9ca94cc8 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -29,6 +29,7 @@ define([ './Cesium3DTile', './Cesium3DTileColorBlendMode', './Cesium3DTileOptimizations', + './Cesium3DTileRefine', './Cesium3DTilesetCache', './Cesium3DTilesetStatistics', './Cesium3DTilesetTraversal', @@ -74,6 +75,7 @@ define([ Cesium3DTile, Cesium3DTileColorBlendMode, Cesium3DTileOptimizations, + Cesium3DTileRefine, Cesium3DTilesetCache, Cesium3DTilesetStatistics, Cesium3DTilesetTraversal, @@ -187,14 +189,10 @@ define([ this._timeSinceLoad = 0.0; this._cullWithChildrenBounds = defaultValue(options.cullWithChildrenBounds, true); + this._allTilesAdditive = true; this._hasMixedContent = false; - this._baseTraversal = new Cesium3DTilesetTraversal.BaseTraversal(); - this._skipTraversal = new Cesium3DTilesetTraversal.SkipTraversal({ - selectionHeuristic : selectionHeuristic - }); - this._backfaceCommands = new ManagedArray(); this._maximumScreenSpaceError = defaultValue(options.maximumScreenSpaceError, 16); @@ -1269,14 +1267,12 @@ define([ // Append the tileset version to the tilesetResource if (!defined(tilesetResource.queryParameters.v)) { var versionQuery = { - v: defaultValue(asset.tilesetVersion, '0.0') + v : defaultValue(asset.tilesetVersion, '0.0') }; this._basePath += '?v=' + versionQuery.v; tilesetResource.setQueryParameters(versionQuery); } - // A tileset.json referenced from a tile may exist in a different directory than the root tileset. - // Get the basePath relative to the external tileset. var rootTile = new Cesium3DTile(this, tilesetResource, tilesetJson.root, parentTile); // If there is a parentTile, add the root of the currently loading tileset @@ -1286,35 +1282,27 @@ define([ rootTile._depth = parentTile._depth + 1; } - ++statistics.numberOfTilesTotal; - var stack = []; - stack.push({ - header : tilesetJson.root, - tile3D : rootTile - }); + stack.push(rootTile); while (stack.length > 0) { var tile = stack.pop(); - var tile3D = tile.tile3D; - var children = tile.header.children; + ++statistics.numberOfTilesTotal; + this._allTilesAdditive = this._allTilesAdditive && (tile.refine === Cesium3DTileRefine.ADD); + var children = tile._header.children; if (defined(children)) { var length = children.length; for (var i = 0; i < length; ++i) { var childHeader = children[i]; - var childTile = new Cesium3DTile(this, tilesetResource, childHeader, tile3D); - tile3D.children.push(childTile); - childTile._depth = tile3D._depth + 1; - ++statistics.numberOfTilesTotal; - stack.push({ - header : childHeader, - tile3D : childTile - }); + var childTile = new Cesium3DTile(this, tilesetResource, childHeader, tile); + tile.children.push(childTile); + childTile._depth = tile._depth + 1; + stack.push(childTile); } } if (this._cullWithChildrenBounds) { - Cesium3DTileOptimizations.checkChildrenWithinParent(tile3D); + Cesium3DTileOptimizations.checkChildrenWithinParent(tile); } } @@ -1400,15 +1388,6 @@ define([ tileset._dynamicScreenSpaceErrorComputedDensity = density; } - function selectionHeuristic(tileset, ancestor, tile) { - var skipLevels = tileset._skipLevelOfDetail ? tileset.skipLevels : 0; - var skipScreenSpaceErrorFactor = tileset._skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; - - return (ancestor !== tile && !tile.hasEmptyContent && !tileset.immediatelyLoadDesiredLevelOfDetail) && - (tile._screenSpaceError < ancestor._screenSpaceError / skipScreenSpaceErrorFactor) && - (tile._depth > ancestor._depth + skipLevels); - } - /////////////////////////////////////////////////////////////////////////// function requestContent(tileset, tile) { @@ -1457,10 +1436,7 @@ define([ }); } - function requestTiles(tileset, outOfCore) { - if (!outOfCore) { - return; - } + function requestTiles(tileset) { var requestedTiles = tileset._requestedTiles; var length = requestedTiles.length; for (var i = 0; i < length; ++i) { @@ -1635,6 +1611,8 @@ define([ var tileVisible = tileset.tileVisible; var i; + console.log(tileset._hasMixedContent); + var bivariateVisibilityTest = tileset._skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer && length > 0; tileset._backfaceCommands.length = 0; @@ -1646,15 +1624,12 @@ define([ var lengthBeforeUpdate = commandList.length; for (i = 0; i < length; ++i) { var tile = selectedTiles[i]; - // tiles may get unloaded and destroyed between selection and update - if (tile.selected) { - // Raise the tileVisible event before update in case the tileVisible event - // handler makes changes that update needs to apply to WebGL resources - tileVisible.raiseEvent(tile); - tile.update(tileset, frameState); - statistics.incrementSelectionCounts(tile.content); - ++statistics.selected; - } + // Raise the tileVisible event before update in case the tileVisible event + // handler makes changes that update needs to apply to WebGL resources + tileVisible.raiseEvent(tile); + tile.update(tileset, frameState); + statistics.incrementSelectionCounts(tile.content); + ++statistics.selected; } var lengthAfterUpdate = commandList.length; var addedCommandsLength = lengthAfterUpdate - lengthBeforeUpdate; @@ -1842,16 +1817,16 @@ define([ var statistics = this._statistics; statistics.clear(); - if (outOfCore) { - processTiles(this, frameState); - } - if (this.dynamicScreenSpaceError) { updateDynamicScreenSpaceError(this, frameState); } - Cesium3DTilesetTraversal.selectTiles(this, frameState, outOfCore); - requestTiles(this, outOfCore); + if (outOfCore) { + Cesium3DTilesetTraversal.selectTiles(this, frameState); + requestTiles(this); + processTiles(this, frameState); + } + updateTiles(this, frameState); if (outOfCore) { diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 3acb1a2a66a3..d33746b9e23c 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -1,5 +1,6 @@ define([ '../Core/CullingVolume', + '../Core/defaultValue', '../Core/defined', '../Core/freezeObject', '../Core/Intersect', @@ -7,10 +8,12 @@ define([ '../Core/Math', '../Core/OrthographicFrustum', './Cesium3DTileChildrenVisibility', + './Cesium3DTileOptimizationHint', './Cesium3DTileRefine', './SceneMode' ], function( CullingVolume, + defaultValue, defined, freezeObject, Intersect, @@ -18,24 +21,44 @@ define([ CesiumMath, OrthographicFrustum, Cesium3DTileChildrenVisibility, + Cesium3DTileOptimizationHint, Cesium3DTileRefine, SceneMode) { 'use strict'; + var baseTraversal = { + stack : new ManagedArray(), + stackMaximumLength : 0 + }; + + var internalBaseTraversal = { + stack : new ManagedArray(), + stackMaximumLength : 0 + }; + + var skipTraversal = { + stack : new ManagedArray(), + stackMaximumLength : 0 + }; + + var selectionTraversal = { + stack : new ManagedArray(), + stackMaximumLength : 0, + ancestorStack : new ManagedArray(), + ancestorStackMaximumLength : 0 + }; + /** * @private */ function Cesium3DTilesetTraversal() { } - - function selectTiles(tileset, frameState, outOfCore) { + Cesium3DTilesetTraversal.selectTiles = function(tileset, frameState) { if (tileset.debugFreezeFrame) { return; } - var maximumScreenSpaceError = tileset._maximumScreenSpaceError; - tileset._desiredTiles.length = 0; tileset._selectedTiles.length = 0; tileset._requestedTiles.length = 0; @@ -45,112 +68,129 @@ define([ tileset._cache.reset(); var root = tileset._root; - root.updateTransform(tileset._modelMatrix); - - if (!root.insideViewerRequestVolume(frameState)) { - return; - } - - root._distanceToCamera = root.distanceToTile(frameState); - - if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= maximumScreenSpaceError) { - // The SSE of not rendering the tree is small enough that the tree does not need to be rendered + var rootVisible = updateVisibility(tileset, root, frameState); + if (!rootVisible) { return; } - root._visibilityPlaneMask = root.visibility(frameState, CullingVolume.MASK_INDETERMINATE); - if (root._visibilityPlaneMask === CullingVolume.MASK_OUTSIDE) { + // The SSE of not rendering the tree is small enough that the tree does not need to be rendered + if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= tileset._maximumScreenSpaceError) { return; } - loadTile(tileset, root, frameState, true); - - if (!tileset._skipLevelOfDetail) { - // just execute base traversal and add tiles to _desiredTiles - tileset._baseTraversal.execute(tileset, root, maximumScreenSpaceError, frameState, outOfCore); - var leaves = tileset._baseTraversal.leaves; - var length = leaves.length; - for (var i = 0; i < length; ++i) { - tileset._desiredTiles.push(leaves.get(i)); - } + if (!tileset._skipLevelOfDetail) {//} || tileset._allTilesAdditive) { + selectBaseTraversal(tileset, root, frameState); } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { - tileset._skipTraversal.execute(tileset, root, frameState, outOfCore); + selectSkipTraversal(tileset, root, frameState); } else { - // leaves of the base traversal is where we start the skip traversal - tileset._baseTraversal.leaves = tileset._skipTraversal.queue1; + selectBaseAndSkipTraversal(tileset, root, frameState); + } + + // TODO : these aren't really correct because min/max never get reset. + tileset._desiredTiles.trim(); + baseTraversal.stack.trim(baseTraversal.stackMaximumLength); + internalBaseTraversal.stack.trim(internalBaseTraversal.stackMaximumLength); + skipTraversal.stack.trim(skipTraversal.stackMaximumLength); + selectionTraversal.stack.trim(selectionTraversal.stackMaximumLength); + selectionTraversal.ancestorStack.trim(selectionTraversal.ancestorStackMaximumLength); + }; - // load and select tiles without skipping up to tileset.baseScreenSpaceError - tileset._baseTraversal.execute(tileset, root, tileset.baseScreenSpaceError, frameState, outOfCore); + function selectBaseTraversal(tileset, root, frameState) { + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + executeBaseTraversal(tileset, root, maximumScreenSpaceError, maximumScreenSpaceError, undefined, frameState); - // skip traversal starts from a prepopulated queue from the base traversal - tileset._skipTraversal.execute(tileset, undefined, frameState, outOfCore); + // Select desired tiles + var desiredTiles = tileset._desiredTiles; + var length = desiredTiles.length; + for (var i = 0; i < length; ++i) { + selectTile(tileset, desiredTiles.get(i), frameState); } + } - // mark tiles for selection or their nearest loaded ancestor - markLoadedTilesForSelection(tileset, frameState, outOfCore); + function selectSkipTraversal(tileset, root, frameState) { + // Start the skip traversal at the root + skipTraversal.stack.push(root); - // sort selected tiles by distance to camera and call selectTile on each - // set tile._selectionDepth on all tiles - traverseAndSelect(tileset, root, frameState); + // Execute the skip traversal + executeSkipTraversal(tileset, tileset._maximumScreenSpaceError, frameState); - tileset._desiredTiles.trim(); + // Mark tiles for selection or their nearest loaded ancestor + markDesiredTilesForSelection(tileset, frameState); + + // Sort selected tiles by distance to camera and call selectTile on each + traverseAndSelect(tileset, root, frameState); } - var descendantStack = []; + function selectBaseAndSkipTraversal(tileset, root, frameState) { + var baseScreenSpaceError = Math.max(tileset.baseScreenSpaceError, tileset.maximumScreenSpaceError); + var maximumScreenSpaceError = tileset.maximumScreenSpaceError; + + // Load and select tiles without skipping up to baseScreenSpaceError + // Leaves of the base traversal is where we start the skip traversal + executeBaseTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, skipTraversal.stack, frameState); - function markLoadedTilesForSelection(tileset, frameState, outOfCore) { + executeSkipTraversal(tileset, maximumScreenSpaceError, frameState); + + // Mark tiles for selection or their nearest loaded ancestor + markDesiredTilesForSelection(tileset, frameState); + + // Sort selected tiles by distance to camera and call selectTile on each + traverseAndSelect(tileset, root, frameState); + } + + function markDesiredTilesForSelection(tileset, frameState) { var tiles = tileset._desiredTiles; var length = tiles.length; for (var i = 0; i < length; ++i) { - var original = tiles.get(i); - - if (hasAdditiveContent(original)) { - original.selected = true; - original._selectedFrame = frameState.frameNumber; - continue; + var tile = tiles.get(i); + var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; + if (defined(loadedTile)) { + markForSelection(tileset, tile, frameState); } + } + } - var loadedTile = original._ancestorWithLoadedContent; - if (original.hasRenderableContent && original.contentAvailable) { - loadedTile = original; - } + function selectTile(tileset, tile, frameState) { + if (tile._selectedFrame === frameState.frameNumber) { + tileset._selectedTiles.push(tile); + } + } - if (defined(loadedTile)) { - loadedTile.selected = true; - loadedTile._selectedFrame = frameState.frameNumber; - } else { - // if no ancestors are ready, traverse down and select ready tiles to minimize empty regions - descendantStack.push(original); - while (descendantStack.length > 0) { - var tile = descendantStack.pop(); - var children = tile.children; - var childrenLength = children.length; - for (var j = 0; j < childrenLength; ++j) { - var child = children[j]; - touch(tileset, child, outOfCore); - if (child.contentAvailable) { - child.selected = true; - child._finalResolution = true; - child._selectedFrame = frameState.frameNumber; - } - if (child._depth - original._depth < 2) { // prevent traversing too far - if (!child.contentAvailable || child.refine === Cesium3DTileRefine.ADD) { - descendantStack.push(child); - } - } - } - } + function markForSelection(tileset, tile, frameState) { + if (tile._selectedFrame === frameState.frameNumber) { + return; + } + + // There may also be a tight box around just the tile's contents, e.g., for a city, we may be + // zoomed into a neighborhood and can cull the skyscrapers in the root tile. + if (tile.contentAvailable && contentVisible(tile, frameState)) { + var tileContent = tile.content; + if (tileContent.featurePropertiesDirty) { + // A feature's property in this tile changed, the tile needs to be re-styled. + tileContent.featurePropertiesDirty = false; + tile.lastStyleTime = 0; // Force applying the style to this tile + tileset._selectedTilesToStyle.push(tile); + } else if ((tile._selectedFrame !== frameState.frameNumber - 1)) { + // Tile is newly selected; it is selected this frame, but was not selected last frame. + tile.lastStyleTime = 0; // Force applying the style to this tile + tileset._selectedTilesToStyle.push(tile); } + tile._selectedFrame = frameState.frameNumber; } } - var scratchStack = []; - var scratchStack2 = []; + function sortChildrenByDistanceToCamera(a, b) { + // Sort by farthest child first since this is going on a stack + if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { + return b._centerZDepth - a._centerZDepth; + } + + return b._distanceToCamera - a._distanceToCamera; + } /** * Traverse the tree while tiles are visible and check if their selected frame is the current frame. * If so, add it to a selection queue. - * Tiles are sorted near to far so we can take advantage of early Z. * Furthermore, this is a preorder traversal so children tiles are selected before ancestor tiles. * * The reason for the preorder traversal is so that tiles can easily be marked with their @@ -170,14 +210,18 @@ define([ * selected tiles must be no deeper than 15. This is still very unlikely. */ function traverseAndSelect(tileset, root, frameState) { - var stack = scratchStack; - var ancestorStack = scratchStack2; - + var stack = selectionTraversal.stack; + var ancestorStack = selectionTraversal.ancestorStack; var lastAncestor; + stack.push(root); + while (stack.length > 0 || ancestorStack.length > 0) { + selectionTraversal.stackMaximumLength = Math.max(selectionTraversal.stackMaximumLength, stack.length); + selectionTraversal.ancestorStackMaximumLength = Math.max(selectionTraversal.ancestorStackMaximumLength, ancestorStack.length); + if (ancestorStack.length > 0) { - var waitingTile = ancestorStack[ancestorStack.length - 1]; + var waitingTile = ancestorStack.get(ancestorStack.length - 1); if (waitingTile._stackLength === stack.length) { ancestorStack.pop(); if (waitingTile === lastAncestor) { @@ -189,36 +233,32 @@ define([ } var tile = stack.pop(); - if (!defined(tile) || !isVisited(tile, frameState)) { + if (!defined(tile)) { + // stack is empty but ancestorStack isn't continue; } - var shouldSelect = tile.selected && tile._selectedFrame === frameState.frameNumber && tile.hasRenderableContent; - + var add = tile.refine === Cesium3DTileRefine.ADD; + var markedForSelection = tile._selectedFrame === frameState.frameNumber; var children = tile.children; var childrenLength = children.length; - children.sort(sortChildrenByDistanceToCamera); - if (shouldSelect) { - if (tile.refine === Cesium3DTileRefine.ADD) { + if (markedForSelection) { + if (add) { tile._finalResolution = true; selectTile(tileset, tile, frameState); } else { tile._selectionDepth = ancestorStack.length; - if (tile._selectionDepth > 0) { tileset._hasMixedContent = true; } - lastAncestor = tile; - if (childrenLength === 0) { tile._finalResolution = true; selectTile(tileset, tile, frameState); continue; } - ancestorStack.push(tile); tile._stackLength = stack.length; } @@ -231,456 +271,72 @@ define([ } } - function selectTile(tileset, tile, frameState) { - // There may also be a tight box around just the tile's contents, e.g., for a city, we may be - // zoomed into a neighborhood and can cull the skyscrapers in the root tile. - if (tile.contentAvailable && ( - (tile._visibilityPlaneMask === CullingVolume.MASK_INSIDE) || - (tile.contentVisibility(frameState) !== Intersect.OUTSIDE) - )) { - tileset._selectedTiles.push(tile); - - var tileContent = tile.content; - if (tileContent.featurePropertiesDirty) { - // A feature's property in this tile changed, the tile needs to be re-styled. - tileContent.featurePropertiesDirty = false; - tile.lastStyleTime = 0; // Force applying the style to this tile - tileset._selectedTilesToStyle.push(tile); - } else if ((tile._lastSelectedFrameNumber !== frameState.frameNumber - 1) || tile.lastStyleTime === 0) { - // Tile is newly selected; it is selected this frame, but was not selected last frame. - tileset._selectedTilesToStyle.push(tile); - } - tile._lastSelectedFrameNumber = frameState.frameNumber; - } - } - - // PERFORMANCE_IDEA: is it worth exploiting frame-to-frame coherence in the sort, i.e., the - // list of children are probably fully or mostly sorted unless the camera moved significantly? - function sortChildrenByDistanceToCamera(a, b) { - // Sort by farthest child first since this is going on a stack - if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { - return b._centerZDepth - a._centerZDepth; - } - - return b._distanceToCamera - a._distanceToCamera; - } - - var emptyArray = freezeObject([]); - - function BaseTraversal() { - this.tileset = undefined; - this.frameState = undefined; - this.outOfCore = undefined; - this.stack = new ManagedArray(); - this.leaves = new ManagedArray(); - this.baseScreenSpaceError = undefined; - this.internalDFS = new InternalBaseTraversal(); - } - - BaseTraversal.prototype.execute = function(tileset, root, baseScreenSpaceError, frameState, outOfCore) { - this.tileset = tileset; - this.frameState = frameState; - this.outOfCore = outOfCore; - this.leaves.length = 0; - this.baseScreenSpaceError = Math.max(baseScreenSpaceError, this.tileset._maximumScreenSpaceError); - this.internalDFS.tileset = this.tileset; - this.internalDFS.frameState = this.frameState; - this.internalDFS.outOfCore = this.outOfCore; - this.internalDFS.baseScreenSpaceError = this.baseScreenSpaceError; - depthFirstSearch(root, this); - }; - - BaseTraversal.prototype.visitStart = function(tile) { - if (!isVisited(tile, this.frameState)) { - visitTile(this.tileset, tile, this.frameState, this.outOfCore); - } - }; - - BaseTraversal.prototype.visitEnd = function(tile) { - tile._lastVisitedFrame = this.frameState.frameNumber; - }; - - BaseTraversal.prototype.getChildren = function(tile) { - var tileset = this.tileset; - var outOfCore = this.outOfCore; - var frameState = this.frameState; - if (!baseUpdateAndCheckChildren(tileset, tile, this.baseScreenSpaceError, frameState)) { - return emptyArray; - } - - var children = tile.children; - var childrenLength = children.length; - var allReady = true; - var replacementWithContent = tile.refine === Cesium3DTileRefine.REPLACE && tile.hasRenderableContent; - for (var i = 0; i < childrenLength; ++i) { - var child = children[i]; - loadTile(tileset, child, frameState, true); - touch(tileset, child, outOfCore); - - // content cannot be replaced until all of the nearest descendants with content are all loaded - if (replacementWithContent) { - if (!child.hasEmptyContent) { - allReady = allReady && child.contentAvailable; - } else { - allReady = allReady && this.internalDFS.execute(child); - } - } - } - - if (allReady) { - return children; - } - - return emptyArray; - }; - - function baseUpdateAndCheckChildren(tileset, tile, baseScreenSpaceError, frameState) { - if (hasAdditiveContent(tile)) { - tileset._desiredTiles.push(tile); - } - - // Stop traversal on the subtree since it will be destroyed - if (tile.hasTilesetContent && tile.contentExpired) { - return false; - } - - // stop traversal when we've attained the desired level of error - if (tile._screenSpaceError <= baseScreenSpaceError && !tile.hasTilesetContent) { - // update children so the leaf handler can check if any are visible for the children union bound optimization - updateChildren(tile, frameState); - return false; - } - - var childrenVisibility = updateChildren(tile, frameState); - var showAdditive = tile.refine === Cesium3DTileRefine.ADD; - var showReplacement = tile.refine === Cesium3DTileRefine.REPLACE && (childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE_IN_REQUEST_VOLUME) !== 0; - - return showAdditive || showReplacement || tile.hasTilesetContent || !defined(tile._ancestorWithContent); - } - - BaseTraversal.prototype.shouldVisit = function(tile) { - return isVisible(tile._visibilityPlaneMask); - }; - - BaseTraversal.prototype.leafHandler = function(tile) { - // if skipLevelOfDetail is off, leaves of the base traversal get pushed to tileset._desiredTiles. additive tiles have already been pushed - if (this.tileset._skipLevelOfDetail || !hasAdditiveContent(tile)) { - if (tile.refine === Cesium3DTileRefine.REPLACE && !childrenAreVisible(tile)) { - ++this.tileset._statistics.numberOfTilesCulledWithChildrenUnion; - return; - } - this.leaves.push(tile); - } - }; - - function InternalBaseTraversal() { - this.tileset = undefined; - this.frameState = undefined; - this.outOfCore = undefined; - this.baseScreenSpaceError = undefined; - this.stack = new ManagedArray(); - this.allLoaded = undefined; - } - - InternalBaseTraversal.prototype.execute = function(root) { - this.allLoaded = true; - depthFirstSearch(root, this); - return this.allLoaded; - }; - - InternalBaseTraversal.prototype.visitStart = function(tile) { - if (!isVisited(tile, this.frameState)) { - visitTile(this.tileset, tile, this.frameState, this.outOfCore); - } - }; - - InternalBaseTraversal.prototype.visitEnd = BaseTraversal.prototype.visitEnd; - - // Continue traversing until we have renderable content. We want the first descendants with content of the root to load - InternalBaseTraversal.prototype.shouldVisit = function(tile) { - return !tile.hasRenderableContent && isVisible(tile._visibilityPlaneMask); - }; - - InternalBaseTraversal.prototype.getChildren = function(tile) { - var tileset = this.tileset; - var frameState = this.frameState; - var outOfCore = this.outOfCore; - - if (!baseUpdateAndCheckChildren(tileset, tile, this.baseScreenSpaceError, frameState)) { - return emptyArray; - } - - var children = tile.children; - var childrenLength = children.length; - for (var i = 0; i < childrenLength; ++i) { - var child = children[i]; - loadTile(tileset, child, frameState, true); - touch(tileset, child, outOfCore); - if (!tile.contentAvailable) { - this.allLoaded = false; - } - } - return children; - }; - - InternalBaseTraversal.prototype.updateAndCheckChildren = BaseTraversal.prototype.updateAndCheckChildren; - - function SkipTraversal(options) { - this.tileset = undefined; - this.frameState = undefined; - this.outOfCore = undefined; - this.queue1 = new ManagedArray(); - this.queue2 = new ManagedArray(); - this.internalDFS = new InternalSkipTraversal(options.selectionHeuristic); - this.maxChildrenLength = 0; - this.scratchQueue = new ManagedArray(); + function contentVisible(tile, frameState) { + return (tile._visibilityPlaneMask === CullingVolume.MASK_INSIDE) || + (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); } - SkipTraversal.prototype.execute = function(tileset, root, frameState, outOfCore) { - this.tileset = tileset; - this.frameState = frameState; - this.outOfCore = outOfCore; - this.internalDFS.frameState = frameState; - this.internalDFS.outOfCore = outOfCore; - - this.maxChildrenLength = 0; - breadthFirstSearch(root, this); - this.queue1.length = 0; - this.queue2.length = 0; - this.scratchQueue.length = 0; - this.scratchQueue.trim(this.maxChildrenLength); - }; - - SkipTraversal.prototype.visitStart = function(tile) { - if (!isVisited(tile, this.frameState)) { - visitTile(this.tileset, tile, this.frameState, this.outOfCore); - } - }; - - SkipTraversal.prototype.visitEnd = BaseTraversal.prototype.visitEnd; - - SkipTraversal.prototype.getChildren = function(tile) { - this.scratchQueue.length = 0; - this.internalDFS.execute(tile, this.scratchQueue); - this.maxChildrenLength = Math.max(this.maxChildrenLength, this.scratchQueue.length); - return this.scratchQueue; - }; - - SkipTraversal.prototype.leafHandler = function(tile) { - // additive tiles have already been pushed - if (!hasAdditiveContent(tile) && !isVisited(tile, this.frameState)) { - this.tileset._desiredTiles.push(tile); + function addDesiredTile(tileset, tile, frameState) { + if (tile._touchedFrame === frameState.frameNumber) { + return; } - }; - function InternalSkipTraversal(selectionHeuristic) { - this.selectionHeuristic = selectionHeuristic; - this.tileset = undefined; - this.frameState = undefined; - this.outOfCore = undefined; - this.root = undefined; - this.queue = undefined; - this.stack = new ManagedArray(); + tileset._desiredTiles.push(tile); } - InternalSkipTraversal.prototype.execute = function(root, queue) { - this.tileset = root._tileset; - this.root = root; - this.queue = queue; - depthFirstSearch(root, this); - }; - - InternalSkipTraversal.prototype.visitStart = function(tile) { - if (!isVisited(tile, this.frameState)) { - visitTile(this.tileset, tile, this.frameState, this.outOfCore); - } - }; - - InternalSkipTraversal.prototype.visitEnd = BaseTraversal.prototype.visitEnd; - - InternalSkipTraversal.prototype.getChildren = function(tile) { - var tileset = this.tileset; - var maximumScreenSpaceError = tileset._maximumScreenSpaceError; - - // Stop traversal on the subtree since it will be destroyed - if (tile.hasTilesetContent && tile.contentExpired) { - return emptyArray; - } - - if (!tile.hasTilesetContent) { - if (tile.refine === Cesium3DTileRefine.ADD) { - // Always load additive tiles - loadTile(tileset, tile, this.frameState, true); - if (hasAdditiveContent(tile)) { - tileset._desiredTiles.push(tile); - } - } - - // stop traversal when we've attained the desired level of error - if (tile._screenSpaceError <= maximumScreenSpaceError) { - updateChildren(tile, this.frameState); - return emptyArray; - } - - // if we have reached the skipping threshold without any loaded ancestors, return empty so this tile is loaded - if ( - (!tile.hasEmptyContent && tile.contentUnloaded) && - defined(tile._ancestorWithLoadedContent) && - this.selectionHeuristic(tileset, tile._ancestorWithLoadedContent, tile)) { - updateChildren(tile, this.frameState); - return emptyArray; - } - } - - var childrenVisibility = updateChildren(tile, this.frameState); - var showAdditive = tile.refine === Cesium3DTileRefine.ADD && tile._screenSpaceError > maximumScreenSpaceError; - var showReplacement = tile.refine === Cesium3DTileRefine.REPLACE && (childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE_IN_REQUEST_VOLUME) !== 0; - - // at least one child is visible, but is not in request volume. the parent must be selected - if (childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE_NOT_IN_REQUEST_VOLUME && tile.refine === Cesium3DTileRefine.REPLACE) { - this.tileset._desiredTiles.push(tile); - } - - if (showAdditive || showReplacement || tile.hasTilesetContent) { - var children = tile.children; - var childrenLength = children.length; - for (var i = 0; i < childrenLength; ++i) { - touch(tileset, children[i], this.outOfCore); - } - return children; - } - - return emptyArray; - }; - - InternalSkipTraversal.prototype.shouldVisit = function(tile) { - return isVisibleAndMeetsSSE(this.tileset, tile, this.frameState); - }; - - InternalSkipTraversal.prototype.leafHandler = function(tile) { - if (tile !== this.root) { - if (tile.refine === Cesium3DTileRefine.REPLACE && !childrenAreVisible(tile)) { - ++this.tileset._statistics.numberOfTilesCulledWithChildrenUnion; - return; - } - if (!tile.hasEmptyContent) { - if (this.tileset.loadSiblings) { - var parent = tile.parent; - var tiles = parent.children; - var length = tiles.length; - for (var i = 0; i < length; ++i) { - loadTile(this.tileset, tiles[i], this.frameState, false); - touch(this.tileset, tiles[i], this.outOfCore); - } - } else { - loadTile(this.tileset, tile, this.frameState, true); - touch(this.tileset, tile, this.outOfCore); - } - } - this.queue.push(tile); - } else if (!hasAdditiveContent(tile)) { - // additive tiles have already been pushed - this.tileset._desiredTiles.push(tile); - } - }; - - function updateChildren(tile, frameState) { - if (isVisited(tile, frameState)) { - return tile._childrenVisibility; + function visitTile(tileset, tile, frameState) { + if (tile._touchedFrame === frameState.frameNumber) { + return; } - var children = tile.children; - - updateTransforms(children, tile.computedTransform); - computeDistanceToCamera(children, frameState); - - return computeChildrenVisibility(tile, frameState); - } - - function isVisited(tile, frameState) { - // because the leaves of one tree traversal are the root of the subsequent traversal, avoid double visitation - return tile._lastVisitedFrame === frameState.frameNumber; - } - - function visitTile(tileset, tile, frameState, outOfCore) { ++tileset._statistics.visited; - tile.selected = false; tile._finalResolution = false; - computeSSE(tile, frameState); - touch(tileset, tile, outOfCore); + tile._ancestorWithContentAvailable = undefined; + tile.updateExpiration(); - tile._ancestorWithContent = undefined; - tile._ancestorWithLoadedContent = undefined; + var parent = tile.parent; if (defined(parent)) { var replace = parent.refine === Cesium3DTileRefine.REPLACE; - tile._ancestorWithContent = (replace && parent.hasRenderableContent) ? parent : parent._ancestorWithContent; - tile._ancestorWithLoadedContent = (replace && parent.hasRenderableContent && parent.contentAvailable) ? parent : parent._ancestorWithLoadedContent; + tile._ancestorWithContentAvailable = (replace && parent.contentAvailable) ? parent : parent._ancestorWithContentAvailable; } } - function touch(tileset, tile, outOfCore) { - if (!outOfCore) { + function touch(tileset, tile, frameState) { + if (tile._touchedFrame === frameState.frameNumber) { return; } - tileset._cache.touch(tile); - } - function computeSSE(tile, frameState) { - if (tile._screenSpaceErrorComputedFrame !== frameState.frameNumber) { - tile._screenSpaceErrorComputedFrame = frameState.frameNumber; - tile._screenSpaceError = getScreenSpaceError(tile._tileset, tile.geometricError, tile, frameState); - } + tileset._cache.touch(tile); } - function checkAdditiveVisibility(tileset, tile, frameState) { - if (defined(tile.parent) && (tile.parent.refine === Cesium3DTileRefine.ADD)) { - return isVisibleAndMeetsSSE(tileset, tile, frameState); - } - return true; + function getPriority(tile, useParentPriority) { + // TODO : we want priority of base traversal to be SSE based so that it refines fast... + // TODO : doesn't really matter though if we have skiplods on, though it does help a bit + // TODO : somehow that means we need some comparison of distance and sse + // The base traversal sets useParentPriority to true so that child tiles can load as soon as possible so that their parent can refine sooner. + // Additive tiles always load based on distance because it subjectively looks better + // TODO : what to do about tileset with heavy mix of replace and add. The priorities will differ a lot. + var parent = tile.parent; + var replace = tile.refine === Cesium3DTileRefine.REPLACE; + var add = tile.refine === Cesium3DTileRefine.ADD; + //if (add) { + return tile._distanceToCamera; + //} else if (replace) { + // var priority = (defined(parent) && (useParentPriority || tile._screenSpaceError === 0.0)) ? parent._screenSpaceError : tile._screenSpaceError; + // return 100000.0 - priority; // TODO : doing this just because RequestScheduler wants lower priority + //} } - function loadTile(tileset, tile, frameState, checkVisibility) { - if ((tile.contentUnloaded || tile.contentExpired) && tile._requestedFrame !== frameState.frameNumber) { - if (!checkVisibility || checkAdditiveVisibility(tileset, tile, frameState)) { - tile._requestedFrame = frameState.frameNumber; - tileset._requestedTiles.push(tile); - } + function loadTile(tileset, tile, useParentPriority, frameState) { + if (tile._touchedFrame === frameState.frameNumber) { + return; } - } - - function computeChildrenVisibility(tile, frameState) { - var flag = Cesium3DTileChildrenVisibility.NONE; - var children = tile.children; - var childrenLength = children.length; - var visibilityPlaneMask = tile._visibilityPlaneMask; - for (var k = 0; k < childrenLength; ++k) { - var child = children[k]; - - var visibilityMask = child.visibility(frameState, visibilityPlaneMask); - - if (isVisible(visibilityMask)) { - flag |= Cesium3DTileChildrenVisibility.VISIBLE; - } - - if (!child.insideViewerRequestVolume(frameState)) { - if (isVisible(visibilityMask)) { - flag |= Cesium3DTileChildrenVisibility.VISIBLE_NOT_IN_REQUEST_VOLUME; - } - visibilityMask = CullingVolume.MASK_OUTSIDE; - } else { - flag |= Cesium3DTileChildrenVisibility.IN_REQUEST_VOLUME; - if (isVisible(visibilityMask)) { - flag |= Cesium3DTileChildrenVisibility.VISIBLE_IN_REQUEST_VOLUME; - } - } - child._visibilityPlaneMask = visibilityMask; + if (tile.contentUnloaded || tile.contentExpired) { + tile._priority = getPriority(tile, useParentPriority); + tileset._requestedTiles.push(tile); } - - tile._childrenVisibility = flag; - - return flag; } function getScreenSpaceError(tileset, geometricError, tile, frameState) { @@ -719,133 +375,270 @@ define([ return error; } - function computeDistanceToCamera(children, frameState) { - var length = children.length; - for (var i = 0; i < length; ++i) { - var child = children[i]; - child._distanceToCamera = child.distanceToTile(frameState); - child._centerZDepth = child.distanceToTileCenter(frameState); + function updateVisibility(tileset, tile, frameState) { + if (tile._updatedVisibilityFrame === frameState.frameNumber) { + return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; } + + var parent = tile.parent; + var parentTransorm = defined(parent) ? parent.computedTransform : tileset._modelMatrix; + var parentVisibilityPlaneMask = defined(parent) ? parent._visibilityPlaneMask : CullingVolume.MASK_INDETERMINATE; + + tile.updateTransform(parentTransorm); + tile._distanceToCamera = tile.distanceToTile(frameState); + tile._centerZDepth = tile.distanceToTileCenter(frameState); + tile._screenSpaceError = getScreenSpaceError(tileset, tile.geometricError, tile, frameState); + + var visibilityPlaneMask = tile.visibility(frameState, parentVisibilityPlaneMask); // Use parent's plane mask to speed up visibility test + var visible = visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; + visible = visible && tile.insideViewerRequestVolume(frameState); + tile._visibilityPlaneMask = visible ? visibilityPlaneMask : CullingVolume.MASK_OUTSIDE; + tile._updatedFrame = frameState.frameNumber; + return visible; } - function updateTransforms(children, parentTransform) { + function updateChildrenVisibility(tileset, tile, frameState) { + var anyVisible = false; + var children = tile.children; var length = children.length; for (var i = 0; i < length; ++i) { - var child = children[i]; - child.updateTransform(parentTransform); + var childVisible = updateVisibility(tileset, children[i], frameState); + anyVisible = anyVisible || childVisible; } + return anyVisible; } - function isVisible(visibilityPlaneMask) { - return visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; - } + function getVisibility(tileset, tile, maximumScreenSpaceError, frameState) { + updateVisibility(tileset, tile, frameState); - function isVisibleAndMeetsSSE(tileset, tile, frameState) { - var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + // Not visible. Visibility is updated in its parent's call to updateChildren. + if (tile._visibilityPlaneMask === CullingVolume.MASK_OUTSIDE) { + return false; + } + + // Don't visit an expired subtree because it will be destroyed + if (tile.hasTilesetContent && tile.contentExpired) { + return false; + } + + // Use parent's geometric error with child's box to see if we already meet the SSE var parent = tile.parent; - if (!defined(parent)) { - return isVisible(tile._visibilityPlaneMask); + if (defined(parent) && (parent.refine === Cesium3DTileRefine.ADD) && getScreenSpaceError(tileset, parent.geometricError, tile, frameState) <= maximumScreenSpaceError) { + return false; } - var showAdditive = parent.refine === Cesium3DTileRefine.ADD && parent._screenSpaceError > maximumScreenSpaceError; - return isVisible(tile._visibilityPlaneMask) && (!showAdditive || getScreenSpaceError(tileset, parent.geometricError, tile, frameState) > maximumScreenSpaceError); - } + // Optimization - if none of the tile's children are visible then this tile isn't visible + var replace = tile.refine === Cesium3DTileRefine.REPLACE; + var useOptimization = tile._optimChildrenWithinParent === Cesium3DTileOptimizationHint.USE_OPTIMIZATION; + var hasChildren = tile.children.length > 0; + if (replace && useOptimization && hasChildren) { + var anyChildrenVisible = updateChildrenVisibility(tileset, tile, frameState); + if (!anyChildrenVisible) { + ++tileset._statistics.numberOfTilesCulledWithChildrenUnion; + return false; + } + } - function childrenAreVisible(tile) { - // optimization does not apply for additive refinement - return tile.refine === Cesium3DTileRefine.ADD || tile.children.length === 0 || tile._childrenVisibility & Cesium3DTileChildrenVisibility.VISIBLE !== 0; + return true; } - function hasAdditiveContent(tile) { - return tile.refine === Cesium3DTileRefine.ADD && tile.hasRenderableContent; + function updateTile(tileset, tile, maximumScreenSpaceError, frameState) { + if (tile._touchedFrame === frameState.frameNumber) { + return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; + } + + var visible = getVisibility(tileset, tile, maximumScreenSpaceError, frameState); + tile._visibilityPlaneMask = visible ? tile._visibilityPlaneMask : CullingVolume.MASK_OUTSIDE; + return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; } - function depthFirstSearch(root, options) { - var stack = options.stack; + function hasEmptyContent(tile) { + return tile.hasEmptyContent || tile.hasTilesetContent; + } - if (defined(root) && (!defined(options.shouldVisit) || options.shouldVisit(root))) { - stack.push(root); - } + function executeBaseTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, leaves, frameState) { + // TODO wording + // Depth-first traversal that loads all tiles that are visible and don't meet the screen space error. + // For replacement refinement: selects tiles to render that can't refine further - either because + // their children aren't loaded yet or they meet the screen space error. + // For additive refinement: select all tiles to render + var stack = baseTraversal.stack; + stack.push(root); - var maxLength = 0; while (stack.length > 0) { - maxLength = Math.max(maxLength, stack.length); + baseTraversal.stackMaximumLength = Math.max(baseTraversal.stackMaximumLength, stack.length); var tile = stack.pop(); - options.visitStart(tile); - var children = options.getChildren(tile); - var isNativeArray = !defined(children.get); - var length = children.length; - for (var i = 0; i < length; ++i) { - var child = isNativeArray ? children[i] : children.get(i); - - if (!defined(options.shouldVisit) || options.shouldVisit(child)) { - stack.push(child); + var add = tile.refine === Cesium3DTileRefine.ADD; + var replace = tile.refine === Cesium3DTileRefine.REPLACE; + var children = tile.children; + var childrenLength = children.length; + var contentAvailable = tile.contentAvailable; + var traverse = (childrenLength > 0) && (tile._screenSpaceError > baseScreenSpaceError); + var parent = tile.parent; + var parentRefines = !defined(parent) || parent._refines; + var refines = traverse && parentRefines; + + if (traverse) { + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + var visible = updateTile(tileset, child, maximumScreenSpaceError, frameState); + if (visible) { + stack.push(child); + } + if (replace && !hasEmptyContent(tile)) { + // Check if the parent can refine to this child. If the child is empty we need to traverse further to + // load all descendants with content. Keep non-visible children loaded since they are still needed before the parent can refine. + // We don't do this for empty tiles because it looks better if children stream in as they are loaded to fill the empty space. + if (!visible) { + loadTile(tileset, child, true, frameState); + touch(tileset, child, frameState); + child._touchedFrame = frameState.frameNumber; + } + + // Always run the internal base traversal even if we already know we can't refine. This keeps the tiles loaded while we wait to refine. + var refinesToChild = hasEmptyContent(child) ? executeInternalBaseTraversal(tileset, child, baseScreenSpaceError, maximumScreenSpaceError, frameState) : child.contentAvailable; + refines = refines && refinesToChild; + } } } - if (length === 0 && defined(options.leafHandler)) { - options.leafHandler(tile); + if (replace && contentAvailable && !refines && parentRefines) { + // Replacement tiles that have loaded content but can't refine further are desired + addDesiredTile(tileset, tile, frameState); } - options.visitEnd(tile); - } - stack.trim(maxLength); - } + if (add && contentAvailable) { + // Additive tiles that have loaded content are always desired + addDesiredTile(tileset, tile, frameState); + } - function breadthFirstSearch(root, options) { - var queue1 = options.queue1; - var queue2 = options.queue2; + if (defined(leaves) && !traverse && (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError)) { + // If the tile cannot traverse further in the base traversal but can traverse further in the skip traversal, add it to leaves + // leaves is undefined if we are just running the base traversal + leaves.push(tile); + } - if (defined(root) && (!defined(options.shouldVisit) || options.shouldVisit(root))) { - queue1.push(root); + visitTile(tileset, tile, frameState); + loadTile(tileset, tile, true, frameState); + touch(tileset, tile, frameState); + tile._touchedFrame = frameState.frameNumber; + tile._refines = refines; } + } - var maxLength = 0; - while (queue1.length > 0) { - var length = queue1.length; - maxLength = Math.max(maxLength, length); - - for (var i = 0; i < length; ++i) { - var tile = queue1.get(i); - options.visitStart(tile); - var children = options.getChildren(tile); - var isNativeArray = !defined(children.get); - var childrenLength = children.length; - for (var j = 0; j < childrenLength; ++j) { - var child = isNativeArray ? children[j] : children.get(j); - - if (!defined(options.shouldVisit) || options.shouldVisit(child)) { - queue2.push(child); - } - } + function executeInternalBaseTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState) { + // Depth-first traversal that checks if all nearest descendants with content are loaded. Ignores visibility. + // The reason we need to implement a full traversal is to handle chains of empty tiles. + var allDescendantsLoaded = true; - if (childrenLength === 0 && defined(options.leafHandler)) { - options.leafHandler(tile); - } - options.visitEnd(tile); + var stack = internalBaseTraversal.stack; + stack.push(root); + + while (stack.length > 0) { + internalBaseTraversal.stackMaximumLength = Math.max(internalBaseTraversal.stackMaximumLength, stack.length); + + var tile = stack.pop(); + var children = tile.children; + var childrenLength = children.length; + + // Only traverse if the tile is empty - we are trying to find descendants with content + var traverse = hasEmptyContent(tile) && (childrenLength > 0) && (tile._screenSpaceError > baseScreenSpaceError); + + // When we reach a "leaf" that does not have content available we know that not all descendants are loaded + // i.e. there will be holes if the parent tries to refine to its children, so don't refine + if (!traverse && !tile.contentAvailable) { + allDescendantsLoaded = false; } - queue1.length = 0; - var temp = queue1; - queue1 = queue2; - queue2 = temp; - options.queue1 = queue1; - options.queue2 = queue2; + var visible = updateTile(tileset, tile, maximumScreenSpaceError, frameState); + if (!visible) { + // Load tiles that aren't visible since they are still needed for the parent to refine + // Tiles that are visible will get loaded from within executeBaseTraversal + loadTile(tileset, tile, true, frameState); + touch(tileset, tile, frameState); + tile._touchedFrame = frameState.frameNumber; + } + + if (traverse) { + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + stack.push(child); + } + } } - queue1.length = 0; - queue2.length = 0; + return allDescendantsLoaded; + } - queue1.trim(maxLength); - queue2.trim(maxLength); + function reachedSkippingThreshold(tileset, tile) { + // TODO : dont understand this comment. Why is defined(tile)? + // If we have reached the skipping threshold without any loaded ancestors, return empty so this tile is loaded + var ancestor = tile._ancestorWithContentAvailable; + var skipLevels = tileset.skipLevelOfDetail ? tileset.skipLevels : 0; + var skipScreenSpaceErrorFactor = tileset.skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; + + return !tileset.immediatelyLoadDesiredLevelOfDetail && + tile.contentUnloaded && + defined(ancestor) && (ancestor !== tile) && + (tile._screenSpaceError < (ancestor._screenSpaceError / skipScreenSpaceErrorFactor)) && + (tile._depth > (ancestor._depth + skipLevels)); } - Cesium3DTilesetTraversal.selectTiles = selectTiles; + function executeSkipTraversal(tileset, maximumScreenSpaceError, frameState) { + // Depth-first traversal that skips tiles until in reaches the skipping threshold. + var i; + + var stack = skipTraversal.stack; + + while (stack.length > 0) { + skipTraversal.stackMaximumLength = Math.max(skipTraversal.stackMaximumLength, stack.length); + + var tile = stack.pop(); + var add = tile.refine === Cesium3DTileRefine.ADD; + var replace = tile.refine === Cesium3DTileRefine.REPLACE; + + visitTile(tileset, tile, frameState); + + var children = tile.children; + var childrenLength = children.length; + var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); + + if (traverse) { + for (i = 0; i < childrenLength; ++i) { + var child = children[i]; + var visible = updateTile(tileset, child, maximumScreenSpaceError, frameState); + if (visible) { + stack.push(child); + } + } + } - Cesium3DTilesetTraversal.BaseTraversal = BaseTraversal; + if (add) { + // Additive tiles are always desired + if (tile.contentAvailable) { + addDesiredTile(tileset, tile, frameState); + } + loadTile(tileset, tile, false, frameState); + touch(tileset, tile, frameState); + tile._touchedFrame = frameState.frameNumber; + } - Cesium3DTilesetTraversal.SkipTraversal = SkipTraversal; + if (replace) { + if (!traverse || reachedSkippingThreshold(tileset, tile)) { + addDesiredTile(tileset, tile, frameState); + loadTile(tileset, tile, false, frameState); + touch(tileset, tile, frameState); + tile._touchedFrame = frameState.frameNumber; + } else { + // Skipped tiles may still have loaded content that we want to preserve + touch(tileset, tile, frameState); + tile._touchedFrame = frameState.frameNumber; + } + } + } + // TODO : ignoring loadSiblings right now + } return Cesium3DTilesetTraversal; }); diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 1e0330376b61..f24d67e4da0a 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1021,7 +1021,7 @@ defineSuite([ // No longer renders the tile with a request volume setZoom(1500.0); - root.hasRenderableContent = true; // mock content + root.contentAvailable = true; // mock content scene.renderForSpecs(); expect(statistics.numberOfCommands).toEqual(4); expect(root.selected).toBe(true); // one child is no longer selected. root is chosen instead From 7500d7d3ed080edddc206a003e683e2ba5c7d7e5 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 20 Mar 2018 15:54:10 -0400 Subject: [PATCH 05/53] Stable --- Source/Scene/Cesium3DTileBatchTable.js | 7 --- Source/Scene/Cesium3DTileset.js | 3 +- Source/Scene/Cesium3DTilesetTraversal.js | 62 ++++++++++++++++++++---- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/Source/Scene/Cesium3DTileBatchTable.js b/Source/Scene/Cesium3DTileBatchTable.js index 87982d59062c..5c158e3ab61d 100644 --- a/Source/Scene/Cesium3DTileBatchTable.js +++ b/Source/Scene/Cesium3DTileBatchTable.js @@ -1356,13 +1356,6 @@ define([ tileset._backfaceCommands.push(derivedCommands.zback); } if (!defined(derivedCommands.stencil) || tile._selectionDepth !== getLastSelectionDepth(derivedCommands.stencil)) { - // if (defined(derivedCommands.stencil)) { - // var currentSelectionDepth = tile._selectionDepth; - // var newSelectionDepth = getLastSelectionDepth(derivedCommands.stencil); - // console.log('My selection depth: ' + tile._selectionDepth); - // console.log('Previous selection depth: ' + (defined(derivedCommands.stencil) ? getLastSelectionDepth(derivedCommands.stencil) : 'None')); - // - // } derivedCommands.stencil = deriveStencilCommand(derivedCommands.originalCommand, tile._selectionDepth); } updateDerivedCommand(derivedCommands.stencil, command); diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 310f9ca94cc8..0e1452e0d670 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -1437,6 +1437,7 @@ define([ } function requestTiles(tileset) { + // TODO : sort requested tiles by priority here so we can avoid requests from being cancelled excessively var requestedTiles = tileset._requestedTiles; var length = requestedTiles.length; for (var i = 0; i < length; ++i) { @@ -1611,8 +1612,6 @@ define([ var tileVisible = tileset.tileVisible; var i; - console.log(tileset._hasMixedContent); - var bivariateVisibilityTest = tileset._skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer && length > 0; tileset._backfaceCommands.length = 0; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index d33746b9e23c..a12efe38bb71 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -145,7 +145,7 @@ define([ var tile = tiles.get(i); var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; if (defined(loadedTile)) { - markForSelection(tileset, tile, frameState); + markForSelection(tileset, loadedTile, frameState); } } } @@ -276,9 +276,23 @@ define([ (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); } + // function addDesiredTile(tileset, tile, frameState) { + // if (tile._touchedFrame === frameState.frameNumber) { + // return; + // } + // + // tileset._desiredTiles.push(tile); + // } + function addDesiredTile(tileset, tile, frameState) { - if (tile._touchedFrame === frameState.frameNumber) { - return; + var desiredTiles = tileset._desiredTiles; + var length = desiredTiles.length; + + for (var i = 0; i < length; ++i) { + var other = desiredTiles[i]; + if (other === tile) { + return; + } } tileset._desiredTiles.push(tile); @@ -503,8 +517,11 @@ define([ } } - if (replace && contentAvailable && !refines && parentRefines) { + // TODO : optimization of rendering visible children even if the parent hasn't refined + + if (!defined(leaves) && replace && contentAvailable && !refines && parentRefines) { // Replacement tiles that have loaded content but can't refine further are desired + // leaves is undefined if we are just running the base traversal addDesiredTile(tileset, tile, frameState); } @@ -513,11 +530,11 @@ define([ addDesiredTile(tileset, tile, frameState); } - if (defined(leaves) && !traverse && (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError)) { - // If the tile cannot traverse further in the base traversal but can traverse further in the skip traversal, add it to leaves - // leaves is undefined if we are just running the base traversal - leaves.push(tile); - } + // if (defined(leaves) && !traverse && (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError)) { + // // If the tile cannot traverse further in the base traversal but can traverse further in the skip traversal, add it to leaves + // // leaves is undefined if we are just running the base traversal + // leaves.push(tile); + // } visitTile(tileset, tile, frameState); loadTile(tileset, tile, true, frameState); @@ -578,6 +595,8 @@ define([ var skipLevels = tileset.skipLevelOfDetail ? tileset.skipLevels : 0; var skipScreenSpaceErrorFactor = tileset.skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; + // TODO : why does ancestor need to be defined? When would the ancestor even equal the tile? + // TODO : should probably be using ancestorWithContent rather than contentAvailable? return !tileset.immediatelyLoadDesiredLevelOfDetail && tile.contentUnloaded && defined(ancestor) && (ancestor !== tile) && @@ -591,6 +610,10 @@ define([ var stack = skipTraversal.stack; + // TODO : possibly remove this + stack.length = 0; + stack.push(tileset._root); + while (stack.length > 0) { skipTraversal.stackMaximumLength = Math.max(skipTraversal.stackMaximumLength, stack.length); @@ -604,6 +627,11 @@ define([ var childrenLength = children.length; var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); + // TODO : possibly remove this + if (traverse && replace && reachedSkippingThreshold(tileset, tile)) { + traverse = false; + } + if (traverse) { for (i = 0; i < childrenLength; ++i) { var child = children[i]; @@ -636,6 +664,22 @@ define([ tile._touchedFrame = frameState.frameNumber; } } + + // if (replace) { + // if (!traverse) { + // addDesiredTile(tileset, tile, frameState); + // loadTile(tileset, tile, false, frameState); + // touch(tileset, tile, frameState); + // tile._touchedFrame = frameState.frameNumber; + // } else if (reachedSkippingThreshold(tileset, tile)) { + // loadTile(tileset, tile, false, frameState); + // touch(tileset, tile, frameState); + // tile._touchedFrame = frameState.frameNumber; + // } else { + // touch(tileset, tile, frameState); + // tile._touchedFrame = frameState.frameNumber; + // } + // } } // TODO : ignoring loadSiblings right now } From b2854393300e6ee2e2fdd7b46a4fa6212ae5e9a0 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 21 Mar 2018 13:55:11 -0400 Subject: [PATCH 06/53] Optimizations --- Source/Scene/Cesium3DTile.js | 1 + Source/Scene/Cesium3DTilesetTraversal.js | 72 ++++++++---------------- 2 files changed, 25 insertions(+), 48 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 84cb889323ac..e39dd5fdb8b3 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -325,6 +325,7 @@ define([ this._updatedVisibilityFrame = 0; this._touchedFrame = 0; this._selectedFrame = 0; + this._ancestorWithContent = undefined; this._ancestorWithContentAvailable = undefined; this._refines = false; this._priority = 0.0; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index a12efe38bb71..48eace509ef1 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -305,14 +305,15 @@ define([ ++tileset._statistics.visited; tile._finalResolution = false; + tile._ancestorWithContent = undefined; tile._ancestorWithContentAvailable = undefined; tile.updateExpiration(); var parent = tile.parent; if (defined(parent)) { - var replace = parent.refine === Cesium3DTileRefine.REPLACE; - tile._ancestorWithContentAvailable = (replace && parent.contentAvailable) ? parent : parent._ancestorWithContentAvailable; + tile._ancestorWithContent = !hasEmptyContent(parent) ? parent : parent._ancestorWithContent; + tile._ancestorWithContentAvailable = parent.contentAvailable ? parent : parent._ancestorWithContentAvailable; } } @@ -589,17 +590,14 @@ define([ } function reachedSkippingThreshold(tileset, tile) { - // TODO : dont understand this comment. Why is defined(tile)? - // If we have reached the skipping threshold without any loaded ancestors, return empty so this tile is loaded - var ancestor = tile._ancestorWithContentAvailable; + // Look at the nearest ancestor that wasn't skipped. It doesn't need to be loaded yet. + var ancestor = tile._ancestorWithContent; var skipLevels = tileset.skipLevelOfDetail ? tileset.skipLevels : 0; var skipScreenSpaceErrorFactor = tileset.skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; - // TODO : why does ancestor need to be defined? When would the ancestor even equal the tile? - // TODO : should probably be using ancestorWithContent rather than contentAvailable? return !tileset.immediatelyLoadDesiredLevelOfDetail && tile.contentUnloaded && - defined(ancestor) && (ancestor !== tile) && + defined(ancestor) && (tile._screenSpaceError < (ancestor._screenSpaceError / skipScreenSpaceErrorFactor)) && (tile._depth > (ancestor._depth + skipLevels)); } @@ -620,26 +618,23 @@ define([ var tile = stack.pop(); var add = tile.refine === Cesium3DTileRefine.ADD; var replace = tile.refine === Cesium3DTileRefine.REPLACE; - - visitTile(tileset, tile, frameState); - var children = tile.children; var childrenLength = children.length; var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); - // TODO : possibly remove this - if (traverse && replace && reachedSkippingThreshold(tileset, tile)) { - traverse = false; - } + visitTile(tileset, tile, frameState); - if (traverse) { - for (i = 0; i < childrenLength; ++i) { - var child = children[i]; - var visible = updateTile(tileset, child, maximumScreenSpaceError, frameState); - if (visible) { - stack.push(child); - } + if (replace) { + if (!traverse) { + addDesiredTile(tileset, tile, frameState); + loadTile(tileset, tile, false, frameState); + } else if (reachedSkippingThreshold(tileset, tile)) { + loadTile(tileset, tile, false, frameState); + traverse = false; } + // Always touch tiles. Even tiles that are skipped should stay loaded. + touch(tileset, tile, frameState); + tile._touchedFrame = frameState.frameNumber; } if (add) { @@ -652,34 +647,15 @@ define([ tile._touchedFrame = frameState.frameNumber; } - if (replace) { - if (!traverse || reachedSkippingThreshold(tileset, tile)) { - addDesiredTile(tileset, tile, frameState); - loadTile(tileset, tile, false, frameState); - touch(tileset, tile, frameState); - tile._touchedFrame = frameState.frameNumber; - } else { - // Skipped tiles may still have loaded content that we want to preserve - touch(tileset, tile, frameState); - tile._touchedFrame = frameState.frameNumber; + if (traverse) { + for (i = 0; i < childrenLength; ++i) { + var child = children[i]; + var visible = updateTile(tileset, child, maximumScreenSpaceError, frameState); + if (visible) { + stack.push(child); + } } } - - // if (replace) { - // if (!traverse) { - // addDesiredTile(tileset, tile, frameState); - // loadTile(tileset, tile, false, frameState); - // touch(tileset, tile, frameState); - // tile._touchedFrame = frameState.frameNumber; - // } else if (reachedSkippingThreshold(tileset, tile)) { - // loadTile(tileset, tile, false, frameState); - // touch(tileset, tile, frameState); - // tile._touchedFrame = frameState.frameNumber; - // } else { - // touch(tileset, tile, frameState); - // tile._touchedFrame = frameState.frameNumber; - // } - // } } // TODO : ignoring loadSiblings right now } From b44933d751b501e7f62e4e5ad2ea25e778f2c103 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 21 Mar 2018 16:19:43 -0400 Subject: [PATCH 07/53] Fix bug with skipping. Solid --- Source/Scene/Cesium3DTile.js | 1 + Source/Scene/Cesium3DTileset.js | 9 ++++++++- Source/Scene/Cesium3DTilesetTraversal.js | 19 ++++++++++++------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index e39dd5fdb8b3..ffb89050c24c 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -325,6 +325,7 @@ define([ this._updatedVisibilityFrame = 0; this._touchedFrame = 0; this._selectedFrame = 0; + this._requestedFrame = 0; this._ancestorWithContent = undefined; this._ancestorWithContentAvailable = undefined; this._refines = false; diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 0e1452e0d670..0d49bcda5df9 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -1436,10 +1436,16 @@ define([ }); } + function sortRequestByPriority(a, b) { + return a._priority - b._priority; + } + function requestTiles(tileset) { - // TODO : sort requested tiles by priority here so we can avoid requests from being cancelled excessively + // Sort requests by priority before making any requests. + // This makes it less likely that requests will be cancelled after being issued. var requestedTiles = tileset._requestedTiles; var length = requestedTiles.length; + requestedTiles.sort(sortRequestByPriority); for (var i = 0; i < length; ++i) { requestContent(tileset, requestedTiles[i]); } @@ -1821,6 +1827,7 @@ define([ } if (outOfCore) { + this._requestedTiles.length = 0; Cesium3DTilesetTraversal.selectTiles(this, frameState); requestTiles(this); processTiles(this, frameState); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 48eace509ef1..cdfb9acc577e 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -61,7 +61,6 @@ define([ tileset._desiredTiles.length = 0; tileset._selectedTiles.length = 0; - tileset._requestedTiles.length = 0; tileset._selectedTilesToStyle.length = 0; tileset._hasMixedContent = false; @@ -312,7 +311,11 @@ define([ var parent = tile.parent; if (defined(parent)) { - tile._ancestorWithContent = !hasEmptyContent(parent) ? parent : parent._ancestorWithContent; + // ancestorWithContent is an ancestor that has content or has the potential to have + // content. Used in conjunction with tileset.skipLevels to know when to skip a tile. + // ancestorWithContentAvailable is an ancestor that we can render if a desired tile is not loaded. + var hasContent = !hasUnloadedContent(parent) || (parent._requestedFrame === frameState.frameNumber); + tile._ancestorWithContent = hasContent ? parent : parent._ancestorWithContent; tile._ancestorWithContentAvailable = parent.contentAvailable ? parent : parent._ancestorWithContentAvailable; } } @@ -348,7 +351,8 @@ define([ return; } - if (tile.contentUnloaded || tile.contentExpired) { + if (hasUnloadedContent(tile) || tile.contentExpired) { + tile._requestedFrame = frameState.frameNumber; tile._priority = getPriority(tile, useParentPriority); tileset._requestedTiles.push(tile); } @@ -426,7 +430,7 @@ define([ function getVisibility(tileset, tile, maximumScreenSpaceError, frameState) { updateVisibility(tileset, tile, frameState); - // Not visible. Visibility is updated in its parent's call to updateChildren. + // Not visible if (tile._visibilityPlaneMask === CullingVolume.MASK_OUTSIDE) { return false; } @@ -471,6 +475,10 @@ define([ return tile.hasEmptyContent || tile.hasTilesetContent; } + function hasUnloadedContent(tile) { + return !hasEmptyContent(tile) && tile.contentUnloaded; + } + function executeBaseTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, leaves, frameState) { // TODO wording // Depth-first traversal that loads all tiles that are visible and don't meet the screen space error. @@ -590,13 +598,11 @@ define([ } function reachedSkippingThreshold(tileset, tile) { - // Look at the nearest ancestor that wasn't skipped. It doesn't need to be loaded yet. var ancestor = tile._ancestorWithContent; var skipLevels = tileset.skipLevelOfDetail ? tileset.skipLevels : 0; var skipScreenSpaceErrorFactor = tileset.skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; return !tileset.immediatelyLoadDesiredLevelOfDetail && - tile.contentUnloaded && defined(ancestor) && (tile._screenSpaceError < (ancestor._screenSpaceError / skipScreenSpaceErrorFactor)) && (tile._depth > (ancestor._depth + skipLevels)); @@ -630,7 +636,6 @@ define([ loadTile(tileset, tile, false, frameState); } else if (reachedSkippingThreshold(tileset, tile)) { loadTile(tileset, tile, false, frameState); - traverse = false; } // Always touch tiles. Even tiles that are skipped should stay loaded. touch(tileset, tile, frameState); From b975f1e980d20f4b1993e0596d3d71cc62d71a90 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 22 Mar 2018 13:51:48 -0400 Subject: [PATCH 08/53] Combine base and skip traversal together --- Source/Scene/Cesium3DTilesetTraversal.js | 343 +++++++++++------------ 1 file changed, 160 insertions(+), 183 deletions(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index cdfb9acc577e..c11a8a5b8592 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -26,17 +26,23 @@ define([ SceneMode) { 'use strict'; - var baseTraversal = { + /** + * @private + */ + function Cesium3DTilesetTraversal() { + } + + var traversal = { stack : new ManagedArray(), stackMaximumLength : 0 }; - var internalBaseTraversal = { + var emptyTraversal = { stack : new ManagedArray(), stackMaximumLength : 0 }; - var skipTraversal = { + var descendantTraversal = { stack : new ManagedArray(), stackMaximumLength : 0 }; @@ -48,12 +54,6 @@ define([ ancestorStackMaximumLength : 0 }; - /** - * @private - */ - function Cesium3DTilesetTraversal() { - } - Cesium3DTilesetTraversal.selectTiles = function(tileset, frameState) { if (tileset.debugFreezeFrame) { return; @@ -66,77 +66,68 @@ define([ tileset._cache.reset(); + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + var root = tileset._root; - var rootVisible = updateVisibility(tileset, root, frameState); + var rootVisible = updateTile(tileset, root, frameState); if (!rootVisible) { return; } // The SSE of not rendering the tree is small enough that the tree does not need to be rendered - if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= tileset._maximumScreenSpaceError) { + if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= maximumScreenSpaceError) { return; } - if (!tileset._skipLevelOfDetail) {//} || tileset._allTilesAdditive) { - selectBaseTraversal(tileset, root, frameState); + if (!tileset._skipLevelOfDetail) {//} || tileset._allTilesAdditive) { TODO + executeBaseTraversal(tileset, root, frameState); } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { - selectSkipTraversal(tileset, root, frameState); + executeSkipTraversal(tileset, root, frameState); } else { - selectBaseAndSkipTraversal(tileset, root, frameState); + executeBaseAndSkipTraversal(tileset, root, frameState); } - // TODO : these aren't really correct because min/max never get reset. tileset._desiredTiles.trim(); - baseTraversal.stack.trim(baseTraversal.stackMaximumLength); - internalBaseTraversal.stack.trim(internalBaseTraversal.stackMaximumLength); - skipTraversal.stack.trim(skipTraversal.stackMaximumLength); + traversal.stack.trim(traversal.stackMaximumLength); + emptyTraversal.stack.trim(emptyTraversal.stackMaximumLength); + descendantTraversal.stack.trim(descendantTraversal.stackMaximumLength); selectionTraversal.stack.trim(selectionTraversal.stackMaximumLength); selectionTraversal.ancestorStack.trim(selectionTraversal.ancestorStackMaximumLength); }; - function selectBaseTraversal(tileset, root, frameState) { + function executeBaseTraversal(tileset, root, frameState) { + var baseScreenSpaceError = tileset._maximumScreenSpaceError; var maximumScreenSpaceError = tileset._maximumScreenSpaceError; - executeBaseTraversal(tileset, root, maximumScreenSpaceError, maximumScreenSpaceError, undefined, frameState); - - // Select desired tiles - var desiredTiles = tileset._desiredTiles; - var length = desiredTiles.length; - for (var i = 0; i < length; ++i) { - selectTile(tileset, desiredTiles.get(i), frameState); - } + executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, true, frameState); + selectDesiredTiles(tileset, frameState); } - function selectSkipTraversal(tileset, root, frameState) { - // Start the skip traversal at the root - skipTraversal.stack.push(root); - - // Execute the skip traversal - executeSkipTraversal(tileset, tileset._maximumScreenSpaceError, frameState); - - // Mark tiles for selection or their nearest loaded ancestor + function executeSkipTraversal(tileset, root, frameState) { + var baseScreenSpaceError = Number.MAX_VALUE; + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, false, frameState); markDesiredTilesForSelection(tileset, frameState); - - // Sort selected tiles by distance to camera and call selectTile on each traverseAndSelect(tileset, root, frameState); } - function selectBaseAndSkipTraversal(tileset, root, frameState) { + function executeBaseAndSkipTraversal(tileset, root, frameState) { var baseScreenSpaceError = Math.max(tileset.baseScreenSpaceError, tileset.maximumScreenSpaceError); var maximumScreenSpaceError = tileset.maximumScreenSpaceError; - - // Load and select tiles without skipping up to baseScreenSpaceError - // Leaves of the base traversal is where we start the skip traversal - executeBaseTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, skipTraversal.stack, frameState); - - executeSkipTraversal(tileset, maximumScreenSpaceError, frameState); - - // Mark tiles for selection or their nearest loaded ancestor + executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, false, frameState); markDesiredTilesForSelection(tileset, frameState); - - // Sort selected tiles by distance to camera and call selectTile on each traverseAndSelect(tileset, root, frameState); } + function selectDesiredTiles(tileset, frameState) { + var desiredTiles = tileset._desiredTiles; + var length = desiredTiles.length; + for (var i = 0; i < length; ++i) { + var tile = desiredTiles.get(i); + markForSelection(tileset, tile, frameState); + selectTile(tileset, tile, frameState); + } + } + function markDesiredTilesForSelection(tileset, frameState) { var tiles = tileset._desiredTiles; var length = tiles.length; @@ -145,13 +136,34 @@ define([ var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; if (defined(loadedTile)) { markForSelection(tileset, loadedTile, frameState); + } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { + // If no ancestors are ready traverse down and select tiles to minimize empty regions. + // This happens often in cases where the camera zooms out and we're waiting for parent tiles to load. + markDescendantsForSelection(tileset, tile, frameState); } } } - function selectTile(tileset, tile, frameState) { - if (tile._selectedFrame === frameState.frameNumber) { - tileset._selectedTiles.push(tile); + function markDescendantsForSelection(tileset, root, frameState) { + var stack = descendantTraversal.stack; + stack.push(root); + while (stack.length > 0) { + descendantTraversal.stackMaximumLength = Math.max(descendantTraversal.stackMaximumLength, stack.length); + var tile = stack.pop(); + var children = tile.children; + var childrenLength = children.length; + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + if (child.contentAvailable) { + updateTile(tileset, child, frameState); + touchTile(tileset, child, frameState); + markForSelection(tileset, child, frameState); + tile._touchedFrame = frameState.frameNumber; + } else if (child._depth - root._depth < 2) { + // Continue traversing, but not too far + stack.push(child); + } + } } } @@ -178,6 +190,14 @@ define([ } } + function selectTile(tileset, tile, frameState) { + if (tile._selectedFrame === frameState.frameNumber) { + tileset._selectedTiles.push(tile); + } else { + console.log('TODO : why am i here'); + } + } + function sortChildrenByDistanceToCamera(a, b) { // Sort by farthest child first since this is going on a stack if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { @@ -188,8 +208,8 @@ define([ } /** - * Traverse the tree while tiles are visible and check if their selected frame is the current frame. - * If so, add it to a selection queue. + * Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue. + * Tiles are sorted near to far so we can take advantage of early Z. * Furthermore, this is a preorder traversal so children tiles are selected before ancestor tiles. * * The reason for the preorder traversal is so that tiles can easily be marked with their @@ -199,7 +219,7 @@ define([ * * We want to select children before their ancestors because there is no guarantee on the relationship between * the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top - * of ancestor regardless of true depth. The stencil tests used require children to be drawn first. @see {@link updateTiles} + * of ancestor regardless of true depth. The stencil tests used require children to be drawn first. * * NOTE: this will no longer work when there is a chain of selected tiles that is longer than the size of the * stencil buffer (usually 8 bits). In other words, the subset of the tree containing only selected tiles must be @@ -275,23 +295,9 @@ define([ (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); } - // function addDesiredTile(tileset, tile, frameState) { - // if (tile._touchedFrame === frameState.frameNumber) { - // return; - // } - // - // tileset._desiredTiles.push(tile); - // } - function addDesiredTile(tileset, tile, frameState) { - var desiredTiles = tileset._desiredTiles; - var length = desiredTiles.length; - - for (var i = 0; i < length; ++i) { - var other = desiredTiles[i]; - if (other === tile) { - return; - } + if (tile._touchedFrame === frameState.frameNumber) { + return; } tileset._desiredTiles.push(tile); @@ -303,24 +309,9 @@ define([ } ++tileset._statistics.visited; - tile._finalResolution = false; - tile._ancestorWithContent = undefined; - tile._ancestorWithContentAvailable = undefined; - - tile.updateExpiration(); - - var parent = tile.parent; - if (defined(parent)) { - // ancestorWithContent is an ancestor that has content or has the potential to have - // content. Used in conjunction with tileset.skipLevels to know when to skip a tile. - // ancestorWithContentAvailable is an ancestor that we can render if a desired tile is not loaded. - var hasContent = !hasUnloadedContent(parent) || (parent._requestedFrame === frameState.frameNumber); - tile._ancestorWithContent = hasContent ? parent : parent._ancestorWithContent; - tile._ancestorWithContentAvailable = parent.contentAvailable ? parent : parent._ancestorWithContentAvailable; - } } - function touch(tileset, tile, frameState) { + function touchTile(tileset, tile, frameState) { if (tile._touchedFrame === frameState.frameNumber) { return; } @@ -427,7 +418,7 @@ define([ return anyVisible; } - function getVisibility(tileset, tile, maximumScreenSpaceError, frameState) { + function getVisibility(tileset, tile, frameState) { updateVisibility(tileset, tile, frameState); // Not visible @@ -442,7 +433,7 @@ define([ // Use parent's geometric error with child's box to see if we already meet the SSE var parent = tile.parent; - if (defined(parent) && (parent.refine === Cesium3DTileRefine.ADD) && getScreenSpaceError(tileset, parent.geometricError, tile, frameState) <= maximumScreenSpaceError) { + if (defined(parent) && (parent.refine === Cesium3DTileRefine.ADD) && getScreenSpaceError(tileset, parent.geometricError, tile, frameState) <= tileset._maximumScreenSpaceError) { return false; } @@ -461,14 +452,31 @@ define([ return true; } - function updateTile(tileset, tile, maximumScreenSpaceError, frameState) { + function updateTile(tileset, tile, frameState) { if (tile._touchedFrame === frameState.frameNumber) { return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; } - var visible = getVisibility(tileset, tile, maximumScreenSpaceError, frameState); + var visible = getVisibility(tileset, tile, frameState); tile._visibilityPlaneMask = visible ? tile._visibilityPlaneMask : CullingVolume.MASK_OUTSIDE; - return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; + + tile._finalResolution = false; + tile._ancestorWithContent = undefined; + tile._ancestorWithContentAvailable = undefined; + + tile.updateExpiration(); + + var parent = tile.parent; + if (defined(parent)) { + // ancestorWithContent is an ancestor that has content or has the potential to have + // content. Used in conjunction with tileset.skipLevels to know when to skip a tile. + // ancestorWithContentAvailable is an ancestor that we can render if a desired tile is not loaded. + var hasContent = !hasUnloadedContent(parent) || (parent._requestedFrame === frameState.frameNumber); + tile._ancestorWithContent = hasContent ? parent : parent._ancestorWithContent; + tile._ancestorWithContentAvailable = parent.contentAvailable ? parent : parent._ancestorWithContentAvailable; + } + + return visible; } function hasEmptyContent(tile) { @@ -479,90 +487,116 @@ define([ return !hasEmptyContent(tile) && tile.contentUnloaded; } - function executeBaseTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, leaves, frameState) { + function inBaseTraversal(tile, baseScreenSpaceError, maximumScreenSpaceError) { + // TODO : what sse would be passed if only base traversal is used? + // TODO : what is maximumScreenSpaceError is 0. Que paso? + if (baseScreenSpaceError === maximumScreenSpaceError) { + return true; + } + + if (tile._screenSpaceError === 0) { + var parent = tile.parent; + if (defined(parent)) { + return inBaseTraversal(parent, baseScreenSpaceError, maximumScreenSpaceError); + } + return true; + } + + return tile._screenSpaceError > baseScreenSpaceError; + } + + function executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, baseTraversalOnly, frameState) { // TODO wording // Depth-first traversal that loads all tiles that are visible and don't meet the screen space error. // For replacement refinement: selects tiles to render that can't refine further - either because // their children aren't loaded yet or they meet the screen space error. // For additive refinement: select all tiles to render - var stack = baseTraversal.stack; + var stack = traversal.stack; stack.push(root); while (stack.length > 0) { - baseTraversal.stackMaximumLength = Math.max(baseTraversal.stackMaximumLength, stack.length); + traversal.stackMaximumLength = Math.max(traversal.stackMaximumLength, stack.length); var tile = stack.pop(); + var baseTraversal = inBaseTraversal(tile, baseScreenSpaceError, maximumScreenSpaceError); var add = tile.refine === Cesium3DTileRefine.ADD; var replace = tile.refine === Cesium3DTileRefine.REPLACE; var children = tile.children; var childrenLength = children.length; - var contentAvailable = tile.contentAvailable; - var traverse = (childrenLength > 0) && (tile._screenSpaceError > baseScreenSpaceError); var parent = tile.parent; - var parentRefines = !defined(parent) || parent._refines; + var parentRefines = !baseTraversalOnly || !defined(parent) || parent._refines; + var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); var refines = traverse && parentRefines; + visitTile(tileset, tile, frameState); + if (traverse) { for (var i = 0; i < childrenLength; ++i) { var child = children[i]; - var visible = updateTile(tileset, child, maximumScreenSpaceError, frameState); + var visible = updateTile(tileset, child, frameState); if (visible) { stack.push(child); } - if (replace && !hasEmptyContent(tile)) { + if (replace && baseTraversalOnly && baseTraversal && !hasEmptyContent(tile)) { // Check if the parent can refine to this child. If the child is empty we need to traverse further to // load all descendants with content. Keep non-visible children loaded since they are still needed before the parent can refine. // We don't do this for empty tiles because it looks better if children stream in as they are loaded to fill the empty space. if (!visible) { loadTile(tileset, child, true, frameState); - touch(tileset, child, frameState); + touchTile(tileset, child, frameState); child._touchedFrame = frameState.frameNumber; } - // Always run the internal base traversal even if we already know we can't refine. This keeps the tiles loaded while we wait to refine. - var refinesToChild = hasEmptyContent(child) ? executeInternalBaseTraversal(tileset, child, baseScreenSpaceError, maximumScreenSpaceError, frameState) : child.contentAvailable; + var refinesToChild = hasEmptyContent(child) ? executeEmptyTraversal(tileset, child, baseScreenSpaceError, maximumScreenSpaceError, frameState) : child.contentAvailable; refines = refines && refinesToChild; } } } - // TODO : optimization of rendering visible children even if the parent hasn't refined - - if (!defined(leaves) && replace && contentAvailable && !refines && parentRefines) { - // Replacement tiles that have loaded content but can't refine further are desired - // leaves is undefined if we are just running the base traversal - addDesiredTile(tileset, tile, frameState); + if (replace) { + if (baseTraversal) { + // Always load tiles in the base traversal + // Select tiles that can't refine further + loadTile(tileset, tile, true, frameState); + if (!refines && parentRefines && tile.contentAvailable) { + addDesiredTile(tileset, tile, frameState); + } + } else { + // Load tiles that are not skipped or can't refine further. In practice roughly half the tiles are unloaded. + // Select tiles that can't refine further. If the tile doesn't have loaded content it will try to select an ancestor with loaded content instead. + if (!refines) { + addDesiredTile(tileset, tile, frameState); + loadTile(tileset, tile, false, frameState); + } else if (reachedSkippingThreshold(tileset, tile)) { + loadTile(tileset, tile, false, frameState); + } + } } - if (add && contentAvailable) { - // Additive tiles that have loaded content are always desired - addDesiredTile(tileset, tile, frameState); + if (add) { + // Additive tiles are always loaded and selected + if (tile.contentAvailable) { + addDesiredTile(tileset, tile, frameState); + } + loadTile(tileset, tile, false, frameState); } - // if (defined(leaves) && !traverse && (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError)) { - // // If the tile cannot traverse further in the base traversal but can traverse further in the skip traversal, add it to leaves - // // leaves is undefined if we are just running the base traversal - // leaves.push(tile); - // } - - visitTile(tileset, tile, frameState); - loadTile(tileset, tile, true, frameState); - touch(tileset, tile, frameState); + touchTile(tileset, tile, frameState); tile._touchedFrame = frameState.frameNumber; tile._refines = refines; } } - function executeInternalBaseTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState) { + function executeEmptyTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState) { // Depth-first traversal that checks if all nearest descendants with content are loaded. Ignores visibility. // The reason we need to implement a full traversal is to handle chains of empty tiles. var allDescendantsLoaded = true; - var stack = internalBaseTraversal.stack; + var stack = emptyTraversal.stack; stack.push(root); while (stack.length > 0) { - internalBaseTraversal.stackMaximumLength = Math.max(internalBaseTraversal.stackMaximumLength, stack.length); + emptyTraversal.stackMaximumLength = Math.max(emptyTraversal.stackMaximumLength, stack.length); var tile = stack.pop(); var children = tile.children; @@ -577,12 +611,12 @@ define([ allDescendantsLoaded = false; } - var visible = updateTile(tileset, tile, maximumScreenSpaceError, frameState); + var visible = updateTile(tileset, tile, frameState); if (!visible) { // Load tiles that aren't visible since they are still needed for the parent to refine // Tiles that are visible will get loaded from within executeBaseTraversal loadTile(tileset, tile, true, frameState); - touch(tileset, tile, frameState); + touchTile(tileset, tile, frameState); tile._touchedFrame = frameState.frameNumber; } @@ -608,62 +642,5 @@ define([ (tile._depth > (ancestor._depth + skipLevels)); } - function executeSkipTraversal(tileset, maximumScreenSpaceError, frameState) { - // Depth-first traversal that skips tiles until in reaches the skipping threshold. - var i; - - var stack = skipTraversal.stack; - - // TODO : possibly remove this - stack.length = 0; - stack.push(tileset._root); - - while (stack.length > 0) { - skipTraversal.stackMaximumLength = Math.max(skipTraversal.stackMaximumLength, stack.length); - - var tile = stack.pop(); - var add = tile.refine === Cesium3DTileRefine.ADD; - var replace = tile.refine === Cesium3DTileRefine.REPLACE; - var children = tile.children; - var childrenLength = children.length; - var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); - - visitTile(tileset, tile, frameState); - - if (replace) { - if (!traverse) { - addDesiredTile(tileset, tile, frameState); - loadTile(tileset, tile, false, frameState); - } else if (reachedSkippingThreshold(tileset, tile)) { - loadTile(tileset, tile, false, frameState); - } - // Always touch tiles. Even tiles that are skipped should stay loaded. - touch(tileset, tile, frameState); - tile._touchedFrame = frameState.frameNumber; - } - - if (add) { - // Additive tiles are always desired - if (tile.contentAvailable) { - addDesiredTile(tileset, tile, frameState); - } - loadTile(tileset, tile, false, frameState); - touch(tileset, tile, frameState); - tile._touchedFrame = frameState.frameNumber; - } - - if (traverse) { - for (i = 0; i < childrenLength; ++i) { - var child = children[i]; - var visible = updateTile(tileset, child, maximumScreenSpaceError, frameState); - if (visible) { - stack.push(child); - } - } - } - } - // TODO : ignoring loadSiblings right now - } - return Cesium3DTilesetTraversal; }); From 5a36e0b13ca1e06659880481b0d2ccdbff6f12a1 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 22 Mar 2018 16:22:01 -0400 Subject: [PATCH 09/53] Remove touched frame checks --- Source/Scene/Cesium3DTilesetTraversal.js | 43 ++++++------------------ 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index c11a8a5b8592..3e2bf3662a81 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -158,7 +158,6 @@ define([ updateTile(tileset, child, frameState); touchTile(tileset, child, frameState); markForSelection(tileset, child, frameState); - tile._touchedFrame = frameState.frameNumber; } else if (child._depth - root._depth < 2) { // Continue traversing, but not too far stack.push(child); @@ -296,27 +295,16 @@ define([ } function addDesiredTile(tileset, tile, frameState) { - if (tile._touchedFrame === frameState.frameNumber) { - return; - } - tileset._desiredTiles.push(tile); } - function visitTile(tileset, tile, frameState) { - if (tile._touchedFrame === frameState.frameNumber) { - return; - } - + function visitTile(tileset) { ++tileset._statistics.visited; } function touchTile(tileset, tile, frameState) { - if (tile._touchedFrame === frameState.frameNumber) { - return; - } - tileset._cache.touch(tile); + tile._touchedFrame = frameState.frameNumber; } function getPriority(tile, useParentPriority) { @@ -329,19 +317,15 @@ define([ var parent = tile.parent; var replace = tile.refine === Cesium3DTileRefine.REPLACE; var add = tile.refine === Cesium3DTileRefine.ADD; - //if (add) { + if (add) { return tile._distanceToCamera; - //} else if (replace) { - // var priority = (defined(parent) && (useParentPriority || tile._screenSpaceError === 0.0)) ? parent._screenSpaceError : tile._screenSpaceError; - // return 100000.0 - priority; // TODO : doing this just because RequestScheduler wants lower priority - //} + } else if (replace) { + var priority = (defined(parent) && (useParentPriority || tile._screenSpaceError === 0.0)) ? parent._screenSpaceError : tile._screenSpaceError; + return 100000.0 - priority; // TODO : doing this just because RequestScheduler wants lower priority + } } function loadTile(tileset, tile, useParentPriority, frameState) { - if (tile._touchedFrame === frameState.frameNumber) { - return; - } - if (hasUnloadedContent(tile) || tile.contentExpired) { tile._requestedFrame = frameState.frameNumber; tile._priority = getPriority(tile, useParentPriority); @@ -453,10 +437,6 @@ define([ } function updateTile(tileset, tile, frameState) { - if (tile._touchedFrame === frameState.frameNumber) { - return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; - } - var visible = getVisibility(tileset, tile, frameState); tile._visibilityPlaneMask = visible ? tile._visibilityPlaneMask : CullingVolume.MASK_OUTSIDE; @@ -490,6 +470,8 @@ define([ function inBaseTraversal(tile, baseScreenSpaceError, maximumScreenSpaceError) { // TODO : what sse would be passed if only base traversal is used? // TODO : what is maximumScreenSpaceError is 0. Que paso? + // TODO : problem with yellow boxes when we don't want them to be yellow + // TODO : if skip traversal always look root if (baseScreenSpaceError === maximumScreenSpaceError) { return true; } @@ -528,7 +510,7 @@ define([ var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); var refines = traverse && parentRefines; - visitTile(tileset, tile, frameState); + visitTile(tileset); if (traverse) { for (var i = 0; i < childrenLength; ++i) { @@ -537,14 +519,13 @@ define([ if (visible) { stack.push(child); } - if (replace && baseTraversalOnly && baseTraversal && !hasEmptyContent(tile)) { + if (replace && baseTraversalOnly && !hasEmptyContent(tile)) { // Check if the parent can refine to this child. If the child is empty we need to traverse further to // load all descendants with content. Keep non-visible children loaded since they are still needed before the parent can refine. // We don't do this for empty tiles because it looks better if children stream in as they are loaded to fill the empty space. if (!visible) { loadTile(tileset, child, true, frameState); touchTile(tileset, child, frameState); - child._touchedFrame = frameState.frameNumber; } // Always run the internal base traversal even if we already know we can't refine. This keeps the tiles loaded while we wait to refine. var refinesToChild = hasEmptyContent(child) ? executeEmptyTraversal(tileset, child, baseScreenSpaceError, maximumScreenSpaceError, frameState) : child.contentAvailable; @@ -582,7 +563,6 @@ define([ } touchTile(tileset, tile, frameState); - tile._touchedFrame = frameState.frameNumber; tile._refines = refines; } } @@ -617,7 +597,6 @@ define([ // Tiles that are visible will get loaded from within executeBaseTraversal loadTile(tileset, tile, true, frameState); touchTile(tileset, tile, frameState); - tile._touchedFrame = frameState.frameNumber; } if (traverse) { From 66f977de871129fce94e72394a0054ede6318b81 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 22 Mar 2018 18:07:13 -0400 Subject: [PATCH 10/53] Remove desiredTiles --- Source/Scene/Cesium3DTileset.js | 1 - Source/Scene/Cesium3DTilesetTraversal.js | 87 +++++++++--------------- 2 files changed, 32 insertions(+), 56 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 0d49bcda5df9..03f4e1a2004f 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -183,7 +183,6 @@ define([ this._processingQueue = []; this._selectedTiles = []; this._requestedTiles = []; - this._desiredTiles = new ManagedArray(); this._selectedTilesToStyle = []; this._loadTimestamp = undefined; this._timeSinceLoad = 0.0; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 3e2bf3662a81..5c2e18b15baf 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -59,7 +59,6 @@ define([ return; } - tileset._desiredTiles.length = 0; tileset._selectedTiles.length = 0; tileset._selectedTilesToStyle.length = 0; tileset._hasMixedContent = false; @@ -87,7 +86,6 @@ define([ executeBaseAndSkipTraversal(tileset, root, frameState); } - tileset._desiredTiles.trim(); traversal.stack.trim(traversal.stackMaximumLength); emptyTraversal.stack.trim(emptyTraversal.stackMaximumLength); descendantTraversal.stack.trim(descendantTraversal.stackMaximumLength); @@ -99,14 +97,12 @@ define([ var baseScreenSpaceError = tileset._maximumScreenSpaceError; var maximumScreenSpaceError = tileset._maximumScreenSpaceError; executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, true, frameState); - selectDesiredTiles(tileset, frameState); } function executeSkipTraversal(tileset, root, frameState) { var baseScreenSpaceError = Number.MAX_VALUE; var maximumScreenSpaceError = tileset._maximumScreenSpaceError; executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, false, frameState); - markDesiredTilesForSelection(tileset, frameState); traverseAndSelect(tileset, root, frameState); } @@ -114,36 +110,9 @@ define([ var baseScreenSpaceError = Math.max(tileset.baseScreenSpaceError, tileset.maximumScreenSpaceError); var maximumScreenSpaceError = tileset.maximumScreenSpaceError; executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, false, frameState); - markDesiredTilesForSelection(tileset, frameState); traverseAndSelect(tileset, root, frameState); } - function selectDesiredTiles(tileset, frameState) { - var desiredTiles = tileset._desiredTiles; - var length = desiredTiles.length; - for (var i = 0; i < length; ++i) { - var tile = desiredTiles.get(i); - markForSelection(tileset, tile, frameState); - selectTile(tileset, tile, frameState); - } - } - - function markDesiredTilesForSelection(tileset, frameState) { - var tiles = tileset._desiredTiles; - var length = tiles.length; - for (var i = 0; i < length; ++i) { - var tile = tiles.get(i); - var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; - if (defined(loadedTile)) { - markForSelection(tileset, loadedTile, frameState); - } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { - // If no ancestors are ready traverse down and select tiles to minimize empty regions. - // This happens often in cases where the camera zooms out and we're waiting for parent tiles to load. - markDescendantsForSelection(tileset, tile, frameState); - } - } - } - function markDescendantsForSelection(tileset, root, frameState) { var stack = descendantTraversal.stack; stack.push(root); @@ -189,15 +158,12 @@ define([ } } - function selectTile(tileset, tile, frameState) { - if (tile._selectedFrame === frameState.frameNumber) { - tileset._selectedTiles.push(tile); - } else { - console.log('TODO : why am i here'); - } + function selectTile(tileset, tile) { + tileset._selectedTiles.push(tile); } function sortChildrenByDistanceToCamera(a, b) { + // TODO : sort during normal traversal // Sort by farthest child first since this is going on a stack if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { return b._centerZDepth - a._centerZDepth; @@ -245,7 +211,7 @@ define([ if (waitingTile === lastAncestor) { waitingTile._finalResolution = true; } - selectTile(tileset, waitingTile, frameState); + selectTile(tileset, waitingTile); continue; } } @@ -265,7 +231,7 @@ define([ if (markedForSelection) { if (add) { tile._finalResolution = true; - selectTile(tileset, tile, frameState); + selectTile(tileset, tile); } else { tile._selectionDepth = ancestorStack.length; if (tile._selectionDepth > 0) { @@ -274,7 +240,7 @@ define([ lastAncestor = tile; if (childrenLength === 0) { tile._finalResolution = true; - selectTile(tileset, tile, frameState); + selectTile(tileset, tile); continue; } ancestorStack.push(tile); @@ -294,8 +260,20 @@ define([ (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); } - function addDesiredTile(tileset, tile, frameState) { - tileset._desiredTiles.push(tile); + function addDesiredTile(tileset, tile, baseTraversalOnly, frameState) { + if (baseTraversalOnly) { + markForSelection(tileset, tile, frameState); + selectTile(tileset, tile); + } else { + var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; + if (defined(loadedTile)) { + markForSelection(tileset, loadedTile, frameState); + } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { + // If no ancestors are ready traverse down and select tiles to minimize empty regions. + // This happens often in cases where the camera zooms out and we're waiting for parent tiles to load. + markDescendantsForSelection(tileset, tile, frameState); + } + } } function visitTile(tileset) { @@ -307,28 +285,27 @@ define([ tile._touchedFrame = frameState.frameNumber; } - function getPriority(tile, useParentPriority) { - // TODO : we want priority of base traversal to be SSE based so that it refines fast... - // TODO : doesn't really matter though if we have skiplods on, though it does help a bit - // TODO : somehow that means we need some comparison of distance and sse + function getPriority(tileset, tile, useParentPriority) { // The base traversal sets useParentPriority to true so that child tiles can load as soon as possible so that their parent can refine sooner. - // Additive tiles always load based on distance because it subjectively looks better - // TODO : what to do about tileset with heavy mix of replace and add. The priorities will differ a lot. + // Additive tiles always load based on distance because it subjectively looks better. + // There may be issues with mixing additive and replacement tileset since SSE and distance are not using the same overall scale. + // Maybe all priorities need to be normalized to 0-1 range. var parent = tile.parent; var replace = tile.refine === Cesium3DTileRefine.REPLACE; var add = tile.refine === Cesium3DTileRefine.ADD; if (add) { return tile._distanceToCamera; } else if (replace) { - var priority = (defined(parent) && (useParentPriority || tile._screenSpaceError === 0.0)) ? parent._screenSpaceError : tile._screenSpaceError; - return 100000.0 - priority; // TODO : doing this just because RequestScheduler wants lower priority + var screenSpaceError = (defined(parent) && (useParentPriority || (tile._screenSpaceError === 0.0))) ? parent._screenSpaceError : tile._screenSpaceError; + var rootScreenSpaceError = tileset._root._screenSpaceError; + return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower priority values (higher priority) } } function loadTile(tileset, tile, useParentPriority, frameState) { if (hasUnloadedContent(tile) || tile.contentExpired) { tile._requestedFrame = frameState.frameNumber; - tile._priority = getPriority(tile, useParentPriority); + tile._priority = getPriority(tileset, tile, useParentPriority); tileset._requestedTiles.push(tile); } } @@ -540,13 +517,13 @@ define([ // Select tiles that can't refine further loadTile(tileset, tile, true, frameState); if (!refines && parentRefines && tile.contentAvailable) { - addDesiredTile(tileset, tile, frameState); + addDesiredTile(tileset, tile, baseTraversalOnly, frameState); } } else { - // Load tiles that are not skipped or can't refine further. In practice roughly half the tiles are unloaded. + // Load tiles that are not skipped or can't refine further. In practice roughly half the tiles stay unloaded. // Select tiles that can't refine further. If the tile doesn't have loaded content it will try to select an ancestor with loaded content instead. if (!refines) { - addDesiredTile(tileset, tile, frameState); + addDesiredTile(tileset, tile, baseTraversalOnly, frameState); loadTile(tileset, tile, false, frameState); } else if (reachedSkippingThreshold(tileset, tile)) { loadTile(tileset, tile, false, frameState); @@ -557,7 +534,7 @@ define([ if (add) { // Additive tiles are always loaded and selected if (tile.contentAvailable) { - addDesiredTile(tileset, tile, frameState); + addDesiredTile(tileset, tile, baseTraversalOnly, frameState); } loadTile(tileset, tile, false, frameState); } From e0a2a86853d592cd9ac71169daba1fce5c7a080e Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 23 Mar 2018 11:17:54 -0400 Subject: [PATCH 11/53] Better handling of processing tiles --- Source/Scene/Cesium3DTileset.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 03f4e1a2004f..1e0eedff1934 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -28,6 +28,7 @@ define([ './Axis', './Cesium3DTile', './Cesium3DTileColorBlendMode', + './Cesium3DTileContentState', './Cesium3DTileOptimizations', './Cesium3DTileRefine', './Cesium3DTilesetCache', @@ -74,6 +75,7 @@ define([ Axis, Cesium3DTile, Cesium3DTileColorBlendMode, + Cesium3DTileContentState, Cesium3DTileOptimizations, Cesium3DTileRefine, Cesium3DTilesetCache, @@ -1461,16 +1463,13 @@ define([ function removeFromProcessingQueue(tileset, tile) { return function() { - var index = tileset._processingQueue.indexOf(tile); - if (index === -1) { + if (tile._contentState === Cesium3DTileContentState.FAILED) { // Not in processing queue // For example, when a url request fails and the ready promise is rejected --tileset._statistics.numberOfPendingRequests; return; } - // Remove from processing queue - tileset._processingQueue.splice(index, 1); --tileset._statistics.numberOfTilesProcessing; if (tile.hasRenderableContent) { @@ -1484,15 +1483,34 @@ define([ }; } + function filterProcessingQueue(tileset) { + var tiles = tileset._processingQueue; + var length = tiles.length; + + var removeCount = 0; + for (var i = 0; i < length; ++i) { + var tile = tiles[i]; + if (tile._contentState !== Cesium3DTileContentState.PROCESSING) { + ++removeCount; + continue; + } + if (removeCount > 0) { + tiles[i - removeCount] = tile; + } + } + tiles.length -= removeCount; + } + function processTiles(tileset, frameState) { var tiles = tileset._processingQueue; var length = tiles.length; // Process tiles in the PROCESSING state so they will eventually move to the READY state. - // Traverse backwards in case a tile is removed as a result of calling process() - for (var i = length - 1; i >= 0; --i) { + for (var i = 0; i < length; ++i) { tiles[i].process(tileset, frameState); } + + filterProcessingQueue(tileset); } /////////////////////////////////////////////////////////////////////////// From dbdd7ef6a245899558e57a2b5db16792c6346451 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 26 Mar 2018 11:51:55 -0400 Subject: [PATCH 12/53] Sort children by distance in main traversal --- Source/Scene/Cesium3DTilesetTraversal.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 5c2e18b15baf..84b903da11a8 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -163,7 +163,6 @@ define([ } function sortChildrenByDistanceToCamera(a, b) { - // TODO : sort during normal traversal // Sort by farthest child first since this is going on a stack if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { return b._centerZDepth - a._centerZDepth; @@ -226,7 +225,6 @@ define([ var markedForSelection = tile._selectedFrame === frameState.frameNumber; var children = tile.children; var childrenLength = children.length; - children.sort(sortChildrenByDistanceToCamera); if (markedForSelection) { if (add) { @@ -264,6 +262,8 @@ define([ if (baseTraversalOnly) { markForSelection(tileset, tile, frameState); selectTile(tileset, tile); + tile._finalResolution = true; + console.log(tile._priority); } else { var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; if (defined(loadedTile)) { @@ -490,6 +490,7 @@ define([ visitTile(tileset); if (traverse) { + children.sort(sortChildrenByDistanceToCamera); for (var i = 0; i < childrenLength; ++i) { var child = children[i]; var visible = updateTile(tileset, child, frameState); From 2894551a041f96f8630a05a2eacdb4308e13a5d7 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 26 Mar 2018 13:35:24 -0400 Subject: [PATCH 13/53] Select empty tiles so their debug bounding volumes get drawn --- Source/Scene/Cesium3DTile.js | 3 +- Source/Scene/Cesium3DTileset.js | 2 +- Source/Scene/Cesium3DTilesetTraversal.js | 79 ++++++++++++------------ 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index ffb89050c24c..fada5afe598c 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -979,11 +979,12 @@ define([ function applyDebugSettings(tile, tileset, frameState) { var hasContentBoundingVolume = defined(tile._header.content) && defined(tile._header.content.boundingVolume); + var empty = tile.hasEmptyContent || tile.hasTilesetContent; var showVolume = tileset.debugShowBoundingVolume || (tileset.debugShowContentBoundingVolume && !hasContentBoundingVolume); if (showVolume) { if (!defined(tile._debugBoundingVolume)) { - var color = tile._finalResolution ? (hasContentBoundingVolume ? Color.WHITE : Color.RED) : Color.YELLOW; + var color = tile._finalResolution ? ((hasContentBoundingVolume || empty) ? Color.WHITE : Color.RED) : Color.YELLOW; tile._debugBoundingVolume = tile._boundingVolume.createDebugVolume(color); } tile._debugBoundingVolume.update(frameState); diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 1e0eedff1934..24dbd7a909de 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -587,7 +587,7 @@ define([ * This property is for debugging only; it is not optimized for production use. *

* When true, renders the bounding volume for each visible tile. The bounding volume is - * white if the tile has a content bounding volume; otherwise, it is red. Tiles that don't meet the + * white if the tile has a content bounding volume or is empty; otherwise, it is red. Tiles that don't meet the * screen space error and are still refining to their descendants are yellow. *

* diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 84b903da11a8..83f8fbca6262 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -113,7 +113,7 @@ define([ traverseAndSelect(tileset, root, frameState); } - function markDescendantsForSelection(tileset, root, frameState) { + function selectDescendants(tileset, root, frameState) { var stack = descendantTraversal.stack; stack.push(root); while (stack.length > 0) { @@ -126,7 +126,7 @@ define([ if (child.contentAvailable) { updateTile(tileset, child, frameState); touchTile(tileset, child, frameState); - markForSelection(tileset, child, frameState); + selectTile(tileset, child, frameState); } else if (child._depth - root._depth < 2) { // Continue traversing, but not too far stack.push(child); @@ -143,22 +143,23 @@ define([ // There may also be a tight box around just the tile's contents, e.g., for a city, we may be // zoomed into a neighborhood and can cull the skyscrapers in the root tile. if (tile.contentAvailable && contentVisible(tile, frameState)) { - var tileContent = tile.content; - if (tileContent.featurePropertiesDirty) { - // A feature's property in this tile changed, the tile needs to be re-styled. - tileContent.featurePropertiesDirty = false; - tile.lastStyleTime = 0; // Force applying the style to this tile - tileset._selectedTilesToStyle.push(tile); - } else if ((tile._selectedFrame !== frameState.frameNumber - 1)) { - // Tile is newly selected; it is selected this frame, but was not selected last frame. - tile.lastStyleTime = 0; // Force applying the style to this tile - tileset._selectedTilesToStyle.push(tile); - } tile._selectedFrame = frameState.frameNumber; } } - function selectTile(tileset, tile) { + function selectTile(tileset, tile, frameState) { + var tileContent = tile.content; + if (tileContent.featurePropertiesDirty) { + // A feature's property in this tile changed, the tile needs to be re-styled. + tileContent.featurePropertiesDirty = false; + tile.lastStyleTime = 0; // Force applying the style to this tile + tileset._selectedTilesToStyle.push(tile); + } else if ((tile._selectedFrame !== frameState.frameNumber - 1)) { + // Tile is newly selected; it is selected this frame, but was not selected last frame. + tile.lastStyleTime = 0; // Force applying the style to this tile + tileset._selectedTilesToStyle.push(tile); + } + tile._selectedFrame = frameState.frameNumber; tileset._selectedTiles.push(tile); } @@ -210,7 +211,7 @@ define([ if (waitingTile === lastAncestor) { waitingTile._finalResolution = true; } - selectTile(tileset, waitingTile); + selectTile(tileset, waitingTile, frameState); continue; } } @@ -221,29 +222,24 @@ define([ continue; } - var add = tile.refine === Cesium3DTileRefine.ADD; - var markedForSelection = tile._selectedFrame === frameState.frameNumber; + var replace = tile.refine === Cesium3DTileRefine.REPLACE; + var markedForSelection = replace && tile.contentAvailable && (tile._selectedFrame === frameState.frameNumber); var children = tile.children; var childrenLength = children.length; if (markedForSelection) { - if (add) { + tile._selectionDepth = ancestorStack.length; + if (tile._selectionDepth > 0) { + tileset._hasMixedContent = true; + } + lastAncestor = tile; + if (childrenLength === 0) { tile._finalResolution = true; - selectTile(tileset, tile); - } else { - tile._selectionDepth = ancestorStack.length; - if (tile._selectionDepth > 0) { - tileset._hasMixedContent = true; - } - lastAncestor = tile; - if (childrenLength === 0) { - tile._finalResolution = true; - selectTile(tileset, tile); - continue; - } - ancestorStack.push(tile); - tile._stackLength = stack.length; + selectTile(tileset, tile, frameState); + continue; } + ancestorStack.push(tile); + tile._stackLength = stack.length; } for (var i = 0; i < childrenLength; ++i) { @@ -258,12 +254,10 @@ define([ (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); } - function addDesiredTile(tileset, tile, baseTraversalOnly, frameState) { - if (baseTraversalOnly) { - markForSelection(tileset, tile, frameState); - selectTile(tileset, tile); + function addDesiredTile(tileset, tile, final, frameState) { + if (final) { + selectTile(tileset, tile, frameState); tile._finalResolution = true; - console.log(tile._priority); } else { var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; if (defined(loadedTile)) { @@ -271,7 +265,7 @@ define([ } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { // If no ancestors are ready traverse down and select tiles to minimize empty regions. // This happens often in cases where the camera zooms out and we're waiting for parent tiles to load. - markDescendantsForSelection(tileset, tile, frameState); + selectDescendants(tileset, tile, frameState); } } } @@ -524,7 +518,7 @@ define([ // Load tiles that are not skipped or can't refine further. In practice roughly half the tiles stay unloaded. // Select tiles that can't refine further. If the tile doesn't have loaded content it will try to select an ancestor with loaded content instead. if (!refines) { - addDesiredTile(tileset, tile, baseTraversalOnly, frameState); + addDesiredTile(tileset, tile, false, frameState); loadTile(tileset, tile, false, frameState); } else if (reachedSkippingThreshold(tileset, tile)) { loadTile(tileset, tile, false, frameState); @@ -535,11 +529,16 @@ define([ if (add) { // Additive tiles are always loaded and selected if (tile.contentAvailable) { - addDesiredTile(tileset, tile, baseTraversalOnly, frameState); + addDesiredTile(tileset, tile, true, frameState); } loadTile(tileset, tile, false, frameState); } + if (hasEmptyContent(tile)) { + // Select empty tiles so that we can see their debug bounding volumes + addDesiredTile(tileset, tile, true, frameState); + } + touchTile(tileset, tile, frameState); tile._refines = refines; } From b9219db9acc422374baf9c166f732e9d12665808 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 26 Mar 2018 14:24:01 -0400 Subject: [PATCH 14/53] Limit selection traversal by SSE and visibility --- Source/Scene/Cesium3DTilesetTraversal.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 83f8fbca6262..eeaa97a191d0 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -194,6 +194,7 @@ define([ * selected tiles must be no deeper than 15. This is still very unlikely. */ function traverseAndSelect(tileset, root, frameState) { + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; var stack = selectionTraversal.stack; var ancestorStack = selectionTraversal.ancestorStack; var lastAncestor; @@ -226,6 +227,7 @@ define([ var markedForSelection = replace && tile.contentAvailable && (tile._selectedFrame === frameState.frameNumber); var children = tile.children; var childrenLength = children.length; + var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); if (markedForSelection) { tile._selectionDepth = ancestorStack.length; @@ -242,9 +244,13 @@ define([ tile._stackLength = stack.length; } - for (var i = 0; i < childrenLength; ++i) { - var child = children[i]; - stack.push(child); + if (traverse) { + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + if (isVisible(child)) { + stack.push(child); + } + } } } } @@ -430,6 +436,10 @@ define([ return visible; } + function isVisible(tile) { + return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; + } + function hasEmptyContent(tile) { return tile.hasEmptyContent || tile.hasTilesetContent; } From 5e7e764a08b403d1135246cb42f2eedae157da2c Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 26 Mar 2018 16:26:41 -0400 Subject: [PATCH 15/53] Update tile outline properly --- Source/Scene/Cesium3DTile.js | 6 +++++- Source/Scene/Cesium3DTilesetTraversal.js | 2 +- Source/Scene/TileBoundingRegion.js | 1 + Source/Scene/TileBoundingSphere.js | 1 + Source/Scene/TileOrientedBoundingBox.js | 1 + 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index fada5afe598c..3f203616c1db 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -2,6 +2,7 @@ define([ '../Core/BoundingSphere', '../Core/Cartesian3', '../Core/Color', + '../Core/ColorGeometryInstanceAttribute', '../Core/CullingVolume', '../Core/defaultValue', '../Core/defined', @@ -36,6 +37,7 @@ define([ BoundingSphere, Cartesian3, Color, + ColorGeometryInstanceAttribute, CullingVolume, defaultValue, defined, @@ -983,11 +985,13 @@ define([ var showVolume = tileset.debugShowBoundingVolume || (tileset.debugShowContentBoundingVolume && !hasContentBoundingVolume); if (showVolume) { + var color = tile._finalResolution ? ((hasContentBoundingVolume || empty) ? Color.WHITE : Color.RED) : Color.YELLOW; if (!defined(tile._debugBoundingVolume)) { - var color = tile._finalResolution ? ((hasContentBoundingVolume || empty) ? Color.WHITE : Color.RED) : Color.YELLOW; tile._debugBoundingVolume = tile._boundingVolume.createDebugVolume(color); } tile._debugBoundingVolume.update(frameState); + var attributes = tile._debugBoundingVolume.getGeometryInstanceAttributes('outline'); + attributes.color = ColorGeometryInstanceAttribute.toValue(color, attributes.color); } else if (!showVolume && defined(tile._debugBoundingVolume)) { tile._debugBoundingVolume = tile._debugBoundingVolume.destroy(); } diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index eeaa97a191d0..5a0d987a4b0e 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -235,7 +235,7 @@ define([ tileset._hasMixedContent = true; } lastAncestor = tile; - if (childrenLength === 0) { + if (!traverse) { tile._finalResolution = true; selectTile(tileset, tile, frameState); continue; diff --git a/Source/Scene/TileBoundingRegion.js b/Source/Scene/TileBoundingRegion.js index 48ac2003f723..52686d5a3d88 100644 --- a/Source/Scene/TileBoundingRegion.js +++ b/Source/Scene/TileBoundingRegion.js @@ -352,6 +352,7 @@ define([ }); var instance = new GeometryInstance({ geometry : geometry, + id : 'outline', modelMatrix : modelMatrix, attributes : { color : ColorGeometryInstanceAttribute.fromColor(color) diff --git a/Source/Scene/TileBoundingSphere.js b/Source/Scene/TileBoundingSphere.js index ffc98d1245ad..278e11ad8a51 100644 --- a/Source/Scene/TileBoundingSphere.js +++ b/Source/Scene/TileBoundingSphere.js @@ -151,6 +151,7 @@ define([ var modelMatrix = Matrix4.fromTranslation(this.center, new Matrix4.clone(Matrix4.IDENTITY)); var instance = new GeometryInstance({ geometry : geometry, + id : 'outline', modelMatrix : modelMatrix, attributes : { color : ColorGeometryInstanceAttribute.fromColor(color) diff --git a/Source/Scene/TileOrientedBoundingBox.js b/Source/Scene/TileOrientedBoundingBox.js index bf09771654a6..c1f0ef277694 100644 --- a/Source/Scene/TileOrientedBoundingBox.js +++ b/Source/Scene/TileOrientedBoundingBox.js @@ -134,6 +134,7 @@ define([ var modelMatrix = Matrix4.fromRotationTranslation(this.boundingVolume.halfAxes, this.boundingVolume.center); var instance = new GeometryInstance({ geometry : geometry, + id : 'outline', modelMatrix : modelMatrix, attributes : { color : ColorGeometryInstanceAttribute.fromColor(color) From 63ad7c3ae611d83ecb1b527fdace137df8e43829 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 26 Mar 2018 17:16:48 -0400 Subject: [PATCH 16/53] Cleanup --- Source/Scene/Cesium3DTile.js | 1 + Source/Scene/Cesium3DTilesetTraversal.js | 303 +++++++++++------------ 2 files changed, 144 insertions(+), 160 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 3f203616c1db..3c4d61a6ecba 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -331,6 +331,7 @@ define([ this._ancestorWithContent = undefined; this._ancestorWithContentAvailable = undefined; this._refines = false; + this._selected = false; this._priority = 0.0; this._isClipped = true; this._clippingPlanesState = 0; // encapsulates (_isClipped, clippingPlanes.enabled) and number/function diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 5a0d987a4b0e..41e18add0270 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -78,7 +78,7 @@ define([ return; } - if (!tileset._skipLevelOfDetail) {//} || tileset._allTilesAdditive) { TODO + if (!tileset._skipLevelOfDetail || tileset._allTilesAdditive) { executeBaseTraversal(tileset, root, frameState); } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { executeSkipTraversal(tileset, root, frameState); @@ -96,73 +96,41 @@ define([ function executeBaseTraversal(tileset, root, frameState) { var baseScreenSpaceError = tileset._maximumScreenSpaceError; var maximumScreenSpaceError = tileset._maximumScreenSpaceError; - executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, true, frameState); + executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState); } function executeSkipTraversal(tileset, root, frameState) { var baseScreenSpaceError = Number.MAX_VALUE; var maximumScreenSpaceError = tileset._maximumScreenSpaceError; - executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, false, frameState); + executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState); traverseAndSelect(tileset, root, frameState); } function executeBaseAndSkipTraversal(tileset, root, frameState) { var baseScreenSpaceError = Math.max(tileset.baseScreenSpaceError, tileset.maximumScreenSpaceError); var maximumScreenSpaceError = tileset.maximumScreenSpaceError; - executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, false, frameState); + executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState); traverseAndSelect(tileset, root, frameState); } - function selectDescendants(tileset, root, frameState) { - var stack = descendantTraversal.stack; - stack.push(root); - while (stack.length > 0) { - descendantTraversal.stackMaximumLength = Math.max(descendantTraversal.stackMaximumLength, stack.length); - var tile = stack.pop(); - var children = tile.children; - var childrenLength = children.length; - for (var i = 0; i < childrenLength; ++i) { - var child = children[i]; - if (child.contentAvailable) { - updateTile(tileset, child, frameState); - touchTile(tileset, child, frameState); - selectTile(tileset, child, frameState); - } else if (child._depth - root._depth < 2) { - // Continue traversing, but not too far - stack.push(child); - } - } - } - } - - function markForSelection(tileset, tile, frameState) { - if (tile._selectedFrame === frameState.frameNumber) { - return; - } - - // There may also be a tight box around just the tile's contents, e.g., for a city, we may be - // zoomed into a neighborhood and can cull the skyscrapers in the root tile. + function selectTile(tileset, tile, frameState) { if (tile.contentAvailable && contentVisible(tile, frameState)) { + var tileContent = tile.content; + if (tileContent.featurePropertiesDirty) { + // A feature's property in this tile changed, the tile needs to be re-styled. + tileContent.featurePropertiesDirty = false; + tile.lastStyleTime = 0; // Force applying the style to this tile + tileset._selectedTilesToStyle.push(tile); + } else if ((tile._selectedFrame !== frameState.frameNumber - 1)) { + // Tile is newly selected; it is selected this frame, but was not selected last frame. + tile.lastStyleTime = 0; // Force applying the style to this tile + tileset._selectedTilesToStyle.push(tile); + } tile._selectedFrame = frameState.frameNumber; + tileset._selectedTiles.push(tile); } } - function selectTile(tileset, tile, frameState) { - var tileContent = tile.content; - if (tileContent.featurePropertiesDirty) { - // A feature's property in this tile changed, the tile needs to be re-styled. - tileContent.featurePropertiesDirty = false; - tile.lastStyleTime = 0; // Force applying the style to this tile - tileset._selectedTilesToStyle.push(tile); - } else if ((tile._selectedFrame !== frameState.frameNumber - 1)) { - // Tile is newly selected; it is selected this frame, but was not selected last frame. - tile.lastStyleTime = 0; // Force applying the style to this tile - tileset._selectedTilesToStyle.push(tile); - } - tile._selectedFrame = frameState.frameNumber; - tileset._selectedTiles.push(tile); - } - function sortChildrenByDistanceToCamera(a, b) { // Sort by farthest child first since this is going on a stack if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { @@ -172,102 +140,43 @@ define([ return b._distanceToCamera - a._distanceToCamera; } - /** - * Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue. - * Tiles are sorted near to far so we can take advantage of early Z. - * Furthermore, this is a preorder traversal so children tiles are selected before ancestor tiles. - * - * The reason for the preorder traversal is so that tiles can easily be marked with their - * selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed. - * This property is important for use in the stencil test because we want to render deeper tiles on top of their - * ancestors. If a tileset is very deep, the depth is unlikely to fit into the stencil buffer. - * - * We want to select children before their ancestors because there is no guarantee on the relationship between - * the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top - * of ancestor regardless of true depth. The stencil tests used require children to be drawn first. - * - * NOTE: this will no longer work when there is a chain of selected tiles that is longer than the size of the - * stencil buffer (usually 8 bits). In other words, the subset of the tree containing only selected tiles must be - * no deeper than 255. It is very, very unlikely this will cause a problem. - * - * NOTE: when the scene has inverted classification enabled, the stencil buffer will be masked to 4 bits. So, the - * selected tiles must be no deeper than 15. This is still very unlikely. - */ - function traverseAndSelect(tileset, root, frameState) { - var maximumScreenSpaceError = tileset._maximumScreenSpaceError; - var stack = selectionTraversal.stack; - var ancestorStack = selectionTraversal.ancestorStack; - var lastAncestor; + function contentVisible(tile, frameState) { + return (tile._visibilityPlaneMask === CullingVolume.MASK_INSIDE) || + (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); + } + function selectDescendants(tileset, root, frameState) { + var stack = descendantTraversal.stack; stack.push(root); - - while (stack.length > 0 || ancestorStack.length > 0) { - selectionTraversal.stackMaximumLength = Math.max(selectionTraversal.stackMaximumLength, stack.length); - selectionTraversal.ancestorStackMaximumLength = Math.max(selectionTraversal.ancestorStackMaximumLength, ancestorStack.length); - - if (ancestorStack.length > 0) { - var waitingTile = ancestorStack.get(ancestorStack.length - 1); - if (waitingTile._stackLength === stack.length) { - ancestorStack.pop(); - if (waitingTile === lastAncestor) { - waitingTile._finalResolution = true; - } - selectTile(tileset, waitingTile, frameState); - continue; - } - } - + while (stack.length > 0) { + descendantTraversal.stackMaximumLength = Math.max(descendantTraversal.stackMaximumLength, stack.length); var tile = stack.pop(); - if (!defined(tile)) { - // stack is empty but ancestorStack isn't - continue; - } - - var replace = tile.refine === Cesium3DTileRefine.REPLACE; - var markedForSelection = replace && tile.contentAvailable && (tile._selectedFrame === frameState.frameNumber); var children = tile.children; var childrenLength = children.length; - var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); - - if (markedForSelection) { - tile._selectionDepth = ancestorStack.length; - if (tile._selectionDepth > 0) { - tileset._hasMixedContent = true; - } - lastAncestor = tile; - if (!traverse) { - tile._finalResolution = true; - selectTile(tileset, tile, frameState); - continue; - } - ancestorStack.push(tile); - tile._stackLength = stack.length; - } - - if (traverse) { - for (var i = 0; i < childrenLength; ++i) { - var child = children[i]; - if (isVisible(child)) { - stack.push(child); - } + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + if (child.contentAvailable) { + updateTile(tileset, child, frameState); + touchTile(tileset, child, frameState); + selectTile(tileset, child, frameState); + } else if (child._depth - root._depth < 2) { + // Continue traversing, but not too far + stack.push(child); } } } } - function contentVisible(tile, frameState) { - return (tile._visibilityPlaneMask === CullingVolume.MASK_INSIDE) || - (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); - } - - function addDesiredTile(tileset, tile, final, frameState) { + function selectDesiredTile(tileset, tile, final, frameState) { if (final) { + // The tile can be selected right away and does not require traverseAndSelect. E.g. additive or empty tiles selectTile(tileset, tile, frameState); tile._finalResolution = true; } else { var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; if (defined(loadedTile)) { - markForSelection(tileset, loadedTile, frameState); + // Tiles marked for selection will be selected in traverseAndSelect + loadedTile._selected = true; } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { // If no ancestors are ready traverse down and select tiles to minimize empty regions. // This happens often in cases where the camera zooms out and we're waiting for parent tiles to load. @@ -417,6 +326,7 @@ define([ var visible = getVisibility(tileset, tile, frameState); tile._visibilityPlaneMask = visible ? tile._visibilityPlaneMask : CullingVolume.MASK_OUTSIDE; + tile._selected = false; tile._finalResolution = false; tile._ancestorWithContent = undefined; tile._ancestorWithContentAvailable = undefined; @@ -448,32 +358,34 @@ define([ return !hasEmptyContent(tile) && tile.contentUnloaded; } - function inBaseTraversal(tile, baseScreenSpaceError, maximumScreenSpaceError) { - // TODO : what sse would be passed if only base traversal is used? - // TODO : what is maximumScreenSpaceError is 0. Que paso? - // TODO : problem with yellow boxes when we don't want them to be yellow - // TODO : if skip traversal always look root - if (baseScreenSpaceError === maximumScreenSpaceError) { - return true; - } + function reachedSkippingThreshold(tileset, tile) { + var ancestor = tile._ancestorWithContent; + var skipLevels = tileset.skipLevelOfDetail ? tileset.skipLevels : 0; + var skipScreenSpaceErrorFactor = tileset.skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; + + return !tileset.immediatelyLoadDesiredLevelOfDetail && + defined(ancestor) && + (tile._screenSpaceError < (ancestor._screenSpaceError / skipScreenSpaceErrorFactor)) && + (tile._depth > (ancestor._depth + skipLevels)); + } + function inBaseTraversal(tile, baseScreenSpaceError) { if (tile._screenSpaceError === 0) { var parent = tile.parent; if (defined(parent)) { - return inBaseTraversal(parent, baseScreenSpaceError, maximumScreenSpaceError); + return parent._screenSpaceError > baseScreenSpaceError; } return true; } - return tile._screenSpaceError > baseScreenSpaceError; } - function executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, baseTraversalOnly, frameState) { - // TODO wording - // Depth-first traversal that loads all tiles that are visible and don't meet the screen space error. - // For replacement refinement: selects tiles to render that can't refine further - either because - // their children aren't loaded yet or they meet the screen space error. - // For additive refinement: select all tiles to render + function executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState) { + // Depth-first traversal that traverses all visible tiles and marks tiles for selection. + // If the traversal is base traversal ONLY then a tile does not refine until all children are loaded. This is the + // traditional replacement refinement approach. + // Otherwise we allow for skipping levels of the tree and rendering children and parent tiles simultaneously. + var baseTraversalOnly = baseScreenSpaceError === maximumScreenSpaceError; var stack = traversal.stack; stack.push(root); @@ -481,7 +393,7 @@ define([ traversal.stackMaximumLength = Math.max(traversal.stackMaximumLength, stack.length); var tile = stack.pop(); - var baseTraversal = inBaseTraversal(tile, baseScreenSpaceError, maximumScreenSpaceError); + var baseTraversal = baseTraversalOnly || inBaseTraversal(tile, baseScreenSpaceError); var add = tile.refine === Cesium3DTileRefine.ADD; var replace = tile.refine === Cesium3DTileRefine.REPLACE; var children = tile.children; @@ -501,7 +413,7 @@ define([ if (visible) { stack.push(child); } - if (replace && baseTraversalOnly && !hasEmptyContent(tile)) { + if (baseTraversalOnly && replace && !hasEmptyContent(tile)) { // Check if the parent can refine to this child. If the child is empty we need to traverse further to // load all descendants with content. Keep non-visible children loaded since they are still needed before the parent can refine. // We don't do this for empty tiles because it looks better if children stream in as they are loaded to fill the empty space. @@ -522,13 +434,13 @@ define([ // Select tiles that can't refine further loadTile(tileset, tile, true, frameState); if (!refines && parentRefines && tile.contentAvailable) { - addDesiredTile(tileset, tile, baseTraversalOnly, frameState); + selectDesiredTile(tileset, tile, baseTraversalOnly, frameState); } } else { // Load tiles that are not skipped or can't refine further. In practice roughly half the tiles stay unloaded. // Select tiles that can't refine further. If the tile doesn't have loaded content it will try to select an ancestor with loaded content instead. - if (!refines) { - addDesiredTile(tileset, tile, false, frameState); + if (!refines) { // eslint-disable-line + selectDesiredTile(tileset, tile, false, frameState); loadTile(tileset, tile, false, frameState); } else if (reachedSkippingThreshold(tileset, tile)) { loadTile(tileset, tile, false, frameState); @@ -539,14 +451,14 @@ define([ if (add) { // Additive tiles are always loaded and selected if (tile.contentAvailable) { - addDesiredTile(tileset, tile, true, frameState); + selectDesiredTile(tileset, tile, true, frameState); } loadTile(tileset, tile, false, frameState); } if (hasEmptyContent(tile)) { // Select empty tiles so that we can see their debug bounding volumes - addDesiredTile(tileset, tile, true, frameState); + selectDesiredTile(tileset, tile, true, frameState); } touchTile(tileset, tile, frameState); @@ -597,15 +509,86 @@ define([ return allDescendantsLoaded; } - function reachedSkippingThreshold(tileset, tile) { - var ancestor = tile._ancestorWithContent; - var skipLevels = tileset.skipLevelOfDetail ? tileset.skipLevels : 0; - var skipScreenSpaceErrorFactor = tileset.skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; + /** + * Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue. + * Tiles are sorted near to far so we can take advantage of early Z. + * Furthermore, this is a preorder traversal so children tiles are selected before ancestor tiles. + * + * The reason for the preorder traversal is so that tiles can easily be marked with their + * selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed. + * This property is important for use in the stencil test because we want to render deeper tiles on top of their + * ancestors. If a tileset is very deep, the depth is unlikely to fit into the stencil buffer. + * + * We want to select children before their ancestors because there is no guarantee on the relationship between + * the children's z-depth and the ancestor's z-depth. We cannot rely on Z because we want the child to appear on top + * of ancestor regardless of true depth. The stencil tests used require children to be drawn first. + * + * NOTE: this will no longer work when there is a chain of selected tiles that is longer than the size of the + * stencil buffer (usually 8 bits). In other words, the subset of the tree containing only selected tiles must be + * no deeper than 255. It is very, very unlikely this will cause a problem. + * + * NOTE: when the scene has inverted classification enabled, the stencil buffer will be masked to 4 bits. So, the + * selected tiles must be no deeper than 15. This is still very unlikely. + */ + function traverseAndSelect(tileset, root, frameState) { + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; + var stack = selectionTraversal.stack; + var ancestorStack = selectionTraversal.ancestorStack; + var lastAncestor; - return !tileset.immediatelyLoadDesiredLevelOfDetail && - defined(ancestor) && - (tile._screenSpaceError < (ancestor._screenSpaceError / skipScreenSpaceErrorFactor)) && - (tile._depth > (ancestor._depth + skipLevels)); + stack.push(root); + + while (stack.length > 0 || ancestorStack.length > 0) { + selectionTraversal.stackMaximumLength = Math.max(selectionTraversal.stackMaximumLength, stack.length); + selectionTraversal.ancestorStackMaximumLength = Math.max(selectionTraversal.ancestorStackMaximumLength, ancestorStack.length); + + if (ancestorStack.length > 0) { + var waitingTile = ancestorStack.get(ancestorStack.length - 1); + if (waitingTile._stackLength === stack.length) { + ancestorStack.pop(); + if (waitingTile === lastAncestor) { + waitingTile._finalResolution = true; + } + selectTile(tileset, waitingTile, frameState); + continue; + } + } + + var tile = stack.pop(); + if (!defined(tile)) { + // stack is empty but ancestorStack isn't + continue; + } + + var markedForSelection = tile._selected; + var children = tile.children; + var childrenLength = children.length; + var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); + + if (markedForSelection) { + tile._selectionDepth = ancestorStack.length; + if (tile._selectionDepth > 0) { + tileset._hasMixedContent = true; + } + lastAncestor = tile; + if (!traverse) { + tile._finalResolution = true; + selectTile(tileset, tile, frameState); + continue; + } + ancestorStack.push(tile); + tile._stackLength = stack.length; + } + + if (traverse) { + for (var i = 0; i < childrenLength; ++i) { + var child = children[i]; + if (isVisible(child)) { + stack.push(child); + } + } + } + } } return Cesium3DTilesetTraversal; From c804dd3c1d20d242ad58ec9487d3853c86a4c4b5 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Mon, 26 Mar 2018 17:43:42 -0400 Subject: [PATCH 17/53] Fix for outlines --- Source/Scene/Cesium3DTilesetTraversal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 41e18add0270..00d682ef04b9 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -114,7 +114,7 @@ define([ } function selectTile(tileset, tile, frameState) { - if (tile.contentAvailable && contentVisible(tile, frameState)) { + if (contentVisible(tile, frameState)) { var tileContent = tile.content; if (tileContent.featurePropertiesDirty) { // A feature's property in this tile changed, the tile needs to be re-styled. From a6aad571e2e79bc1948269c5ed3a96769ad50ea8 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 27 Mar 2018 18:04:34 -0400 Subject: [PATCH 18/53] Fixing tests --- Source/Scene/Cesium3DTile.js | 2 +- Source/Scene/Cesium3DTileset.js | 3 +- Source/Scene/Cesium3DTilesetTraversal.js | 17 +++++-- Specs/Cesium3DTilesTester.js | 1 + .../Tilesets/TilesetOfTilesets/tileset2.json | 2 +- .../TilesetOfTilesets/tileset3/tileset3.json | 2 +- Specs/Scene/Cesium3DTilesetSpec.js | 50 +++++++++++-------- 7 files changed, 46 insertions(+), 31 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 3c4d61a6ecba..73da68df57c4 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -838,7 +838,7 @@ define([ * Computes the distance from the center of the tile's bounding volume to the camera. * * @param {FrameState} frameState The frame state. - * @returns {Number} The distance, in meters, or zero if the camera is inside the bounding volume. + * @returns {Number} The distance, in meters. * * @private */ diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 24dbd7a909de..e0c947ca6958 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -1502,6 +1502,7 @@ define([ } function processTiles(tileset, frameState) { + filterProcessingQueue(tileset); var tiles = tileset._processingQueue; var length = tiles.length; @@ -1509,8 +1510,6 @@ define([ for (var i = 0; i < length; ++i) { tiles[i].process(tileset, frameState); } - - filterProcessingQueue(tileset); } /////////////////////////////////////////////////////////////////////////// diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 00d682ef04b9..7be94d1d604e 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -261,10 +261,10 @@ define([ } var parent = tile.parent; - var parentTransorm = defined(parent) ? parent.computedTransform : tileset._modelMatrix; + var parentTransform = defined(parent) ? parent.computedTransform : tileset._modelMatrix; var parentVisibilityPlaneMask = defined(parent) ? parent._visibilityPlaneMask : CullingVolume.MASK_INDETERMINATE; - tile.updateTransform(parentTransorm); + tile.updateTransform(parentTransform); tile._distanceToCamera = tile.distanceToTile(frameState); tile._centerZDepth = tile.distanceToTileCenter(frameState); tile._screenSpaceError = getScreenSpaceError(tileset, tile.geometricError, tile, frameState); @@ -346,6 +346,15 @@ define([ return visible; } + function updateChildren(tileset, tile, frameState) { + var children = tile.children; + var length = children.length; + for (var i = 0; i < length; ++i) { + updateTile(tileset, children[i], frameState); + } + children.sort(sortChildrenByDistanceToCamera); + } + function isVisible(tile) { return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; } @@ -406,10 +415,10 @@ define([ visitTile(tileset); if (traverse) { - children.sort(sortChildrenByDistanceToCamera); + updateChildren(tileset, tile, frameState); for (var i = 0; i < childrenLength; ++i) { var child = children[i]; - var visible = updateTile(tileset, child, frameState); + var visible = isVisible(child); if (visible) { stack.push(child); } diff --git a/Specs/Cesium3DTilesTester.js b/Specs/Cesium3DTilesTester.js index 9c6e6476593b..1ba742bc8ae1 100644 --- a/Specs/Cesium3DTilesTester.js +++ b/Specs/Cesium3DTilesTester.js @@ -97,6 +97,7 @@ define([ scene.renderForSpecs(); return tileset.tilesLoaded; }).then(function() { + scene.renderForSpecs(); return tileset; }); }; diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json index e2e4b8f7a4c4..855d5fcad7cb 100644 --- a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json @@ -31,7 +31,7 @@ 20 ] }, - "geometricError": 0, + "geometricError": 70, "content": { "url": "tileset3/tileset3.json" } diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/tileset3.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/tileset3.json index 76266f647b3c..9ac1abf95d6b 100644 --- a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/tileset3.json +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset3/tileset3.json @@ -2,7 +2,7 @@ "asset": { "version": "1.0" }, - "geometricError": 0, + "geometricError": 70, "root": { "boundingVolume": { "region": [ diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index f24d67e4da0a..5b0ad50cd672 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -197,6 +197,10 @@ defineSuite([ setZoom(5.0); } + function isSelected(tileset, tile) { + return tileset._selectedTiles.indexOf(tile) > -1; + } + it('throws with undefined url', function() { expect(function() { return new Cesium3DTileset(); @@ -1005,26 +1009,28 @@ defineSuite([ }); it('replacement refinement - selects tile when inside viewer request volume', function() { - return Cesium3DTilesTester.loadTileset(scene, tilesetWithViewerRequestVolumeUrl).then(function(tileset) { + var options = { + skipLevelOfDetail : false + } + return Cesium3DTilesTester.loadTileset(scene, tilesetWithViewerRequestVolumeUrl, options).then(function(tileset) { var statistics = tileset._statistics; var root = tileset._root; root.refine = Cesium3DTileRefine.REPLACE; - // Force root tile to always not meet SSE since this is just checking the request volume - tileset.maximumScreenSpaceError = 0.0; + root.hasRenderableContent = true; // mock content + tileset.maximumScreenSpaceError = 0.0; // Force root tile to always not meet SSE since this is just checking the request volume // Renders all 5 tiles setZoom(20.0); scene.renderForSpecs(); expect(statistics.numberOfCommands).toEqual(5); - expect(root.selected).toBe(false); + expect(isSelected(tileset, root)).toBe(false); // No longer renders the tile with a request volume setZoom(1500.0); - root.contentAvailable = true; // mock content scene.renderForSpecs(); expect(statistics.numberOfCommands).toEqual(4); - expect(root.selected).toBe(true); // one child is no longer selected. root is chosen instead + expect(isSelected(tileset, root)).toBe(true); // one child is no longer selected. root is chosen instead }); }); @@ -1106,7 +1112,7 @@ defineSuite([ expect(statistics.numberOfPendingRequests).toEqual(4); // Loading child content tiles return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { - expect(root.selected).toEqual(false); + expect(isSelected(tileset, root)).toEqual(false); expect(statistics.numberOfCommands).toEqual(4); // Render child content tiles }); }); @@ -1144,7 +1150,7 @@ defineSuite([ expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).toEqual(CullingVolume.MASK_OUTSIDE); expect(tileset._selectedTiles.length).toEqual(0); - expect(childRoot.selected).toBe(false); + expect(isSelected(tileset, childRoot)).toBe(false); }); }); @@ -1163,7 +1169,7 @@ defineSuite([ expect(childRoot.children[2].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); - expect(childRoot.selected).toBe(false); + expect(isSelected(tileset, childRoot)).toBe(false); }); }); @@ -1183,7 +1189,7 @@ defineSuite([ expect(childRoot.children[2].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); - expect(childRoot.selected).toBe(true); + expect(isSelected(tileset, childRoot)).toBe(true); }); }); }); @@ -1202,7 +1208,7 @@ defineSuite([ expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); expect(tileset._selectedTiles.length).toEqual(1); - expect(childRoot.selected).toBe(true); + expect(isSelected(tileset, childRoot)).toBe(true); }); }); @@ -1222,12 +1228,12 @@ defineSuite([ expect(childRoot.children[3].visibility(scene.frameState, CullingVolume.MASK_INDETERMINATE)).not.toEqual(CullingVolume.MASK_OUTSIDE); expect(tileset._selectedTiles.length).toEqual(1); - expect(childRoot.selected).toBe(true); + expect(isSelected(tileset, childRoot)).toBe(true); childRoot.geometricError = 200; scene.renderForSpecs(); expect(tileset._selectedTiles.length).toEqual(4); - expect(childRoot.selected).toBe(false); + expect(isSelected(tileset, childRoot)).toBe(false); }); }); }); @@ -2579,9 +2585,9 @@ defineSuite([ // 2 for root tile, 1 for child, 1 for stencil clear // Tiles that are marked as finalResolution, including leaves, do not create back face commands expect(statistics.numberOfCommands).toEqual(4); - expect(root.selected).toBe(true); + expect(isSelected(tileset, root)).toBe(true); expect(root._finalResolution).toBe(false); - expect(root.children[0].children[0].children[3].selected).toBe(true); + expect(isSelected(tileset, root.children[0].children[0].children[3])).toBe(true); expect(root.children[0].children[0].children[3]._finalResolution).toBe(true); expect(tileset._hasMixedContent).toBe(true); @@ -2607,12 +2613,12 @@ defineSuite([ // 2 for root tile, 1 for child, 1 for stencil clear expect(statistics.numberOfCommands).toEqual(1); - expect(root.selected).toBe(true); + expect(isSelected(tileset, root)).toBe(true); expect(root._finalResolution).toBe(true); - expect(root.children[0].children[0].children[0].selected).toBe(false); - expect(root.children[0].children[0].children[1].selected).toBe(false); - expect(root.children[0].children[0].children[2].selected).toBe(false); - expect(root.children[0].children[0].children[3].selected).toBe(false); + expect(isSelected(tileset, root.children[0].children[0].children[0])).toBe(false); + expect(isSelected(tileset, root.children[0].children[0].children[1])).toBe(false); + expect(isSelected(tileset, root.children[0].children[0].children[2])).toBe(false); + expect(isSelected(tileset, root.children[0].children[0].children[3])).toBe(false); expect(tileset._hasMixedContent).toBe(false); }); }); @@ -2691,8 +2697,8 @@ defineSuite([ expect(child.contentReady).toBe(true); expect(parent.contentReady).toBe(false); - expect(child.selected).toBe(true); - expect(parent.selected).toBe(false); + expect(isSelected(tileset, child)).toBe(true); + expect(isSelected(tileset, parent)).toBe(false); expect(statistics.numberOfCommands).toEqual(1); }); }); From 4bbacf886d00412125cb1fbe911b9fa2c0e3520c Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 28 Mar 2018 09:39:52 -0400 Subject: [PATCH 19/53] Cleanup --- .../Scene/Cesium3DTileChildrenVisibility.js | 19 ------------------- Source/Scene/Cesium3DTilesetTraversal.js | 1 - Specs/Scene/Cesium3DTilesetSpec.js | 3 ++- 3 files changed, 2 insertions(+), 21 deletions(-) delete mode 100644 Source/Scene/Cesium3DTileChildrenVisibility.js diff --git a/Source/Scene/Cesium3DTileChildrenVisibility.js b/Source/Scene/Cesium3DTileChildrenVisibility.js deleted file mode 100644 index ddfd279b147d..000000000000 --- a/Source/Scene/Cesium3DTileChildrenVisibility.js +++ /dev/null @@ -1,19 +0,0 @@ -define([ - '../Core/freezeObject' - ], function( - freezeObject) { - 'use strict'; - - /** - * @private - */ - var Cesium3DTileChildrenVisibility = { - NONE : 0, // No children visible - VISIBLE : 1, // At least one child visible - IN_REQUEST_VOLUME : 2, // At least one child in viewer request volume - VISIBLE_IN_REQUEST_VOLUME : 4, // At least one child both visible and in viewer request volume - VISIBLE_NOT_IN_REQUEST_VOLUME : 8 // At least one child visible but not in viewer request volume - }; - - return freezeObject(Cesium3DTileChildrenVisibility); -}); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 7be94d1d604e..e217c69b4dbe 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -273,7 +273,6 @@ define([ var visible = visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; visible = visible && tile.insideViewerRequestVolume(frameState); tile._visibilityPlaneMask = visible ? visibilityPlaneMask : CullingVolume.MASK_OUTSIDE; - tile._updatedFrame = frameState.frameNumber; return visible; } diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 5b0ad50cd672..df258b19176a 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1011,12 +1011,13 @@ defineSuite([ it('replacement refinement - selects tile when inside viewer request volume', function() { var options = { skipLevelOfDetail : false - } + }; return Cesium3DTilesTester.loadTileset(scene, tilesetWithViewerRequestVolumeUrl, options).then(function(tileset) { var statistics = tileset._statistics; var root = tileset._root; root.refine = Cesium3DTileRefine.REPLACE; + root.hasEmptyContent = false; root.hasRenderableContent = true; // mock content tileset.maximumScreenSpaceError = 0.0; // Force root tile to always not meet SSE since this is just checking the request volume From 248c44342578b02a21d1aa9ec256fd80fed42694 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 28 Mar 2018 10:55:59 -0400 Subject: [PATCH 20/53] Better handling of request volume --- Source/Scene/Cesium3DTile.js | 3 +- Source/Scene/Cesium3DTilesetTraversal.js | 107 +++++++++++++++-------- Specs/Scene/Cesium3DTilesetSpec.js | 2 +- 3 files changed, 73 insertions(+), 39 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 73da68df57c4..cf2222031145 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -23,7 +23,6 @@ define([ '../Core/Resource', '../Core/RuntimeError', '../ThirdParty/when', - './Cesium3DTileChildrenVisibility', './Cesium3DTileContentFactory', './Cesium3DTileContentState', './Cesium3DTileOptimizationHint', @@ -58,7 +57,6 @@ define([ Resource, RuntimeError, when, - Cesium3DTileChildrenVisibility, Cesium3DTileContentFactory, Cesium3DTileContentState, Cesium3DTileOptimizationHint, @@ -318,6 +316,7 @@ define([ this._centerZDepth = 0; // TODO : remove? this._screenSpaceError = 0; this._visibilityPlaneMask = 0; + this._visibilityFlag = 0; this._finalResolution = true; this._depth = 0; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index e217c69b4dbe..a320a509443c 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -7,7 +7,6 @@ define([ '../Core/ManagedArray', '../Core/Math', '../Core/OrthographicFrustum', - './Cesium3DTileChildrenVisibility', './Cesium3DTileOptimizationHint', './Cesium3DTileRefine', './SceneMode' @@ -20,7 +19,6 @@ define([ ManagedArray, CesiumMath, OrthographicFrustum, - Cesium3DTileChildrenVisibility, Cesium3DTileOptimizationHint, Cesium3DTileRefine, SceneMode) { @@ -32,6 +30,34 @@ define([ function Cesium3DTilesetTraversal() { } + var VisibilityFlag = { + NONE : 0, + VISIBLE : 1, + IN_REQUEST_VOLUME : 2 + }; + + function isVisibleBit(flag) { + return (flag & VisibilityFlag.VISIBLE) > 0; + } + + function inRequestVolumeBit(flag) { + return (flag & VisibilityFlag.IN_REQUEST_VOLUME) > 0; + } + + function clearVisibility(tile) { + tile._visibilityFlag = tile._visibilityFlag & ~VisibilityFlag.VISIBLE; + } + + function isVisible(tile) { + var flag = tile._visibilityFlag; + return isVisibleBit(flag) && inRequestVolumeBit(flag); + } + + function isVisibleButNotInRequestVolume(tile) { + var flag = tile._visibilityFlag; + return isVisibleBit(flag) && !inRequestVolumeBit(flag); + } + var traversal = { stack : new ManagedArray(), stackMaximumLength : 0 @@ -68,8 +94,8 @@ define([ var maximumScreenSpaceError = tileset._maximumScreenSpaceError; var root = tileset._root; - var rootVisible = updateTile(tileset, root, frameState); - if (!rootVisible) { + updateTile(tileset, root, frameState); + if (!isVisible(root)) { return; } @@ -257,9 +283,11 @@ define([ function updateVisibility(tileset, tile, frameState) { if (tile._updatedVisibilityFrame === frameState.frameNumber) { - return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; + return; } + var visibilityFlag = VisibilityFlag.NONE; + var parent = tile.parent; var parentTransform = defined(parent) ? parent.computedTransform : tileset._modelMatrix; var parentVisibilityPlaneMask = defined(parent) ? parent._visibilityPlaneMask : CullingVolume.MASK_INDETERMINATE; @@ -268,42 +296,49 @@ define([ tile._distanceToCamera = tile.distanceToTile(frameState); tile._centerZDepth = tile.distanceToTileCenter(frameState); tile._screenSpaceError = getScreenSpaceError(tileset, tile.geometricError, tile, frameState); + tile._visibilityPlaneMask = tile.visibility(frameState, parentVisibilityPlaneMask); // Use parent's plane mask to speed up visibility test - var visibilityPlaneMask = tile.visibility(frameState, parentVisibilityPlaneMask); // Use parent's plane mask to speed up visibility test - var visible = visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; - visible = visible && tile.insideViewerRequestVolume(frameState); - tile._visibilityPlaneMask = visible ? visibilityPlaneMask : CullingVolume.MASK_OUTSIDE; - return visible; + if (tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE) { + visibilityFlag |= VisibilityFlag.VISIBLE; + } + + if (tile.insideViewerRequestVolume(frameState)) { + visibilityFlag |= VisibilityFlag.IN_REQUEST_VOLUME; + } + + tile._visibilityFlag = visibilityFlag; } - function updateChildrenVisibility(tileset, tile, frameState) { + function anyChildrenVisible(tileset, tile, frameState) { var anyVisible = false; var children = tile.children; var length = children.length; for (var i = 0; i < length; ++i) { - var childVisible = updateVisibility(tileset, children[i], frameState); - anyVisible = anyVisible || childVisible; + var child = children[i]; + updateVisibility(tileset, child, frameState); + anyVisible = anyVisible || isVisible(child); } return anyVisible; } - function getVisibility(tileset, tile, frameState) { + function updateTileVisibility(tileset, tile, frameState) { updateVisibility(tileset, tile, frameState); - // Not visible - if (tile._visibilityPlaneMask === CullingVolume.MASK_OUTSIDE) { - return false; + if (!isVisible(tile)) { + return; } // Don't visit an expired subtree because it will be destroyed if (tile.hasTilesetContent && tile.contentExpired) { - return false; + clearVisibility(tile); + return; } // Use parent's geometric error with child's box to see if we already meet the SSE var parent = tile.parent; if (defined(parent) && (parent.refine === Cesium3DTileRefine.ADD) && getScreenSpaceError(tileset, parent.geometricError, tile, frameState) <= tileset._maximumScreenSpaceError) { - return false; + clearVisibility(tile); + return; } // Optimization - if none of the tile's children are visible then this tile isn't visible @@ -311,19 +346,16 @@ define([ var useOptimization = tile._optimChildrenWithinParent === Cesium3DTileOptimizationHint.USE_OPTIMIZATION; var hasChildren = tile.children.length > 0; if (replace && useOptimization && hasChildren) { - var anyChildrenVisible = updateChildrenVisibility(tileset, tile, frameState); - if (!anyChildrenVisible) { + if (!anyChildrenVisible(tileset, tile, frameState)) { ++tileset._statistics.numberOfTilesCulledWithChildrenUnion; - return false; + clearVisibility(tile); + return; } } - - return true; } function updateTile(tileset, tile, frameState) { - var visible = getVisibility(tileset, tile, frameState); - tile._visibilityPlaneMask = visible ? tile._visibilityPlaneMask : CullingVolume.MASK_OUTSIDE; + updateTileVisibility(tileset, tile, frameState); tile._selected = false; tile._finalResolution = false; @@ -341,8 +373,6 @@ define([ tile._ancestorWithContent = hasContent ? parent : parent._ancestorWithContent; tile._ancestorWithContentAvailable = parent.contentAvailable ? parent : parent._ancestorWithContentAvailable; } - - return visible; } function updateChildren(tileset, tile, frameState) { @@ -354,10 +384,6 @@ define([ children.sort(sortChildrenByDistanceToCamera); } - function isVisible(tile) { - return tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; - } - function hasEmptyContent(tile) { return tile.hasEmptyContent || tile.hasTilesetContent; } @@ -429,8 +455,17 @@ define([ loadTile(tileset, child, true, frameState); touchTile(tileset, child, frameState); } - // Always run the internal base traversal even if we already know we can't refine. This keeps the tiles loaded while we wait to refine. - var refinesToChild = hasEmptyContent(child) ? executeEmptyTraversal(tileset, child, baseScreenSpaceError, maximumScreenSpaceError, frameState) : child.contentAvailable; + + var refinesToChild; + if (isVisibleButNotInRequestVolume(child)) { + refinesToChild = false; + } else if (hasEmptyContent(child)) { + // Always run the internal base traversal even if we already know we can't refine. This keeps the tiles loaded while we wait to refine. + refinesToChild = executeEmptyTraversal(tileset, child, baseScreenSpaceError, maximumScreenSpaceError, frameState); + } else { + refinesToChild = child.contentAvailable; + } + refines = refines && refinesToChild; } } @@ -498,8 +533,8 @@ define([ allDescendantsLoaded = false; } - var visible = updateTile(tileset, tile, frameState); - if (!visible) { + updateTile(tileset, tile, frameState); + if (!isVisible(tile)) { // Load tiles that aren't visible since they are still needed for the parent to refine // Tiles that are visible will get loaded from within executeBaseTraversal loadTile(tileset, tile, true, frameState); diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index df258b19176a..379327d4706c 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1017,7 +1017,7 @@ defineSuite([ var root = tileset._root; root.refine = Cesium3DTileRefine.REPLACE; - root.hasEmptyContent = false; + root.hasEmptyContent = false; // mock content root.hasRenderableContent = true; // mock content tileset.maximumScreenSpaceError = 0.0; // Force root tile to always not meet SSE since this is just checking the request volume From db216c1d731f50d496862800aafb55cc6341e7ab Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 28 Mar 2018 13:06:31 -0400 Subject: [PATCH 21/53] Include additive tiles in final selection --- Source/Scene/Cesium3DTile.js | 2 +- Source/Scene/Cesium3DTilesetTraversal.js | 91 ++++++++++++------------ Specs/Scene/Cesium3DTilesetSpec.js | 18 ++--- 3 files changed, 55 insertions(+), 56 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index cf2222031145..d42385cc022d 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -330,7 +330,7 @@ define([ this._ancestorWithContent = undefined; this._ancestorWithContentAvailable = undefined; this._refines = false; - this._selected = false; + this._shouldSelect = false; this._priority = 0.0; this._isClipped = true; this._clippingPlanesState = 0; // encapsulates (_isClipped, clippingPlanes.enabled) and number/function diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index a320a509443c..1d63a88843b8 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -195,19 +195,22 @@ define([ function selectDesiredTile(tileset, tile, final, frameState) { if (final) { - // The tile can be selected right away and does not require traverseAndSelect. E.g. additive or empty tiles - selectTile(tileset, tile, frameState); - tile._finalResolution = true; - } else { - var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; - if (defined(loadedTile)) { - // Tiles marked for selection will be selected in traverseAndSelect - loadedTile._selected = true; - } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { - // If no ancestors are ready traverse down and select tiles to minimize empty regions. - // This happens often in cases where the camera zooms out and we're waiting for parent tiles to load. - selectDescendants(tileset, tile, frameState); + if (tile.contentAvailable) { + // The tile can be selected right away and does not require traverseAndSelect + tile._finalResolution = true; + selectTile(tileset, tile, frameState); } + return; + } + + var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; + if (defined(loadedTile)) { + // Tiles will be selected in traverseAndSelect + loadedTile._shouldSelect = true; + } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { + // If no ancestors are ready traverse down and select tiles to minimize empty regions. + // This happens often in cases where the camera zooms out and we're waiting for parent tiles to load. + selectDescendants(tileset, tile, frameState); } } @@ -357,7 +360,7 @@ define([ function updateTile(tileset, tile, frameState) { updateTileVisibility(tileset, tile, frameState); - tile._selected = false; + tile._shouldSelect = false; tile._finalResolution = false; tile._ancestorWithContent = undefined; tile._ancestorWithContentAvailable = undefined; @@ -452,7 +455,7 @@ define([ // load all descendants with content. Keep non-visible children loaded since they are still needed before the parent can refine. // We don't do this for empty tiles because it looks better if children stream in as they are loaded to fill the empty space. if (!visible) { - loadTile(tileset, child, true, frameState); + loadTile(tileset, child, baseTraversalOnly, frameState); touchTile(tileset, child, frameState); } @@ -471,39 +474,33 @@ define([ } } - if (replace) { + if (hasEmptyContent(tile)) { + // Select empty tiles so that we can see their debug bounding volumes + selectTile(tileset, tile, frameState); + } else if (add) { + // Additive tiles are always loaded and selected + selectDesiredTile(tileset, tile, baseTraversalOnly, frameState); + loadTile(tileset, tile, baseTraversalOnly, frameState); + } else if (replace) { if (baseTraversal) { // Always load tiles in the base traversal // Select tiles that can't refine further - loadTile(tileset, tile, true, frameState); - if (!refines && parentRefines && tile.contentAvailable) { + loadTile(tileset, tile, baseTraversalOnly, frameState); + if (!refines && parentRefines) { selectDesiredTile(tileset, tile, baseTraversalOnly, frameState); } } else { // Load tiles that are not skipped or can't refine further. In practice roughly half the tiles stay unloaded. // Select tiles that can't refine further. If the tile doesn't have loaded content it will try to select an ancestor with loaded content instead. if (!refines) { // eslint-disable-line - selectDesiredTile(tileset, tile, false, frameState); - loadTile(tileset, tile, false, frameState); + selectDesiredTile(tileset, tile, baseTraversalOnly, frameState); + loadTile(tileset, tile, baseTraversalOnly, frameState); } else if (reachedSkippingThreshold(tileset, tile)) { - loadTile(tileset, tile, false, frameState); + loadTile(tileset, tile, baseTraversalOnly, frameState); } } } - if (add) { - // Additive tiles are always loaded and selected - if (tile.contentAvailable) { - selectDesiredTile(tileset, tile, true, frameState); - } - loadTile(tileset, tile, false, frameState); - } - - if (hasEmptyContent(tile)) { - // Select empty tiles so that we can see their debug bounding volumes - selectDesiredTile(tileset, tile, true, frameState); - } - touchTile(tileset, tile, frameState); tile._refines = refines; } @@ -603,24 +600,30 @@ define([ continue; } - var markedForSelection = tile._selected; + var add = tile.refine === Cesium3DTileRefine.ADD; + var shouldSelect = tile._shouldSelect; var children = tile.children; var childrenLength = children.length; var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); - if (markedForSelection) { - tile._selectionDepth = ancestorStack.length; - if (tile._selectionDepth > 0) { - tileset._hasMixedContent = true; - } - lastAncestor = tile; - if (!traverse) { + if (shouldSelect) { + if (add) { tile._finalResolution = true; selectTile(tileset, tile, frameState); - continue; + } else { + tile._selectionDepth = ancestorStack.length; + if (tile._selectionDepth > 0) { + tileset._hasMixedContent = true; + } + lastAncestor = tile; + if (!traverse) { + tile._finalResolution = true; + selectTile(tileset, tile, frameState); + continue; + } + ancestorStack.push(tile); + tile._stackLength = stack.length; } - ancestorStack.push(tile); - tile._stackLength = stack.length; } if (traverse) { diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 379327d4706c..7594d6b241dc 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -987,9 +987,7 @@ defineSuite([ }); it('replacement refinement - selects root when sse is not met and children are not ready', function() { - // Set view so that only root tile is loaded initially viewRootOnly(); - return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { var root = tileset._root; root.refine = Cesium3DTileRefine.REPLACE; @@ -1042,7 +1040,6 @@ defineSuite([ // E E // C C C C // - viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement1Url).then(function(tileset) { viewAllTiles(); @@ -1051,15 +1048,14 @@ defineSuite([ var statistics = tileset._statistics; var root = tileset._root; - return when.join(root.children[0].contentReadyPromise, root.children[1].contentReadyPromise).then(function() { - // Even though root's children are loaded, the grandchildren need to be loaded before it becomes refinable - expect(numberOfChildrenWithoutContent(root)).toEqual(0); // Children are loaded - expect(statistics.numberOfCommands).toEqual(1); // No stencil or backface commands; no mixed content - expect(statistics.numberOfPendingRequests).toEqual(4); // Loading grandchildren + // Even though root's children are loaded, the grandchildren need to be loaded before it becomes refinable + expect(numberOfChildrenWithoutContent(root)).toEqual(0); // Children are loaded + expect(statistics.numberOfCommands).toEqual(1); // No stencil or backface commands; no mixed content + expect(statistics.numberOfPendingRequests).toEqual(4); // Loading grandchildren - return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { - expect(statistics.numberOfCommands).toEqual(4); // Render children - }); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { + scene.renderForSpecs(); + expect(statistics.numberOfCommands).toEqual(4); // Render children }); }); }); From 1f51e9b6d6786c90022338a8c7e504a6eed70403 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 28 Mar 2018 14:04:17 -0400 Subject: [PATCH 22/53] Add empty tiles to a different selection list --- Source/Scene/Cesium3DTileset.js | 31 +++++++++++++++++++----- Source/Scene/Cesium3DTilesetTraversal.js | 9 +++++-- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index e0c947ca6958..5684d2e41f24 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -184,6 +184,7 @@ define([ this._cache = new Cesium3DTilesetCache(); this._processingQueue = []; this._selectedTiles = []; + this._emptyTiles = []; this._requestedTiles = []; this._selectedTilesToStyle = []; this._loadTimestamp = undefined; @@ -1599,8 +1600,12 @@ define([ } function updateTileDebugLabels(tileset, frameState) { + var i; + var tile; var selectedTiles = tileset._selectedTiles; - var length = selectedTiles.length; + var selectedLength = selectedTiles.length; + var emptyTiles = tileset._emptyTiles; + var emptyLength = emptyTiles.length; tileset._tileDebugLabels.removeAll(); if (tileset.debugPickedTileLabelOnly) { @@ -1610,10 +1615,16 @@ define([ label.pixelOffset = new Cartesian2(15, -15); // Offset to avoid picking the label. } } else { - for (var i = 0; i < length; ++i) { - var tile = selectedTiles[i]; + for (i = 0; i < selectedLength; ++i) { + tile = selectedTiles[i]; addTileDebugLabel(tile, tileset, computeTileLabelPosition(tile)); } + for (i = 0; i < emptyLength; ++i) { + tile = emptyTiles[i]; + if (tile.hasTilesetContent) { + addTileDebugLabel(tile, tileset, computeTileLabelPosition(tile)); + } + } } tileset._tileDebugLabels.update(frameState); } @@ -1630,9 +1641,12 @@ define([ var commandList = frameState.commandList; var numberOfInitialCommands = commandList.length; var selectedTiles = tileset._selectedTiles; - var length = selectedTiles.length; + var selectedLength = selectedTiles.length; + var emptyTiles = tileset._emptyTiles; + var emptyLength = emptyTiles.length; var tileVisible = tileset.tileVisible; var i; + var tile; var bivariateVisibilityTest = tileset._skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer && length > 0; @@ -1643,8 +1657,8 @@ define([ } var lengthBeforeUpdate = commandList.length; - for (i = 0; i < length; ++i) { - var tile = selectedTiles[i]; + for (i = 0; i < selectedLength; ++i) { + tile = selectedTiles[i]; // Raise the tileVisible event before update in case the tileVisible event // handler makes changes that update needs to apply to WebGL resources tileVisible.raiseEvent(tile); @@ -1652,6 +1666,11 @@ define([ statistics.incrementSelectionCounts(tile.content); ++statistics.selected; } + for (i = 0; i < emptyLength; ++i) { + tile = emptyTiles[i]; + tile.update(tileset, frameState); + } + var lengthAfterUpdate = commandList.length; var addedCommandsLength = lengthAfterUpdate - lengthBeforeUpdate; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 1d63a88843b8..867d94dde752 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -87,6 +87,7 @@ define([ tileset._selectedTiles.length = 0; tileset._selectedTilesToStyle.length = 0; + tileset._emptyTiles.length = 0; tileset._hasMixedContent = false; tileset._cache.reset(); @@ -139,6 +140,10 @@ define([ traverseAndSelect(tileset, root, frameState); } + function addEmptyTile(tileset, tile) { + tileset._emptyTiles.push(tile); + } + function selectTile(tileset, tile, frameState) { if (contentVisible(tile, frameState)) { var tileContent = tile.content; @@ -475,8 +480,8 @@ define([ } if (hasEmptyContent(tile)) { - // Select empty tiles so that we can see their debug bounding volumes - selectTile(tileset, tile, frameState); + // Add empty tiles so that we can see its debug bounding volumes + addEmptyTile(tileset, tile, frameState); } else if (add) { // Additive tiles are always loaded and selected selectDesiredTile(tileset, tile, baseTraversalOnly, frameState); From 978f9f66209d7fa0446a79fe35bb6472d1037a10 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 28 Mar 2018 16:14:30 -0400 Subject: [PATCH 23/53] Reorganization of checking refinement --- Source/Scene/Cesium3DTilesetTraversal.js | 196 +++++++++++++---------- Specs/Scene/Cesium3DTilesetSpec.js | 5 +- 2 files changed, 111 insertions(+), 90 deletions(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 867d94dde752..fd5656551c9b 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -105,7 +105,7 @@ define([ return; } - if (!tileset._skipLevelOfDetail || tileset._allTilesAdditive) { + if (!skipLevelOfDetail(tileset)) { executeBaseTraversal(tileset, root, frameState); } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { executeSkipTraversal(tileset, root, frameState); @@ -140,10 +140,21 @@ define([ traverseAndSelect(tileset, root, frameState); } + function skipLevelOfDetail(tileset) { + // Optimization: if all tiles are additive we can turn skipLevelOfDetail off and save some processing + return tileset._skipLevelOfDetail && !tileset._allTilesAdditive; + } + function addEmptyTile(tileset, tile) { + tile._finalResolution = true; tileset._emptyTiles.push(tile); } + function contentVisible(tile, frameState) { + return (tile._visibilityPlaneMask === CullingVolume.MASK_INSIDE) || + (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); + } + function selectTile(tileset, tile, frameState) { if (contentVisible(tile, frameState)) { var tileContent = tile.content; @@ -162,20 +173,6 @@ define([ } } - function sortChildrenByDistanceToCamera(a, b) { - // Sort by farthest child first since this is going on a stack - if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { - return b._centerZDepth - a._centerZDepth; - } - - return b._distanceToCamera - a._distanceToCamera; - } - - function contentVisible(tile, frameState) { - return (tile._visibilityPlaneMask === CullingVolume.MASK_INSIDE) || - (tile.contentVisibility(frameState) !== Intersect.OUTSIDE); - } - function selectDescendants(tileset, root, frameState) { var stack = descendantTraversal.stack; stack.push(root); @@ -198,8 +195,8 @@ define([ } } - function selectDesiredTile(tileset, tile, final, frameState) { - if (final) { + function selectDesiredTile(tileset, tile, frameState) { + if (!skipLevelOfDetail(tileset)) { if (tile.contentAvailable) { // The tile can be selected right away and does not require traverseAndSelect tile._finalResolution = true; @@ -208,9 +205,10 @@ define([ return; } + // If this tile is not loaded attempt to select its ancestor instead var loadedTile = tile.contentAvailable ? tile : tile._ancestorWithContentAvailable; if (defined(loadedTile)) { - // Tiles will be selected in traverseAndSelect + // Tiles will actually be selected in traverseAndSelect loadedTile._shouldSelect = true; } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { // If no ancestors are ready traverse down and select tiles to minimize empty regions. @@ -228,10 +226,10 @@ define([ tile._touchedFrame = frameState.frameNumber; } - function getPriority(tileset, tile, useParentPriority) { - // The base traversal sets useParentPriority to true so that child tiles can load as soon as possible so that their parent can refine sooner. + function getPriority(tileset, tile) { + // If skipLevelOfDetail is off we try to load child tiles as soon as possible so that their parent can refine sooner. // Additive tiles always load based on distance because it subjectively looks better. - // There may be issues with mixing additive and replacement tileset since SSE and distance are not using the same overall scale. + // There may be issues with mixing additive and replacement tiles since SSE and distance are different types of values. // Maybe all priorities need to be normalized to 0-1 range. var parent = tile.parent; var replace = tile.refine === Cesium3DTileRefine.REPLACE; @@ -239,16 +237,17 @@ define([ if (add) { return tile._distanceToCamera; } else if (replace) { - var screenSpaceError = (defined(parent) && (useParentPriority || (tile._screenSpaceError === 0.0))) ? parent._screenSpaceError : tile._screenSpaceError; + var useParentScreenSpaceError = defined(parent) && (!skipLevelOfDetail(tileset) || (tile._screenSpaceError === 0.0)); + var screenSpaceError = useParentScreenSpaceError ? parent._screenSpaceError : tile._screenSpaceError; var rootScreenSpaceError = tileset._root._screenSpaceError; return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower priority values (higher priority) } } - function loadTile(tileset, tile, useParentPriority, frameState) { + function loadTile(tileset, tile, frameState) { if (hasUnloadedContent(tile) || tile.contentExpired) { tile._requestedFrame = frameState.frameNumber; - tile._priority = getPriority(tileset, tile, useParentPriority); + tile._priority = getPriority(tileset, tile); tileset._requestedTiles.push(tile); } } @@ -383,15 +382,6 @@ define([ } } - function updateChildren(tileset, tile, frameState) { - var children = tile.children; - var length = children.length; - for (var i = 0; i < length; ++i) { - updateTile(tileset, children[i], frameState); - } - children.sort(sortChildrenByDistanceToCamera); - } - function hasEmptyContent(tile) { return tile.hasEmptyContent || tile.hasTilesetContent; } @@ -402,16 +392,77 @@ define([ function reachedSkippingThreshold(tileset, tile) { var ancestor = tile._ancestorWithContent; - var skipLevels = tileset.skipLevelOfDetail ? tileset.skipLevels : 0; - var skipScreenSpaceErrorFactor = tileset.skipLevelOfDetail ? tileset.skipScreenSpaceErrorFactor : 1.0; - return !tileset.immediatelyLoadDesiredLevelOfDetail && defined(ancestor) && - (tile._screenSpaceError < (ancestor._screenSpaceError / skipScreenSpaceErrorFactor)) && - (tile._depth > (ancestor._depth + skipLevels)); + (tile._screenSpaceError < (ancestor._screenSpaceError / tileset.skipScreenSpaceErrorFactor)) && + (tile._depth > (ancestor._depth + tileset.skipLevels)); } - function inBaseTraversal(tile, baseScreenSpaceError) { + function sortChildrenByDistanceToCamera(a, b) { + // Sort by farthest child first since this is going on a stack + if (b._distanceToCamera === 0 && a._distanceToCamera === 0) { + return b._centerZDepth - a._centerZDepth; + } + + return b._distanceToCamera - a._distanceToCamera; + } + + function updateAndPushChildren(tileset, tile, stack, frameState) { + var i; + var replace = tile.refine === Cesium3DTileRefine.REPLACE; + var children = tile.children; + var length = children.length; + + for (i = 0; i < length; ++i) { + updateTile(tileset, children[i], frameState); + } + + // Sort by distance to take advantage of early Z and reduce artifacts for skipLevelOfDetail + children.sort(sortChildrenByDistanceToCamera); + + // For traditional replacement refinement we need to check if all children are loaded before we can refine + // The reason we always refine empty tiles is it looks better if children stream in as they are loaded to fill the empty space + var checkRefines = !skipLevelOfDetail(tileset) && replace && !hasEmptyContent(parent); + var refines = true; + + var anyChildrenVisible = false; + for (i = 0; i < length; ++i) { + var child = children[i]; + var visible = isVisible(child); + if (visible) { + stack.push(child); + anyChildrenVisible = true; + } + if (checkRefines) { + if (!visible) { + // Keep non-visible children loaded since they are still needed before the parent can refine. + loadTile(tileset, child, frameState); + touchTile(tileset, child, frameState); + } + var childRefines; + if (isVisibleButNotInRequestVolume(child)) { + childRefines = false; + } else if (hasEmptyContent(child)) { + // We need to traverse past any empty tiles to know if we can refine + childRefines = executeEmptyTraversal(tileset, child, frameState); + } else { + childRefines = child.contentAvailable; + } + refines = refines && childRefines; + } + } + + if (!anyChildrenVisible) { + refines = false; + } + + return refines; + } + + function inBaseTraversal(tileset, tile, baseScreenSpaceError) { + if (!skipLevelOfDetail(tileset)) { + return true; + } if (tile._screenSpaceError === 0) { var parent = tile.parent; if (defined(parent)) { @@ -424,10 +475,9 @@ define([ function executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState) { // Depth-first traversal that traverses all visible tiles and marks tiles for selection. - // If the traversal is base traversal ONLY then a tile does not refine until all children are loaded. This is the + // If skipLevelOfDetail is off then a tile does not refine until all children are loaded. This is the // traditional replacement refinement approach. // Otherwise we allow for skipping levels of the tree and rendering children and parent tiles simultaneously. - var baseTraversalOnly = baseScreenSpaceError === maximumScreenSpaceError; var stack = traversal.stack; stack.push(root); @@ -435,87 +485,58 @@ define([ traversal.stackMaximumLength = Math.max(traversal.stackMaximumLength, stack.length); var tile = stack.pop(); - var baseTraversal = baseTraversalOnly || inBaseTraversal(tile, baseScreenSpaceError); + var baseTraversal = inBaseTraversal(tile, baseScreenSpaceError); var add = tile.refine === Cesium3DTileRefine.ADD; var replace = tile.refine === Cesium3DTileRefine.REPLACE; var children = tile.children; var childrenLength = children.length; var parent = tile.parent; - var parentRefines = !baseTraversalOnly || !defined(parent) || parent._refines; + var parentRefines = !defined(parent) || parent._refines; var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); - var refines = traverse && parentRefines; - - visitTile(tileset); + var refines = false; if (traverse) { - updateChildren(tileset, tile, frameState); - for (var i = 0; i < childrenLength; ++i) { - var child = children[i]; - var visible = isVisible(child); - if (visible) { - stack.push(child); - } - if (baseTraversalOnly && replace && !hasEmptyContent(tile)) { - // Check if the parent can refine to this child. If the child is empty we need to traverse further to - // load all descendants with content. Keep non-visible children loaded since they are still needed before the parent can refine. - // We don't do this for empty tiles because it looks better if children stream in as they are loaded to fill the empty space. - if (!visible) { - loadTile(tileset, child, baseTraversalOnly, frameState); - touchTile(tileset, child, frameState); - } - - var refinesToChild; - if (isVisibleButNotInRequestVolume(child)) { - refinesToChild = false; - } else if (hasEmptyContent(child)) { - // Always run the internal base traversal even if we already know we can't refine. This keeps the tiles loaded while we wait to refine. - refinesToChild = executeEmptyTraversal(tileset, child, baseScreenSpaceError, maximumScreenSpaceError, frameState); - } else { - refinesToChild = child.contentAvailable; - } - - refines = refines && refinesToChild; - } - } + refines = updateAndPushChildren(tileset, tile, stack, frameState); } if (hasEmptyContent(tile)) { - // Add empty tiles so that we can see its debug bounding volumes + // Add empty tile so we can see its debug bounding volumes addEmptyTile(tileset, tile, frameState); } else if (add) { // Additive tiles are always loaded and selected - selectDesiredTile(tileset, tile, baseTraversalOnly, frameState); - loadTile(tileset, tile, baseTraversalOnly, frameState); + selectDesiredTile(tileset, tile, frameState); + loadTile(tileset, tile, frameState); } else if (replace) { if (baseTraversal) { // Always load tiles in the base traversal // Select tiles that can't refine further - loadTile(tileset, tile, baseTraversalOnly, frameState); + loadTile(tileset, tile, frameState); if (!refines && parentRefines) { - selectDesiredTile(tileset, tile, baseTraversalOnly, frameState); + selectDesiredTile(tileset, tile, frameState); } } else { // Load tiles that are not skipped or can't refine further. In practice roughly half the tiles stay unloaded. // Select tiles that can't refine further. If the tile doesn't have loaded content it will try to select an ancestor with loaded content instead. if (!refines) { // eslint-disable-line - selectDesiredTile(tileset, tile, baseTraversalOnly, frameState); - loadTile(tileset, tile, baseTraversalOnly, frameState); + selectDesiredTile(tileset, tile, frameState); + loadTile(tileset, tile, frameState); } else if (reachedSkippingThreshold(tileset, tile)) { - loadTile(tileset, tile, baseTraversalOnly, frameState); + loadTile(tileset, tile, frameState); } } } + visitTile(tileset); touchTile(tileset, tile, frameState); tile._refines = refines; } } - function executeEmptyTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState) { + function executeEmptyTraversal(tileset, root, frameState) { // Depth-first traversal that checks if all nearest descendants with content are loaded. Ignores visibility. // The reason we need to implement a full traversal is to handle chains of empty tiles. var allDescendantsLoaded = true; - + var maximumScreenSpaceError = tileset._maximumScreenSpaceError; var stack = emptyTraversal.stack; stack.push(root); @@ -527,7 +548,7 @@ define([ var childrenLength = children.length; // Only traverse if the tile is empty - we are trying to find descendants with content - var traverse = hasEmptyContent(tile) && (childrenLength > 0) && (tile._screenSpaceError > baseScreenSpaceError); + var traverse = hasEmptyContent(tile) && (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); // When we reach a "leaf" that does not have content available we know that not all descendants are loaded // i.e. there will be holes if the parent tries to refine to its children, so don't refine @@ -538,8 +559,7 @@ define([ updateTile(tileset, tile, frameState); if (!isVisible(tile)) { // Load tiles that aren't visible since they are still needed for the parent to refine - // Tiles that are visible will get loaded from within executeBaseTraversal - loadTile(tileset, tile, true, frameState); + loadTile(tileset, tile, frameState); touchTile(tileset, tile, frameState); } diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 7594d6b241dc..b9f0dd2dd457 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1191,9 +1191,10 @@ defineSuite([ }); }); - it('does select visibile tiles with visible children failing request volumes', function() { + it('does select visible tiles with visible children failing request volumes', function() { viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { + scene.renderForSpecs(); var root = tileset._root; var childRoot = root.children[0]; @@ -1209,7 +1210,7 @@ defineSuite([ }); }); - it('does select visibile tiles with visible children passing request volumes', function() { + it('does select visible tiles with visible children passing request volumes', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { var root = tileset._root; var childRoot = root.children[0]; From 582a51fc71690551db7cf28e85fc686714bc7a57 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 28 Mar 2018 16:37:15 -0400 Subject: [PATCH 24/53] Fix geometric errors on TilesetOfTilesets --- .../Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json | 2 +- .../Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json index 4b1aba30142c..98a3089d2cf5 100644 --- a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset.json @@ -32,7 +32,7 @@ 88 ] }, - "geometricError": 70, + "geometricError": 240, "refine": "ADD", "content": { "url": "tileset2.json" diff --git a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json index 855d5fcad7cb..2b7a8d55fe89 100644 --- a/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json +++ b/Specs/Data/Cesium3DTiles/Tilesets/TilesetOfTilesets/tileset2.json @@ -2,7 +2,7 @@ "asset": { "version": "1.0" }, - "geometricError": 70, + "geometricError": 240, "root": { "boundingVolume": { "region": [ From 49c005b806aff34ef3dfe9a38bfa61205b18b0bf Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 28 Mar 2018 17:59:10 -0400 Subject: [PATCH 25/53] Add back loadSiblings and fix more tests --- Source/Scene/Cesium3DTileset.js | 2 +- Source/Scene/Cesium3DTilesetTraversal.js | 17 ++++++++--------- Specs/Scene/Cesium3DTilesetSpec.js | 14 +++----------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 5684d2e41f24..4222d3196ab2 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -1648,7 +1648,7 @@ define([ var i; var tile; - var bivariateVisibilityTest = tileset._skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer && length > 0; + var bivariateVisibilityTest = tileset._skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer && selectedLength > 0; tileset._backfaceCommands.length = 0; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index fd5656551c9b..170f2f1c3083 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -422,23 +422,22 @@ define([ // For traditional replacement refinement we need to check if all children are loaded before we can refine // The reason we always refine empty tiles is it looks better if children stream in as they are loaded to fill the empty space - var checkRefines = !skipLevelOfDetail(tileset) && replace && !hasEmptyContent(parent); + var checkRefines = !skipLevelOfDetail(tileset) && replace && !hasEmptyContent(tile); var refines = true; var anyChildrenVisible = false; for (i = 0; i < length; ++i) { var child = children[i]; - var visible = isVisible(child); - if (visible) { + if (isVisible(child)) { stack.push(child); anyChildrenVisible = true; + } else if (checkRefines || tileset.loadSiblings) { + // Keep non-visible children loaded since they are still needed before the parent can refine. + // Or loadSiblings is true so we should always load tiles regardless of visibility. + loadTile(tileset, child, frameState); + touchTile(tileset, child, frameState); } if (checkRefines) { - if (!visible) { - // Keep non-visible children loaded since they are still needed before the parent can refine. - loadTile(tileset, child, frameState); - touchTile(tileset, child, frameState); - } var childRefines; if (isVisibleButNotInRequestVolume(child)) { childRefines = false; @@ -485,7 +484,7 @@ define([ traversal.stackMaximumLength = Math.max(traversal.stackMaximumLength, stack.length); var tile = stack.pop(); - var baseTraversal = inBaseTraversal(tile, baseScreenSpaceError); + var baseTraversal = inBaseTraversal(tileset, tile, baseScreenSpaceError); var add = tile.refine === Cesium3DTileRefine.ADD; var replace = tile.refine === Cesium3DTileRefine.REPLACE; var children = tile.children; diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index b9f0dd2dd457..d152aaa1bc6d 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -183,12 +183,6 @@ defineSuite([ scene.camera.moveDown(200.0); } - function viewBottomRight() { - viewAllTiles(); - scene.camera.moveRight(200.0); - scene.camera.moveDown(200.0); - } - function viewInstances() { setZoom(30.0); } @@ -1257,7 +1251,6 @@ defineSuite([ // Check that headers are equal var subtreeRoot = root.children[0]; - expect(root.geometricError).toEqual(subtreeRoot.geometricError); expect(root.refine).toEqual(subtreeRoot.refine); expect(root.contentBoundingVolume.boundingVolume).toEqual(subtreeRoot.contentBoundingVolume.boundingVolume); @@ -2556,7 +2549,6 @@ defineSuite([ it('adds stencil clear command first when unresolved', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { - tileset._root.children[0].children[0].children[0].unloadContent(); tileset._root.children[0].children[0].children[1].unloadContent(); tileset._root.children[0].children[0].children[2].unloadContent(); @@ -2570,7 +2562,6 @@ defineSuite([ it('creates duplicate backface commands', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { - var statistics = tileset._statistics; var root = tileset._root; @@ -2663,8 +2654,8 @@ defineSuite([ }); }); - xit('immediatelyLoadDesiredLevelOfDetail', function() { - viewBottomRight(); + it('immediatelyLoadDesiredLevelOfDetail', function() { + viewBottomLeft(); var tileset = scene.primitives.add(new Cesium3DTileset({ url : tilesetOfTilesetsUrl, immediatelyLoadDesiredLevelOfDetail : true @@ -2674,6 +2665,7 @@ defineSuite([ return tileset._root.contentReadyPromise.then(function() { tileset._root.refine = Cesium3DTileRefine.REPLACE; tileset._root.children[0].refine = Cesium3DTileRefine.REPLACE; + tileset._allTilesAdditive = false; return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { var statistics = tileset._statistics; expect(statistics.numberOfTilesWithContentReady).toBe(1); From 45090175328e110e5206d8804fa0dca52476da09 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 28 Mar 2018 22:17:42 -0400 Subject: [PATCH 26/53] Fix 2 tests --- Specs/Scene/Cesium3DTilesetSpec.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index d152aaa1bc6d..a7f7b412ca42 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1186,8 +1186,11 @@ defineSuite([ }); it('does select visible tiles with visible children failing request volumes', function() { + var options = { + cullWithChildrenBounds : false + }; viewRootOnly(); - return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl).then(function(tileset) { + return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl, options).then(function(tileset) { scene.renderForSpecs(); var root = tileset._root; var childRoot = root.children[0]; @@ -2641,8 +2644,7 @@ defineSuite([ it('loadSiblings', function() { viewBottomLeft(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url, { - loadSiblings : false, - baseScreenSpaceError: 1000000000 + loadSiblings : false }).then(function(tileset) { var statistics = tileset._statistics; expect(statistics.numberOfTilesWithContentReady).toBe(2); From 7d2128191e7f72835a6f6c3d12002f3ade4cb4d8 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 29 Mar 2018 10:29:48 -0400 Subject: [PATCH 27/53] Fix expiration --- Source/Scene/Cesium3DTile.js | 2 +- Source/Scene/Cesium3DTilesetTraversal.js | 19 ++++++++++--------- Specs/Scene/Cesium3DTilesetSpec.js | 4 ++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index d42385cc022d..f137ba9c01fa 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -692,7 +692,7 @@ define([ updateExpireDate(that); // Refresh style for expired content - that.lastStyleTime = 0; + that._selectedFrame = 0; that._contentState = Cesium3DTileContentState.READY; that._contentReadyPromise.resolve(content); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 170f2f1c3083..afa26439158f 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -234,7 +234,9 @@ define([ var parent = tile.parent; var replace = tile.refine === Cesium3DTileRefine.REPLACE; var add = tile.refine === Cesium3DTileRefine.ADD; - if (add) { + if (tile.hasTilesetContent) { + return 0.0; // Load external tileset as soon as possible + } else if (add) { return tile._distanceToCamera; } else if (replace) { var useParentScreenSpaceError = defined(parent) && (!skipLevelOfDetail(tileset) || (tile._screenSpaceError === 0.0)); @@ -335,12 +337,6 @@ define([ return; } - // Don't visit an expired subtree because it will be destroyed - if (tile.hasTilesetContent && tile.contentExpired) { - clearVisibility(tile); - return; - } - // Use parent's geometric error with child's box to see if we already meet the SSE var parent = tile.parent; if (defined(parent) && (parent.refine === Cesium3DTileRefine.ADD) && getScreenSpaceError(tileset, parent.geometricError, tile, frameState) <= tileset._maximumScreenSpaceError) { @@ -363,14 +359,13 @@ define([ function updateTile(tileset, tile, frameState) { updateTileVisibility(tileset, tile, frameState); + tile.updateExpiration(); tile._shouldSelect = false; tile._finalResolution = false; tile._ancestorWithContent = undefined; tile._ancestorWithContentAvailable = undefined; - tile.updateExpiration(); - var parent = tile.parent; if (defined(parent)) { // ancestorWithContent is an ancestor that has content or has the potential to have @@ -494,6 +489,11 @@ define([ var traverse = (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); var refines = false; + if (tile.hasTilesetContent && tile.contentExpired) { + // Don't traverse expired subtree because it will be destroyed + traverse = false; + } + if (traverse) { refines = updateAndPushChildren(tileset, tile, stack, frameState); } @@ -501,6 +501,7 @@ define([ if (hasEmptyContent(tile)) { // Add empty tile so we can see its debug bounding volumes addEmptyTile(tileset, tile, frameState); + loadTile(tileset, tile, frameState); } else if (add) { // Additive tiles are always loaded and selected selectDesiredTile(tileset, tile, frameState); diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index a7f7b412ca42..45a6f917a5c1 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -2877,7 +2877,11 @@ defineSuite([ tile.expireDate = JulianDate.addSeconds(JulianDate.now(), -1.0, new JulianDate()); // Stays in the expired state until the request goes through + var originalMaxmimumRequests = RequestScheduler.maximumRequests; + RequestScheduler.maximumRequests = 0; // Artificially limit Request Scheduler so the request won't go through scene.renderForSpecs(); + RequestScheduler.maximumRequests = originalMaxmimumRequests; + expect(tile.contentExpired).toBe(true); return pollToPromise(function() { From ca07240dcfeabffbbaeaffc6d5e48fb417c16945 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 29 Mar 2018 10:59:08 -0400 Subject: [PATCH 28/53] Remove unneeeded scene.renderForSpecs --- Specs/Scene/Cesium3DTilesetSpec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 45a6f917a5c1..52565adf898a 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1191,7 +1191,6 @@ defineSuite([ }; viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacementWithViewerRequestVolumeUrl, options).then(function(tileset) { - scene.renderForSpecs(); var root = tileset._root; var childRoot = root.children[0]; From fae93f7981566a532139a99a6b3b3cd58c61b45f Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 29 Mar 2018 11:55:16 -0400 Subject: [PATCH 29/53] Fix remaining tests and starting to reorg for pick pass --- Source/Scene/Cesium3DTileset.js | 9 +++++++-- Source/Scene/Cesium3DTilesetTraversal.js | 8 ++++++-- Specs/Scene/Cesium3DTileSpec.js | 1 + 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 4222d3196ab2..5571b30630b1 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -1862,8 +1862,13 @@ define([ } if (outOfCore) { - this._requestedTiles.length = 0; - Cesium3DTilesetTraversal.selectTiles(this, frameState); + this._cache.reset(); + } + + this._requestedTiles.length = 0; + Cesium3DTilesetTraversal.selectTiles(this, frameState); + + if (outOfCore) { requestTiles(this); processTiles(this, frameState); } diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index afa26439158f..e895cac8d4bb 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -85,13 +85,15 @@ define([ return; } + if (frameState.passes.pick) { + console.log('pick'); + } + tileset._selectedTiles.length = 0; tileset._selectedTilesToStyle.length = 0; tileset._emptyTiles.length = 0; tileset._hasMixedContent = false; - tileset._cache.reset(); - var maximumScreenSpaceError = tileset._maximumScreenSpaceError; var root = tileset._root; @@ -291,6 +293,7 @@ define([ } function updateVisibility(tileset, tile, frameState) { + // TODO : pick pass might be different if (tile._updatedVisibilityFrame === frameState.frameNumber) { return; } @@ -316,6 +319,7 @@ define([ } tile._visibilityFlag = visibilityFlag; + tile._updatedVisibilityFrame = frameState.frameNumber; } function anyChildrenVisible(tileset, tile, frameState) { diff --git a/Specs/Scene/Cesium3DTileSpec.js b/Specs/Scene/Cesium3DTileSpec.js index a0699cedf8fd..590031c45c21 100644 --- a/Specs/Scene/Cesium3DTileSpec.js +++ b/Specs/Scene/Cesium3DTileSpec.js @@ -317,6 +317,7 @@ defineSuite([ var scene; beforeEach(function() { scene = createScene(); + scene.frameState.passes.render = true; }); afterEach(function() { From 72ec719bac39c9f0c45cc931d3fa178b89f9c0aa Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 29 Mar 2018 13:45:40 -0400 Subject: [PATCH 30/53] Fix picking statistics --- Source/Scene/Cesium3DTilesetTraversal.js | 10 ++++------ .../Cesium3DTilesInspectorViewModel.js | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index e895cac8d4bb..5f104421a515 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -85,10 +85,6 @@ define([ return; } - if (frameState.passes.pick) { - console.log('pick'); - } - tileset._selectedTiles.length = 0; tileset._selectedTilesToStyle.length = 0; tileset._emptyTiles.length = 0; @@ -224,6 +220,9 @@ define([ } function touchTile(tileset, tile, frameState) { + if (frameState.passes.pick) { + return; + } tileset._cache.touch(tile); tile._touchedFrame = frameState.frameNumber; } @@ -293,8 +292,7 @@ define([ } function updateVisibility(tileset, tile, frameState) { - // TODO : pick pass might be different - if (tile._updatedVisibilityFrame === frameState.frameNumber) { + if (tile._updatedVisibilityFrame === frameState.frameNumber && !frameState.passes.pick) { return; } diff --git a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js index b7a6153f547d..0641d2ff10b6 100644 --- a/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js +++ b/Source/Widgets/Cesium3DTilesInspector/Cesium3DTilesInspectorViewModel.js @@ -71,7 +71,7 @@ define([ return ''; } - var statistics = tileset.statistics; + var statistics = isPick ? tileset._statisticsLastPick : tileset._statisticsLastColor; // Since the pick pass uses a smaller frustum around the pixel of interest, // the statistics will be different than the normal render pass. @@ -81,7 +81,7 @@ define([ '
  • Visited: ' + statistics.visited.toLocaleString() + '
  • ' + // Number of commands returned is likely to be higher than the number of tiles selected // because of tiles that create multiple commands. - '
  • Selected: ' + tileset._selectedTiles.length.toLocaleString() + '
  • ' + + '
  • Selected: ' + statistics.selected.toLocaleString() + '
  • ' + // Number of commands executed is likely to be higher because of commands overlapping // multiple frustums. '
  • Commands: ' + statistics.numberOfCommands.toLocaleString() + '
  • '; From e558f2f39cf62a8b91b4c5dc3156be5ce9e06bff Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 29 Mar 2018 14:14:27 -0400 Subject: [PATCH 31/53] Fix refine for skip-lods off --- Source/Scene/Cesium3DTilesetTraversal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 5f104421a515..5baf031b793a 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -497,7 +497,7 @@ define([ } if (traverse) { - refines = updateAndPushChildren(tileset, tile, stack, frameState); + refines = updateAndPushChildren(tileset, tile, stack, frameState) && parentRefines; } if (hasEmptyContent(tile)) { From 0cb8dcd71bcf751169fdcdc13c84139250c66e26 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 29 Mar 2018 15:10:39 -0400 Subject: [PATCH 32/53] Always include root tile in base traversal --- Source/Scene/Cesium3DTilesetTraversal.js | 25 ++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 5baf031b793a..60d334b2b7b3 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -185,6 +185,7 @@ define([ updateTile(tileset, child, frameState); touchTile(tileset, child, frameState); selectTile(tileset, child, frameState); + tile._finalResolution = true; } else if (child._depth - root._depth < 2) { // Continue traversing, but not too far stack.push(child); @@ -197,8 +198,8 @@ define([ if (!skipLevelOfDetail(tileset)) { if (tile.contentAvailable) { // The tile can be selected right away and does not require traverseAndSelect - tile._finalResolution = true; selectTile(tileset, tile, frameState); + tile._finalResolution = true; } return; } @@ -208,9 +209,9 @@ define([ if (defined(loadedTile)) { // Tiles will actually be selected in traverseAndSelect loadedTile._shouldSelect = true; - } else if (tileset.immediatelyLoadDesiredLevelOfDetail) { + } else { // If no ancestors are ready traverse down and select tiles to minimize empty regions. - // This happens often in cases where the camera zooms out and we're waiting for parent tiles to load. + // This happens often for immediatelyLoadDesiredLevelOfDetail where parent tiles are not necessarily loaded before zooming out. selectDescendants(tileset, tile, frameState); } } @@ -459,13 +460,17 @@ define([ if (!skipLevelOfDetail(tileset)) { return true; } - if (tile._screenSpaceError === 0) { - var parent = tile.parent; - if (defined(parent)) { - return parent._screenSpaceError > baseScreenSpaceError; - } + if (tileset.immediatelyLoadDesiredLevelOfDetail) { + return false; + } + var parent = tile.parent; + if (!defined(tile._ancestorWithContent)) { + // Include root tiles in the base traversal always so we will have something to select up to return true; } + if (tile._screenSpaceError === 0) { + return parent._screenSpaceError > baseScreenSpaceError; + } return tile._screenSpaceError > baseScreenSpaceError; } @@ -635,8 +640,8 @@ define([ if (shouldSelect) { if (add) { - tile._finalResolution = true; selectTile(tileset, tile, frameState); + tile._finalResolution = true; } else { tile._selectionDepth = ancestorStack.length; if (tile._selectionDepth > 0) { @@ -644,8 +649,8 @@ define([ } lastAncestor = tile; if (!traverse) { - tile._finalResolution = true; selectTile(tileset, tile, frameState); + tile._finalResolution = true; continue; } ancestorStack.push(tile); From 6d29c37a96b60f7933559f30c4894c8699cb334a Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 22 Feb 2018 14:20:37 -0500 Subject: [PATCH 33/53] Move tileset cache to its own class --- Source/Scene/Cesium3DTile.js | 8 +-- Source/Scene/Cesium3DTileset.js | 66 +++++--------------- Source/Scene/Cesium3DTilesetCache.js | 79 ++++++++++++++++++++++++ Source/Scene/Cesium3DTilesetTraversal.js | 15 ++--- Specs/Scene/Cesium3DTilesetSpec.js | 8 +-- 5 files changed, 108 insertions(+), 68 deletions(-) create mode 100644 Source/Scene/Cesium3DTilesetCache.js diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 18101211d979..57e588774403 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -250,14 +250,16 @@ define([ this.hasTilesetContent = false; /** - * The corresponding node in the cache replacement list. + * The corresponding node in the cache. + * + * See {@link Cesium3DTilesetCache} * * @type {DoublyLinkedListNode} * @readonly * * @private */ - this.replacementNode = undefined; + this.cacheNode = undefined; var expire = header.expire; var expireDuration; @@ -732,8 +734,6 @@ define([ this._contentReadyToProcessPromise = undefined; this._contentReadyPromise = undefined; - this.replacementNode = undefined; - this.lastStyleTime = 0; this._debugColorizeTiles = false; diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index e4590a8d60e1..25ef90c9dff2 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -29,6 +29,7 @@ define([ './Cesium3DTile', './Cesium3DTileColorBlendMode', './Cesium3DTileOptimizations', + './Cesium3DTilesetCache', './Cesium3DTilesetStatistics', './Cesium3DTilesetTraversal', './Cesium3DTileStyleEngine', @@ -73,6 +74,7 @@ define([ Cesium3DTile, Cesium3DTileColorBlendMode, Cesium3DTileOptimizations, + Cesium3DTilesetCache, Cesium3DTilesetStatistics, Cesium3DTilesetTraversal, Cesium3DTileStyleEngine, @@ -175,6 +177,7 @@ define([ this._properties = undefined; // Metadata for per-model/point/etc properties this._geometricError = undefined; // Geometric error when the tree is not rendered at all this._gltfUpAxis = undefined; + this._cache = new Cesium3DTilesetCache(); this._processingQueue = []; this._selectedTiles = []; this._requestedTiles = []; @@ -183,14 +186,6 @@ define([ this._loadTimestamp = undefined; this._timeSinceLoad = 0.0; - var replacementList = new DoublyLinkedList(); - - // [head, sentinel) -> tiles that weren't selected this frame and may be replaced - // (sentinel, tail] -> tiles that were selected this frame - this._replacementList = replacementList; // Tiles with content loaded. For cache management. - this._replacementSentinel = replacementList.add(); - this._trimTiles = false; - this._cullWithChildrenBounds = defaultValue(options.cullWithChildrenBounds, true); this._hasMixedContent = false; @@ -1502,10 +1497,7 @@ define([ tileset._statistics.incrementLoadCounts(tile.content); ++tileset._statistics.numberOfTilesWithContentReady; - // Add to the tile cache. Previously expired tiles are already in the cache. - if (!defined(tile.replacementNode)) { - tile.replacementNode = tileset._replacementList.add(tile); - } + tileset._cache.add(tile); } }; } @@ -1745,52 +1737,27 @@ define([ stack.push(children[i]); } if (tile !== root) { - unloadTileFromCache(tileset, tile); - tile.destroy(); + destroyTile(tileset, tile); --statistics.numberOfTilesTotal; } } root.children = []; } - function unloadTileFromCache(tileset, tile) { - var node = tile.replacementNode; - if (!defined(node)) { - return; - } - - var statistics = tileset._statistics; - var replacementList = tileset._replacementList; - var tileUnload = tileset.tileUnload; + function unloadTile(tileset, tile) { + tileset.tileUnload.raiseEvent(tile); + tileset._statistics.decrementLoadCounts(tile.content); + --tileset._statistics.numberOfTilesWithContentReady; + tile.unloadContent(); + } - tileUnload.raiseEvent(tile); - replacementList.remove(node); - statistics.decrementLoadCounts(tile.content); - --statistics.numberOfTilesWithContentReady; + function destroyTile(tileset, tile) { + tileset._cache.unloadTile(tileset, tile, unloadTile); + tile.destroy(); } function unloadTiles(tileset) { - var trimTiles = tileset._trimTiles; - tileset._trimTiles = false; - - var replacementList = tileset._replacementList; - - var totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; - var maximumMemoryUsageInBytes = tileset._maximumMemoryUsage * 1024 * 1024; - - // Traverse the list only to the sentinel since tiles/nodes to the - // right of the sentinel were used this frame. - // - // The sub-list to the left of the sentinel is ordered from LRU to MRU. - var sentinel = tileset._replacementSentinel; - var node = replacementList.head; - while ((node !== sentinel) && ((totalMemoryUsageInBytes > maximumMemoryUsageInBytes) || trimTiles)) { - var tile = node.item; - node = node.next; - unloadTileFromCache(tileset, tile); - tile.unloadContent(); - totalMemoryUsageInBytes = tileset.totalMemoryUsageInBytes; - } + tileset._cache.unloadTiles(tileset, unloadTile); } /** @@ -1803,8 +1770,7 @@ define([ *

    */ Cesium3DTileset.prototype.trimLoadedTiles = function() { - // Defer to next frame so WebGL delete calls happen inside the render loop - this._trimTiles = true; + this._cache.trim(); }; /////////////////////////////////////////////////////////////////////////// diff --git a/Source/Scene/Cesium3DTilesetCache.js b/Source/Scene/Cesium3DTilesetCache.js new file mode 100644 index 000000000000..848a41da2529 --- /dev/null +++ b/Source/Scene/Cesium3DTilesetCache.js @@ -0,0 +1,79 @@ +define([ + '../Core/defined', + '../Core/DoublyLinkedList' + ], function( + defined, + DoublyLinkedList) { + 'use strict'; + + /** + * Stores tiles with content loaded. + * + * @private + */ + function Cesium3DTilesetCache() { + // [head, sentinel) -> tiles that weren't selected this frame and may be removed from the cache + // (sentinel, tail] -> tiles that were selected this frame + this._list = new DoublyLinkedList(); + this._sentinel = this._list.add(); + this._trimTiles = false; + } + + Cesium3DTilesetCache.prototype.reset = function() { + // Move sentinel node to the tail so, at the start of the frame, all tiles + // may be potentially replaced. Tiles are moved to the right of the sentinel + // when they are selected so they will not be replaced. + this._list.splice(this._list.tail, this._sentinel); + }; + + Cesium3DTilesetCache.prototype.touch = function(tile) { + var node = tile.cacheNode; + if (defined(node)) { + this._list.splice(this._sentinel, node); + } + }; + + Cesium3DTilesetCache.prototype.add = function(tile) { + if (!defined(tile.cacheNode)) { + tile.cacheNode = this._list.add(tile); + } + }; + + Cesium3DTilesetCache.prototype.unloadTile = function(tileset, tile, unloadCallback) { + var node = tile.cacheNode; + if (!defined(node)) { + return; + } + + this._list.remove(node); + tile.cacheNode = undefined; + unloadCallback(tileset, tile); + }; + + Cesium3DTilesetCache.prototype.unloadTiles = function(tileset, unloadCallback) { + var trimTiles = this._trimTiles; + this._trimTiles = false; + + var list = this._list; + + var maximumMemoryUsageInBytes = tileset.maximumMemoryUsage * 1024 * 1024; + + // Traverse the list only to the sentinel since tiles/nodes to the + // right of the sentinel were used this frame. + // + // The sub-list to the left of the sentinel is ordered from LRU to MRU. + var sentinel = this._sentinel; + var node = list.head; + while ((node !== sentinel) && ((tileset.totalMemoryUsageInBytes > maximumMemoryUsageInBytes) || trimTiles)) { + var tile = node.item; + node = node.next; + this.unloadTile(tileset, tile, unloadCallback); + } + }; + + Cesium3DTilesetCache.prototype.trim = function() { + this._trimTiles = true; + }; + + return Cesium3DTilesetCache; +}); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 3052b00ddb79..3acb1a2a66a3 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -25,7 +25,9 @@ define([ /** * @private */ - var Cesium3DTilesetTraversal = {}; + function Cesium3DTilesetTraversal() { + } + function selectTiles(tileset, frameState, outOfCore) { if (tileset.debugFreezeFrame) { @@ -40,11 +42,7 @@ define([ tileset._selectedTilesToStyle.length = 0; tileset._hasMixedContent = false; - // Move sentinel node to the tail so, at the start of the frame, all tiles - // may be potentially replaced. Tiles are moved to the right of the sentinel - // when they are selected so they will not be replaced. - var replacementList = tileset._replacementList; - replacementList.splice(replacementList.tail, tileset._replacementSentinel); + tileset._cache.reset(); var root = tileset._root; root.updateTransform(tileset._modelMatrix); @@ -625,10 +623,7 @@ define([ if (!outOfCore) { return; } - var node = tile.replacementNode; - if (defined(node)) { - tileset._replacementList.splice(tileset._replacementSentinel, node); - } + tileset._cache.touch(tile); } function computeSSE(tile, frameState) { diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index ce9f61a122cb..1e0330376b61 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -2299,14 +2299,14 @@ defineSuite([ it('Unloads cached tiles in a tileset with external tileset.json using maximumMemoryUsage', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetOfTilesetsUrl).then(function(tileset) { var statistics = tileset._statistics; - var replacementList = tileset._replacementList; + var cacheList = tileset._cache._list; tileset.maximumMemoryUsage = 0.025; scene.renderForSpecs(); expect(statistics.numberOfCommands).toEqual(5); expect(statistics.numberOfTilesWithContentReady).toEqual(5); - expect(replacementList.length - 1).toEqual(5); // Only tiles with content are on the replacement list. -1 for sentinel. + expect(cacheList.length - 1).toEqual(5); // Only tiles with content are on the replacement list. -1 for sentinel. // Zoom out so only root tile is needed to meet SSE. This unloads // all tiles except the root and one of the b3dm children @@ -2315,7 +2315,7 @@ defineSuite([ expect(statistics.numberOfCommands).toEqual(1); expect(statistics.numberOfTilesWithContentReady).toEqual(2); - expect(replacementList.length - 1).toEqual(2); + expect(cacheList.length - 1).toEqual(2); // Reset camera so all tiles are reloaded viewAllTiles(); @@ -2324,7 +2324,7 @@ defineSuite([ expect(statistics.numberOfCommands).toEqual(5); expect(statistics.numberOfTilesWithContentReady).toEqual(5); - expect(replacementList.length - 1).toEqual(5); + expect(cacheList.length - 1).toEqual(5); }); }); }); From 5c97c9b7ea9fd4c413ace25daccb42984837dd16 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 27 Mar 2018 14:24:02 -0400 Subject: [PATCH 34/53] Rename file --- .../InstancedGltfExternal/{Box.glb => boxtemp.glb} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/{Box.glb => boxtemp.glb} (100%) diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/Box.glb b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/boxtemp.glb similarity index 100% rename from Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/Box.glb rename to Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/boxtemp.glb From 15921c22800f7cb77ad4b33834c3cae16780165d Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 27 Mar 2018 14:26:08 -0400 Subject: [PATCH 35/53] Rename file back to lowercase --- .../InstancedGltfExternal/{boxtemp.glb => box.glb} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/{boxtemp.glb => box.glb} (100%) diff --git a/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/boxtemp.glb b/Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/box.glb similarity index 100% rename from Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/boxtemp.glb rename to Specs/Data/Cesium3DTiles/Instanced/InstancedGltfExternal/box.glb From bc9c55a4570cb614833fc945b6381ed1180ec950 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 29 Mar 2018 16:33:45 -0400 Subject: [PATCH 36/53] Fix eslint error --- Source/Scene/Cesium3DTilesetTraversal.js | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 3acb1a2a66a3..2fd1038170ff 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -28,7 +28,6 @@ define([ function Cesium3DTilesetTraversal() { } - function selectTiles(tileset, frameState, outOfCore) { if (tileset.debugFreezeFrame) { return; From ee36a460b83969229cae0b70c6496decff078923 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Thu, 29 Mar 2018 16:37:04 -0400 Subject: [PATCH 37/53] Temp CHANGES.md --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 14eaaaef168b..8fa83ccdcd2a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ Change Log ========== +### 1.45 - 2018-05-01 +##### Additions :tada: +* 3D Tiles changes - TODO + ### 1.44 - 2018-04-02 ##### Breaking Changes :mega: From 7254492c5841ba9306d646fffc6b6ff8978ae150 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 3 Apr 2018 13:17:14 -0400 Subject: [PATCH 38/53] Removed hasTilesetContent check when loading because it will always be false until the tile is loaded --- Source/Scene/Cesium3DTilesetTraversal.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 60d334b2b7b3..5a46192d9972 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -236,9 +236,7 @@ define([ var parent = tile.parent; var replace = tile.refine === Cesium3DTileRefine.REPLACE; var add = tile.refine === Cesium3DTileRefine.ADD; - if (tile.hasTilesetContent) { - return 0.0; // Load external tileset as soon as possible - } else if (add) { + if (add) { return tile._distanceToCamera; } else if (replace) { var useParentScreenSpaceError = defined(parent) && (!skipLevelOfDetail(tileset) || (tile._screenSpaceError === 0.0)); From 8e5883514d8614d0c42a9a3d5694497707843e88 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 4 Apr 2018 14:35:06 -0400 Subject: [PATCH 39/53] Add back comment about expired tiles --- Source/Scene/Cesium3DTileset.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index b8b91ca3fd23..aab8c326ca54 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -1496,6 +1496,7 @@ define([ tileset._statistics.incrementLoadCounts(tile.content); ++tileset._statistics.numberOfTilesWithContentReady; + // Add to the tile cache. Previously expired tiles are already in the cache and won't get re-added. tileset._cache.add(tile); } }; From 3f6714b9e2621b0aa31d148611c3fd14b7a086d4 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 12 Jun 2018 21:43:04 -0400 Subject: [PATCH 40/53] Add and update comments --- Source/Scene/Cesium3DTile.js | 3 ++- Source/Scene/Cesium3DTilesetTraversal.js | 30 +++++++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 0ce42fc85d5b..83185a17ab0c 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -321,7 +321,8 @@ define([ this._stackLength = 0; this._selectionDepth = 0; - this._updatedVisibilityFrame = 0; + this._updatedVisibilityFrameRender = 0; + this._updatedVisibilityFramePick = 0; this._touchedFrame = 0; this._selectedFrame = 0; this._requestedFrame = 0; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 5a46192d9972..3d3a44123b74 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -291,8 +291,21 @@ define([ } function updateVisibility(tileset, tile, frameState) { - if (tile._updatedVisibilityFrame === frameState.frameNumber && !frameState.passes.pick) { - return; + // These checks are here to prevent updating tile visibility multiple times - once when checking children + // visibility with the cullWithChildrenBounds optimization, and again when checking the tile itself. + // This is also the only reason updateVisibility and updateTileVisibility are separate functions. + // Render and pick passes require different visibility checks since they use different frustums. + if (frameState.passes.pick) { + if (tile._updatedVisibilityFramePick === frameState.frameNumber) { + return; + } + tile._updatedVisibilityFramePick = frameState.frameNumber; + } + if (frameState.passes.render) { + if (tile._updatedVisibilityFrameRender === frameState.frameNumber) { + return; + } + tile._updatedVisibilityFrameRender = frameState.frameNumber; } var visibilityFlag = VisibilityFlag.NONE; @@ -316,7 +329,6 @@ define([ } tile._visibilityFlag = visibilityFlag; - tile._updatedVisibilityFrame = frameState.frameNumber; } function anyChildrenVisible(tileset, tile, frameState) { @@ -461,13 +473,13 @@ define([ if (tileset.immediatelyLoadDesiredLevelOfDetail) { return false; } - var parent = tile.parent; if (!defined(tile._ancestorWithContent)) { - // Include root tiles in the base traversal always so we will have something to select up to + // Include the highest up tiles with content in the base traversal so we have something to select up to return true; } if (tile._screenSpaceError === 0) { - return parent._screenSpaceError > baseScreenSpaceError; + // If a leaf, use parent's SSE + return tile.parent._screenSpaceError > baseScreenSpaceError; } return tile._screenSpaceError > baseScreenSpaceError; } @@ -475,8 +487,10 @@ define([ function executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState) { // Depth-first traversal that traverses all visible tiles and marks tiles for selection. // If skipLevelOfDetail is off then a tile does not refine until all children are loaded. This is the - // traditional replacement refinement approach. - // Otherwise we allow for skipping levels of the tree and rendering children and parent tiles simultaneously. + // traditional replacement refinement approach and is called the base traversal. + // Tiles that have a greater screen space error than the base screen space error are part of the base traversal, + // all other tiles are part of the skip traversal. The skip traversal allows for skipping levels of the tree + // and rendering children and parent tiles simultaneously. var stack = traversal.stack; stack.push(root); From 5856714239fe92d46177bb30bf78465e1df69faf Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 4 Jul 2018 12:34:16 -0400 Subject: [PATCH 41/53] Simplify update visibility checking --- Source/Scene/Cesium3DTile.js | 3 +-- Source/Scene/Cesium3DTileset.js | 18 ++++++++++++++++++ Source/Scene/Cesium3DTilesetTraversal.js | 19 ++++++------------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 5d7c8b551851..86563d9018d9 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -323,8 +323,7 @@ define([ this._stackLength = 0; this._selectionDepth = 0; - this._updatedVisibilityFrameRender = 0; - this._updatedVisibilityFramePick = 0; + this._updatedVisibilityFrame = 0; this._touchedFrame = 0; this._selectedFrame = 0; this._requestedFrame = 0; diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 6d412f94cd5b..1dfde692f84b 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -180,6 +180,8 @@ define([ this._selectedTilesToStyle = []; this._loadTimestamp = undefined; this._timeSinceLoad = 0.0; + this._pass = 0; + this._passDirty = true; this._cullWithChildrenBounds = defaultValue(options.cullWithChildrenBounds, true); this._allTilesAdditive = true; @@ -1806,6 +1808,18 @@ define([ /////////////////////////////////////////////////////////////////////////// + function getPass(frameState) { + // Convert the passes object to a bitfield to determine when the pass has changed. + var passState = 0; + var passes = frameState.passes; + for (var pass in passes) { + if (passes.hasOwnProperty(pass)) { + passState = (passState << 1) | (passes[pass] ? 1 : 0); + } + } + return passState; + } + /** * Called when {@link Viewer} or {@link CesiumWidget} render the scene to * get the draw commands needed to render this primitive. @@ -1854,6 +1868,10 @@ define([ this._cache.reset(); } + var pass = getPass(frameState); + this._passDirty = this._pass !== pass; + this._pass = pass; + this._requestedTiles.length = 0; Cesium3DTilesetTraversal.selectTiles(this, frameState); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 3d3a44123b74..b6887b8c051f 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -222,6 +222,7 @@ define([ function touchTile(tileset, tile, frameState) { if (frameState.passes.pick) { + // Tiles already touched in render pass. return; } tileset._cache.touch(tile); @@ -291,23 +292,15 @@ define([ } function updateVisibility(tileset, tile, frameState) { - // These checks are here to prevent updating tile visibility multiple times - once when checking children + // Prevent updating tile visibility multiple times - once when checking children // visibility with the cullWithChildrenBounds optimization, and again when checking the tile itself. // This is also the only reason updateVisibility and updateTileVisibility are separate functions. - // Render and pick passes require different visibility checks since they use different frustums. - if (frameState.passes.pick) { - if (tile._updatedVisibilityFramePick === frameState.frameNumber) { - return; - } - tile._updatedVisibilityFramePick = frameState.frameNumber; - } - if (frameState.passes.render) { - if (tile._updatedVisibilityFrameRender === frameState.frameNumber) { - return; - } - tile._updatedVisibilityFrameRender = frameState.frameNumber; + if (!tileset._passDirty && (tile._updatedVisibilityFrame === frameState.frameNumber)) { + return; } + tile._updatedVisibilityFrame = frameState.frameNumber; + var visibilityFlag = VisibilityFlag.NONE; var parent = tile.parent; From 6bda10efb9f8b6088e6a74aa99937ad9184ce6f8 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 4 Jul 2018 15:49:21 -0400 Subject: [PATCH 42/53] Simplify visibility code and tweak comments --- Source/Scene/Cesium3DTile.js | 3 +- Source/Scene/Cesium3DTilesetTraversal.js | 109 ++++++++--------------- 2 files changed, 40 insertions(+), 72 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 86563d9018d9..10d13534cdf1 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -316,7 +316,8 @@ define([ this._centerZDepth = 0; // TODO : remove? this._screenSpaceError = 0; this._visibilityPlaneMask = 0; - this._visibilityFlag = 0; + this._visible = false; + this._inRequestVolume = false; this._finalResolution = true; this._depth = 0; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index b6887b8c051f..6888ccc8ee48 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -30,32 +30,8 @@ define([ function Cesium3DTilesetTraversal() { } - var VisibilityFlag = { - NONE : 0, - VISIBLE : 1, - IN_REQUEST_VOLUME : 2 - }; - - function isVisibleBit(flag) { - return (flag & VisibilityFlag.VISIBLE) > 0; - } - - function inRequestVolumeBit(flag) { - return (flag & VisibilityFlag.IN_REQUEST_VOLUME) > 0; - } - - function clearVisibility(tile) { - tile._visibilityFlag = tile._visibilityFlag & ~VisibilityFlag.VISIBLE; - } - function isVisible(tile) { - var flag = tile._visibilityFlag; - return isVisibleBit(flag) && inRequestVolumeBit(flag); - } - - function isVisibleButNotInRequestVolume(tile) { - var flag = tile._visibilityFlag; - return isVisibleBit(flag) && !inRequestVolumeBit(flag); + return tile._visible && tile._inRequestVolume; } var traversal = { @@ -80,6 +56,8 @@ define([ ancestorStackMaximumLength : 0 }; + var descendantSelectionDepth = 2; + Cesium3DTilesetTraversal.selectTiles = function(tileset, frameState) { if (tileset.debugFreezeFrame) { return; @@ -90,16 +68,16 @@ define([ tileset._emptyTiles.length = 0; tileset._hasMixedContent = false; - var maximumScreenSpaceError = tileset._maximumScreenSpaceError; - var root = tileset._root; updateTile(tileset, root, frameState); + + // The root tile is not visible if (!isVisible(root)) { return; } // The SSE of not rendering the tree is small enough that the tree does not need to be rendered - if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= maximumScreenSpaceError) { + if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= tileset._maximumScreenSpaceError) { return; } @@ -139,7 +117,7 @@ define([ } function skipLevelOfDetail(tileset) { - // Optimization: if all tiles are additive we can turn skipLevelOfDetail off and save some processing + // Optimization: if all tiles are additive turn off skipLevelOfDetail to save some processing return tileset._skipLevelOfDetail && !tileset._allTilesAdditive; } @@ -186,7 +164,7 @@ define([ touchTile(tileset, child, frameState); selectTile(tileset, child, frameState); tile._finalResolution = true; - } else if (child._depth - root._depth < 2) { + } else if (child._depth - root._depth < descendantSelectionDepth) { // Continue traversing, but not too far stack.push(child); } @@ -221,8 +199,8 @@ define([ } function touchTile(tileset, tile, frameState) { - if (frameState.passes.pick) { - // Tiles already touched in render pass. + if (tile._touchedFrame === frameState.frameNumber) { + // Prevents another pass from touching the frame again return; } tileset._cache.touch(tile); @@ -230,10 +208,11 @@ define([ } function getPriority(tileset, tile) { - // If skipLevelOfDetail is off we try to load child tiles as soon as possible so that their parent can refine sooner. - // Additive tiles always load based on distance because it subjectively looks better. - // There may be issues with mixing additive and replacement tiles since SSE and distance are different types of values. - // Maybe all priorities need to be normalized to 0-1 range. + // If skipLevelOfDetail is off try to load child tiles as soon as possible so that their parent can refine sooner. + // Additive tiles are prioritized by distance because it subjectively looks better. + // Replacement tiles are prioritized by screen space error. + // A tileset that has both additive and replacement tiles may not prioritize tiles as effectively since SSE and distance + // are different types of values. Maybe all priorities need to be normalized to 0-1 range. var parent = tile.parent; var replace = tile.refine === Cesium3DTileRefine.REPLACE; var add = tile.refine === Cesium3DTileRefine.ADD; @@ -243,7 +222,7 @@ define([ var useParentScreenSpaceError = defined(parent) && (!skipLevelOfDetail(tileset) || (tile._screenSpaceError === 0.0)); var screenSpaceError = useParentScreenSpaceError ? parent._screenSpaceError : tile._screenSpaceError; var rootScreenSpaceError = tileset._root._screenSpaceError; - return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower priority values (higher priority) + return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower values (e.g. 0.0 is highest priority) } } @@ -292,17 +271,16 @@ define([ } function updateVisibility(tileset, tile, frameState) { - // Prevent updating tile visibility multiple times - once when checking children - // visibility with the cullWithChildrenBounds optimization, and again when checking the tile itself. - // This is also the only reason updateVisibility and updateTileVisibility are separate functions. if (!tileset._passDirty && (tile._updatedVisibilityFrame === frameState.frameNumber)) { + // Return early if visibility has already been checked during this pass and this frame. + // The visibility may have already been checked if the cullWithChildrenBounds optimization is used. + // Visibility must be checked again if the pass changes since the pick pass uses + // a smaller frustum than the render pass. return; } tile._updatedVisibilityFrame = frameState.frameNumber; - var visibilityFlag = VisibilityFlag.NONE; - var parent = tile.parent; var parentTransform = defined(parent) ? parent.computedTransform : tileset._modelMatrix; var parentVisibilityPlaneMask = defined(parent) ? parent._visibilityPlaneMask : CullingVolume.MASK_INDETERMINATE; @@ -312,16 +290,8 @@ define([ tile._centerZDepth = tile.distanceToTileCenter(frameState); tile._screenSpaceError = getScreenSpaceError(tileset, tile.geometricError, tile, frameState); tile._visibilityPlaneMask = tile.visibility(frameState, parentVisibilityPlaneMask); // Use parent's plane mask to speed up visibility test - - if (tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE) { - visibilityFlag |= VisibilityFlag.VISIBLE; - } - - if (tile.insideViewerRequestVolume(frameState)) { - visibilityFlag |= VisibilityFlag.IN_REQUEST_VOLUME; - } - - tile._visibilityFlag = visibilityFlag; + tile._visible = tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; + tile._inRequestVolume = tile.insideViewerRequestVolume(frameState); } function anyChildrenVisible(tileset, tile, frameState) { @@ -343,10 +313,10 @@ define([ return; } - // Use parent's geometric error with child's box to see if we already meet the SSE + // Use parent's geometric error with child's box to see if the tile already meet the SSE var parent = tile.parent; if (defined(parent) && (parent.refine === Cesium3DTileRefine.ADD) && getScreenSpaceError(tileset, parent.geometricError, tile, frameState) <= tileset._maximumScreenSpaceError) { - clearVisibility(tile); + tile._visible = false; return; } @@ -357,7 +327,7 @@ define([ if (replace && useOptimization && hasChildren) { if (!anyChildrenVisible(tileset, tile, frameState)) { ++tileset._statistics.numberOfTilesCulledWithChildrenUnion; - clearVisibility(tile); + tile._visible = false; return; } } @@ -376,7 +346,7 @@ define([ if (defined(parent)) { // ancestorWithContent is an ancestor that has content or has the potential to have // content. Used in conjunction with tileset.skipLevels to know when to skip a tile. - // ancestorWithContentAvailable is an ancestor that we can render if a desired tile is not loaded. + // ancestorWithContentAvailable is an ancestor that is rendered if a desired tile is not loaded. var hasContent = !hasUnloadedContent(parent) || (parent._requestedFrame === frameState.frameNumber); tile._ancestorWithContent = hasContent ? parent : parent._ancestorWithContent; tile._ancestorWithContentAvailable = parent.contentAvailable ? parent : parent._ancestorWithContentAvailable; @@ -421,8 +391,8 @@ define([ // Sort by distance to take advantage of early Z and reduce artifacts for skipLevelOfDetail children.sort(sortChildrenByDistanceToCamera); - // For traditional replacement refinement we need to check if all children are loaded before we can refine - // The reason we always refine empty tiles is it looks better if children stream in as they are loaded to fill the empty space + // For traditional replacement refinement only refine if all children are loaded. + // Empty tiles are exempt since it looks better if children stream in as they are loaded to fill the empty space. var checkRefines = !skipLevelOfDetail(tileset) && replace && !hasEmptyContent(tile); var refines = true; @@ -434,16 +404,15 @@ define([ anyChildrenVisible = true; } else if (checkRefines || tileset.loadSiblings) { // Keep non-visible children loaded since they are still needed before the parent can refine. - // Or loadSiblings is true so we should always load tiles regardless of visibility. + // Or loadSiblings is true so always load tiles regardless of visibility. loadTile(tileset, child, frameState); touchTile(tileset, child, frameState); } if (checkRefines) { var childRefines; - if (isVisibleButNotInRequestVolume(child)) { + if (!child._inRequestVolume) { childRefines = false; } else if (hasEmptyContent(child)) { - // We need to traverse past any empty tiles to know if we can refine childRefines = executeEmptyTraversal(tileset, child, frameState); } else { childRefines = child.contentAvailable; @@ -467,10 +436,10 @@ define([ return false; } if (!defined(tile._ancestorWithContent)) { - // Include the highest up tiles with content in the base traversal so we have something to select up to + // Include root or near-root tiles in the base traversal so there is something to select up to return true; } - if (tile._screenSpaceError === 0) { + if (tile._screenSpaceError === 0.0) { // If a leaf, use parent's SSE return tile.parent._screenSpaceError > baseScreenSpaceError; } @@ -479,8 +448,8 @@ define([ function executeTraversal(tileset, root, baseScreenSpaceError, maximumScreenSpaceError, frameState) { // Depth-first traversal that traverses all visible tiles and marks tiles for selection. - // If skipLevelOfDetail is off then a tile does not refine until all children are loaded. This is the - // traditional replacement refinement approach and is called the base traversal. + // If skipLevelOfDetail is off then a tile does not refine until all children are loaded. + // This is the traditional replacement refinement approach and is called the base traversal. // Tiles that have a greater screen space error than the base screen space error are part of the base traversal, // all other tiles are part of the skip traversal. The skip traversal allows for skipping levels of the tree // and rendering children and parent tiles simultaneously. @@ -511,7 +480,7 @@ define([ } if (hasEmptyContent(tile)) { - // Add empty tile so we can see its debug bounding volumes + // Add empty tile just to show its debug bounding volume addEmptyTile(tileset, tile, frameState); loadTile(tileset, tile, frameState); } else if (add) { @@ -546,7 +515,6 @@ define([ function executeEmptyTraversal(tileset, root, frameState) { // Depth-first traversal that checks if all nearest descendants with content are loaded. Ignores visibility. - // The reason we need to implement a full traversal is to handle chains of empty tiles. var allDescendantsLoaded = true; var maximumScreenSpaceError = tileset._maximumScreenSpaceError; var stack = emptyTraversal.stack; @@ -559,11 +527,11 @@ define([ var children = tile.children; var childrenLength = children.length; - // Only traverse if the tile is empty - we are trying to find descendants with content + // Only traverse if the tile is empty - traversal stop at descendants with content var traverse = hasEmptyContent(tile) && (childrenLength > 0) && (tile._screenSpaceError > maximumScreenSpaceError); - // When we reach a "leaf" that does not have content available we know that not all descendants are loaded - // i.e. there will be holes if the parent tries to refine to its children, so don't refine + // Traversal stops but the tile does not have content yet. + // There will be holes if the parent tries to refine to its children, so don't refine. if (!traverse && !tile.contentAvailable) { allDescendantsLoaded = false; } @@ -588,7 +556,6 @@ define([ /** * Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue. - * Tiles are sorted near to far so we can take advantage of early Z. * Furthermore, this is a preorder traversal so children tiles are selected before ancestor tiles. * * The reason for the preorder traversal is so that tiles can easily be marked with their From 79e77c7ff11ab25deef080738ded789bca2c1f64 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 4 Jul 2018 16:12:35 -0400 Subject: [PATCH 43/53] Tweak and fix tests to get better code coverage --- Specs/Scene/Cesium3DTilesetSpec.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index a3222b836512..88df58ee7a97 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1031,6 +1031,7 @@ defineSuite([ // viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement1Url).then(function(tileset) { + tileset.skipLevelOfDetail = false; viewAllTiles(); scene.renderForSpecs(); @@ -1060,6 +1061,7 @@ defineSuite([ viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement2Url).then(function(tileset) { + tileset.skipLevelOfDetail = false; var statistics = tileset._statistics; return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function() { expect(statistics.numberOfCommands).toEqual(1); @@ -1085,6 +1087,7 @@ defineSuite([ viewRootOnly(); return Cesium3DTilesTester.loadTileset(scene, tilesetReplacement3Url).then(function(tileset) { + tileset.skipLevelOfDetail = false; var statistics = tileset._statistics; var root = tileset._root; expect(statistics.numberOfCommands).toEqual(1); @@ -2665,6 +2668,15 @@ defineSuite([ return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { var statistics = tileset._statistics; expect(statistics.numberOfTilesWithContentReady).toBe(1); + // Renders child while parent loads + viewRootOnly(); + scene.renderForSpecs(); + expect(isSelected(tileset, tileset._root.children[0])); + expect(!isSelected(tileset, tileset._root)); + return Cesium3DTilesTester.waitForTilesLoaded(scene, tileset).then(function(tileset) { + expect(!isSelected(tileset, tileset._root.children[0])); + expect(isSelected(tileset, tileset._root)); + }); }); }); }); From 716c6a4b7f5598f6fc17e2ec2458021ee9eaa2bf Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 4 Jul 2018 16:30:32 -0400 Subject: [PATCH 44/53] Draw empty bounding volumes as a different color (green) --- Source/Scene/Cesium3DTile.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 10d13534cdf1..67fdfcb89c32 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -988,7 +988,16 @@ define([ var showVolume = tileset.debugShowBoundingVolume || (tileset.debugShowContentBoundingVolume && !hasContentBoundingVolume); if (showVolume) { - var color = tile._finalResolution ? ((hasContentBoundingVolume || empty) ? Color.WHITE : Color.RED) : Color.YELLOW; + var color; + if (!tile._finalResolution) { + color = Color.YELLOW; + } else if (hasContentBoundingVolume) { + color = Color.WHITE; + } else if (empty) { + color = Color.GREEN; + } else { + color = Color.RED; + } if (!defined(tile._debugBoundingVolume)) { tile._debugBoundingVolume = tile._boundingVolume.createDebugVolume(color); } From a0d5c5c4116031e04cb028e02bf40dd5b9d64398 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 4 Jul 2018 20:08:03 -0400 Subject: [PATCH 45/53] Comments and .peek --- Source/Core/ManagedArray.js | 17 ++++++++++++++--- Source/Scene/Cesium3DTilesetTraversal.js | 6 +++--- Specs/Core/ManagedArraySpec.js | 10 ++++++++++ 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Source/Core/ManagedArray.js b/Source/Core/ManagedArray.js index fed116228452..8c63178ed689 100644 --- a/Source/Core/ManagedArray.js +++ b/Source/Core/ManagedArray.js @@ -74,9 +74,9 @@ define([ * Sets the element at an index. Resizes the array if index is greater than the length of the array. * * @param {Number} index The index to set. - * @param {*} value The value to set at index. + * @param {*} element The element to set at index. */ - ManagedArray.prototype.set = function(index, value) { + ManagedArray.prototype.set = function(index, element) { //>>includeStart('debug', pragmas.debug); Check.typeOf.number('index', index); //>>includeEnd('debug'); @@ -84,11 +84,22 @@ define([ if (index >= this.length) { this.length = index + 1; } - this._array[index] = value; + this._array[index] = element; + }; + + /** + * Returns the last element in the array without modifying the array. + * + * @returns {*} The last element in the array. + */ + ManagedArray.prototype.peek = function() { + return this._array[this._length - 1]; }; /** * Push an element into the array. + * + * @param {*} element The element to push. */ ManagedArray.prototype.push = function(element) { var index = this.length++; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 6888ccc8ee48..ae8e6a09525a 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -222,7 +222,7 @@ define([ var useParentScreenSpaceError = defined(parent) && (!skipLevelOfDetail(tileset) || (tile._screenSpaceError === 0.0)); var screenSpaceError = useParentScreenSpaceError ? parent._screenSpaceError : tile._screenSpaceError; var rootScreenSpaceError = tileset._root._screenSpaceError; - return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower values (e.g. 0.0 is highest priority) + return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower values (e.g. root tile is highest priority) } } @@ -240,7 +240,6 @@ define([ return 0.0; } - // Avoid divide by zero when viewer is inside the tile var camera = frameState.camera; var frustum = camera.frustum; var context = frameState.context; @@ -255,6 +254,7 @@ define([ var pixelSize = Math.max(frustum.top - frustum.bottom, frustum.right - frustum.left) / Math.max(width, height); error = geometricError / pixelSize; } else { + // Avoid divide by zero when viewer is inside the tile var distance = Math.max(tile._distanceToCamera, CesiumMath.EPSILON7); var sseDenominator = camera.frustum.sseDenominator; error = (geometricError * height) / (distance * sseDenominator); @@ -587,7 +587,7 @@ define([ selectionTraversal.ancestorStackMaximumLength = Math.max(selectionTraversal.ancestorStackMaximumLength, ancestorStack.length); if (ancestorStack.length > 0) { - var waitingTile = ancestorStack.get(ancestorStack.length - 1); + var waitingTile = ancestorStack.peek(); if (waitingTile._stackLength === stack.length) { ancestorStack.pop(); if (waitingTile === lastAncestor) { diff --git a/Specs/Core/ManagedArraySpec.js b/Specs/Core/ManagedArraySpec.js index 94c1c5b32a38..c050bb8f6013 100644 --- a/Specs/Core/ManagedArraySpec.js +++ b/Specs/Core/ManagedArraySpec.js @@ -55,6 +55,16 @@ defineSuite([ expect(array.length).toEqual(6); }); + it('peeks at the last element of the array', function() { + var array = new ManagedArray(); + expect(array.peek()).toBeUndefined(); + array.push(0); + expect(array.peek()).toBe(0); + array.push(1); + array.push(2); + expect(array.peek()).toBe(2); + }); + it('can push values', function() { var array = new ManagedArray(); var length = 10; From 6a8fcfab3c2469deec7f54dc680094eaf43285b0 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 4 Jul 2018 20:24:37 -0400 Subject: [PATCH 46/53] Updated CHANGES.md --- CHANGES.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 04cd837bf343..3c329a4fbe5c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,16 @@ Change Log ========== +### 1.48 2018-08-01 + +##### Fixes :wrench: +* Several performance improvements and fixes to the 3D Tiles traversal code. [#6390](https://github.com/AnalyticalGraphicsInc/cesium/pull/6390) + * Improved load performance when `skipLevelOfDetail` is false. + * Fixed a bug that caused some skipped tiles to load when `skipLevelOfDetail` is true. + * Fixed pick statistics in the 3D Tiles Inspector. + * Fixed drawing of debug labels for external tilesets. + * Fixed drawing of debug outlines for empty tiles. + ### 1.47 - 2018-07-02 ##### Breaking Changes :mega: From 270599616b7a24f7b0b4e0ecc83fabf681264871 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Wed, 4 Jul 2018 20:56:12 -0400 Subject: [PATCH 47/53] Remove RequestQueue code --- Source/Core/RequestQueue.js | 186 ---------------------------- Source/Core/RequestScheduler.js | 82 +++++++------ Source/Scene/GlobeSurfaceTile.js | 6 - Specs/Core/HeapSpec.js | 174 ++++++++++++++++++++++++++ Specs/Core/RequestQueueSpec.js | 189 ----------------------------- Specs/Core/RequestSchedulerSpec.js | 38 +++--- 6 files changed, 234 insertions(+), 441 deletions(-) delete mode 100644 Source/Core/RequestQueue.js create mode 100644 Specs/Core/HeapSpec.js delete mode 100644 Specs/Core/RequestQueueSpec.js diff --git a/Source/Core/RequestQueue.js b/Source/Core/RequestQueue.js deleted file mode 100644 index 169483d3881c..000000000000 --- a/Source/Core/RequestQueue.js +++ /dev/null @@ -1,186 +0,0 @@ -define([ - './Check', - './defineProperties' - ], function( - Check, - defineProperties) { - 'use strict'; - - /** - * Priority queue for the {@link RequestScheduler} implemented as a sorted array. - * The request with the highest priority is placed at index 0 and the request - * with lowest priority is placed at index length - 1. - *

    - * A lower request.priority value indicates that the request has higher priority. See {@link Request#priority}. - *

    - * - * @alias RequestQueue - * @constructor - * @private - * - * @param {Number} maximumLength The maximum length of the queue. - */ - function RequestQueue(maximumLength) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.number('maximumLength', maximumLength); - //>>includeEnd('debug'); - - this._array = new Array(maximumLength); - this._length = 0; - this._maximumLength = maximumLength; - } - - defineProperties(RequestQueue.prototype, { - /** - * Gets the length of the queue. - * - * @memberof RequestQueue.prototype - * - * @type {Number} - * @readonly - */ - length : { - get : function() { - return this._length; - } - } - }); - - /** - * Get the request at the given index. - * - * @param {Number} index The index of the request. - * - * @return {Request} The request at the given index. - */ - RequestQueue.prototype.get = function(index) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.number.greaterThanOrEquals('index', index, 0); - Check.typeOf.number.lessThan('index', index, this._length); - //>>includeEnd('debug'); - return this._array[index]; - }; - - /** - * Insert a request into the queue. If the length would grow greater than the maximum length - * of the queue, the lowest priority request is removed and returned. - * - * @param {Request} request The request to insert. - * - * @return {Request|undefined} The request that was removed from the queue if the queue is at full capacity. - */ - RequestQueue.prototype.insert = function(request) { - //>>includeStart('debug', pragmas.debug); - Check.defined('request', request); - //>>includeEnd('debug'); - - var array = this._array; - var previousLength = this._length; - var length = this._length; - var maximumLength = this._maximumLength; - - if (length < maximumLength) - { - ++this._length; - } - - if (previousLength === 0) - { - array[0] = request; - return; - } - - var removedRequest; - var lastIndex = previousLength - 1; - - if (previousLength === maximumLength) { - var lastRequest = array[lastIndex]; - if (request.priority >= lastRequest.priority) { - // The array is full and the priority value of this request is too high to be inserted. - return request; - } - // The array is full and the inserted request pushes off the last request - removedRequest = lastRequest; - --lastIndex; - } - - while (lastIndex >= 0 && request.priority < array[lastIndex].priority) { - array[lastIndex + 1] = array[lastIndex]; // Shift element to the right - --lastIndex; - } - array[lastIndex + 1] = request; - - return removedRequest; - }; - - /** - * Call the given function for each request in the queue. - * - * @type {RequestQueue~ForEachCallback} The function to call. - */ - RequestQueue.prototype.forEach = function(callback) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.func('callback', callback); - //>>includeEnd('debug'); - - var array = this._array; - var length = this._length; - for (var i = 0; i < length; ++i) { - callback(array[i]); - } - }; - - /** - * Sorts the queue. - */ - RequestQueue.prototype.sort = function() { - var array = this._array; - var length = this._length; - - // Use insertion sort since our array is small and likely to be mostly sorted already. - // Additionally length may be smaller than the array's actual length, so calling array.sort will lead to incorrect results for uninitialized values. - for (var i = 1; i < length; ++i) { - var j = i; - while ((j > 0) && (array[j - 1].priority > array[j].priority)) { - var temp = array[j - 1]; - array[j - 1] = array[j]; - array[j] = temp; - --j; - } - } - }; - - /** - * Remove length number of requests from the top of the queue. - * - * @param {Number} length The number of requests to remove. - */ - RequestQueue.prototype.remove = function(length) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.number.greaterThanOrEquals('length', length, 0); - Check.typeOf.number.lessThanOrEquals('length', length, this._length); - //>>includeEnd('debug'); - if (length === 0) { - return; - } - if (length === this._length) { - this._length = 0; - return; - } - - // Shift remaining requests back to the left - var array = this._array; - for (var i = length; i < this._length; ++i) { - array[i - length] = array[i]; - } - this._length -= length; - }; - - /** - * The callback to use in forEach. - * @callback RequestQueue~ForEachCallback - * @param {Request} request The request. - */ - - return RequestQueue; -}); diff --git a/Source/Core/RequestScheduler.js b/Source/Core/RequestScheduler.js index 8c307663d3a4..6fd455b58007 100644 --- a/Source/Core/RequestScheduler.js +++ b/Source/Core/RequestScheduler.js @@ -6,9 +6,9 @@ define([ './defined', './defineProperties', './Event', + './Heap', './isBlobUri', './isDataUri', - './RequestQueue', './RequestState' ], function( Uri, @@ -18,12 +18,16 @@ define([ defined, defineProperties, Event, + Heap, isBlobUri, isDataUri, - RequestQueue, RequestState) { 'use strict'; + function sortRequests(a, b) { + return a.priority - b.priority; + } + var statistics = { numberOfAttemptedRequests : 0, numberOfActiveRequests : 0, @@ -33,8 +37,12 @@ define([ numberOfActiveRequestsEver : 0 }; - var requestQueueLength = 20; - var requestQueue = new RequestQueue(requestQueueLength); + var priorityHeapLength = 20; + var requestHeap = new Heap({ + comparator : sortRequests + }); + requestHeap.maximumLength = priorityHeapLength; + requestHeap.reserve(priorityHeapLength); var activeRequests = []; var numberOfActiveRequestsByServer = {}; @@ -115,28 +123,29 @@ define([ }, /** - * The maximum length of the request queue. This limits the number of requests that are sorted by priority. Only applies to requests that are not yet active. + * The maximum size of the priority heap. This limits the number of requests that are sorted by priority. Only applies to requests that are not yet active. * * @memberof RequestScheduler * * @type {Number} * @default 20 - * - * @private */ - requestQueueLength : { + priorityHeapLength : { get : function() { - return requestQueueLength; + return priorityHeapLength; }, set : function(value) { - // Cancel all requests and resize the queue - var length = requestQueue.length; - for (var i = 0; i < length; ++i) { - var request = requestQueue.get(i); - cancelRequest(request); + // If the new length shrinks the heap, need to cancel some of the requests. + // Since this value is not intended to be tweaked regularly it is fine to just cancel the high priority requests. + if (value < priorityHeapLength) { + while (requestHeap.length > value) { + var request = requestHeap.pop(); + cancelRequest(request); + } } - requestQueue = new RequestQueue(value); - RequestScheduler.requestQueue = requestQueue; + priorityHeapLength = value; + requestHeap.maximumLength = value; + requestHeap.reserve(value); } } }); @@ -245,19 +254,21 @@ define([ } activeRequests.length -= removeCount; - // Update priority of issued requests and resort the queue - requestQueue.forEach(updatePriority); - requestQueue.sort(); + // Update priority of issued requests and resort the heap + var issuedRequests = requestHeap.internalArray; + var issuedLength = requestHeap.length; + for (i = 0; i < issuedLength; ++i) { + updatePriority(issuedRequests[i]); + } + requestHeap.resort(); // Get the number of open slots and fill with the highest priority requests. // Un-throttled requests are automatically added to activeRequests, so activeRequests.length may exceed maximumRequests var openSlots = Math.max(RequestScheduler.maximumRequests - activeRequests.length, 0); var filledSlots = 0; - var processedRequests = 0; - var totalRequests = requestQueue.length; - while (filledSlots < openSlots && processedRequests < totalRequests) { - // Loop until all open slots are filled or the queue becomes empty - request = requestQueue.get(processedRequests++); + while (filledSlots < openSlots && requestHeap.length > 0) { + // Loop until all open slots are filled or the heap becomes empty + request = requestHeap.pop(); if (request.cancelled) { // Request was explicitly cancelled cancelRequest(request); @@ -273,7 +284,6 @@ define([ startRequest(request); ++filledSlots; } - requestQueue.remove(processedRequests); updateStatistics(); }; @@ -346,9 +356,10 @@ define([ return undefined; } - // Insert into the priority queue and see if a request was bumped off. If this request is the lowest priority it will be returned. + // Insert into the priority heap and see if a request was bumped off. If this request is the lowest + // priority it will be returned. updatePriority(request); - var removedRequest = requestQueue.insert(request); + var removedRequest = requestHeap.insert(request); if (defined(removedRequest)) { if (removedRequest === request) { @@ -398,19 +409,12 @@ define([ * @private */ RequestScheduler.clearForSpecs = function() { - var request; - var length; - var i; - - length = requestQueue.length; - for (i = 0; i < length; ++i) { - request = requestQueue.get(i); + while (requestHeap.length > 0) { + var request = requestHeap.pop(); cancelRequest(request); } - requestQueue.remove(length); - - length = activeRequests.length; - for (i = 0; i < length; ++i) { + var length = activeRequests.length; + for (var i = 0; i < length; ++i) { cancelRequest(activeRequests[i]); } activeRequests.length = 0; @@ -439,7 +443,7 @@ define([ * * @private */ - RequestScheduler.requestQueue = requestQueue; + RequestScheduler.requestHeap = requestHeap; return RequestScheduler; }); diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index efa1ab9f40bd..46bba1be5100 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -7,7 +7,6 @@ define([ '../Core/defineProperties', '../Core/IntersectionTests', '../Core/PixelFormat', - '../Core/Rectangle', '../Renderer/PixelDatatype', '../Renderer/Sampler', '../Renderer/Texture', @@ -29,7 +28,6 @@ define([ defineProperties, IntersectionTests, PixelFormat, - Rectangle, PixelDatatype, Sampler, Texture, @@ -256,10 +254,6 @@ define([ function createPriorityFunction(surfaceTile, frameState) { return function() { - if (Rectangle.contains(surfaceTile.tileBoundingRegion.rectangle, frameState.camera.positionCartographic)) { - // If the camera is inside this tile's bounding region treat it as highest priority - return 0.0; - } return surfaceTile.tileBoundingRegion.distanceToCamera(frameState); }; } diff --git a/Specs/Core/HeapSpec.js b/Specs/Core/HeapSpec.js new file mode 100644 index 000000000000..1b77e269652e --- /dev/null +++ b/Specs/Core/HeapSpec.js @@ -0,0 +1,174 @@ +defineSuite([ + 'Core/Heap' + ], function( + Heap) { + 'use strict'; + + var length = 100; + + function checkHeap(heap, comparator) { + var array = heap.internalArray; + var pass = true; + var length = heap.length; + for (var i = 0; i < length; ++i) { + var left = 2 * (i + 1) - 1; + var right = 2 * (i + 1); + if (left < heap.length) { + pass = pass && (comparator(array[i], array[left]) <= 0); + } + if (right < heap.length) { + pass = pass && (comparator(array[i], array[right]) <= 0); + } + } + return pass; + } + + // min heap + function comparator(a, b) { + return a - b; + } + + it('maintains heap property on insert', function() { + var heap = new Heap({ + comparator : comparator + }); + var pass = true; + for (var i = 0; i < length; ++i) { + heap.insert(Math.random()); + pass = pass && checkHeap(heap, comparator); + } + + expect(pass).toBe(true); + }); + + it('maintains heap property on pop', function() { + var heap = new Heap({ + comparator : comparator + }); + var i; + for (i = 0; i < length; ++i) { + heap.insert(Math.random()); + } + var pass = true; + for (i = 0; i < length; ++i) { + heap.pop(); + pass = pass && checkHeap(heap, comparator); + } + expect(pass).toBe(true); + }); + + it('limited by maximum length', function() { + var heap = new Heap({ + comparator : comparator + }); + heap.maximumLength = length / 2; + var pass = true; + for (var i = 0; i < length; ++i) { + heap.insert(Math.random()); + pass = pass && checkHeap(heap, comparator); + } + expect(pass).toBe(true); + expect(heap.length <= heap.maximumLength).toBe(true); + // allowed one extra slot for swapping + expect(heap.internalArray.length).toBeLessThanOrEqualTo(heap.maximumLength + 1); + }); + + it('pops in sorted order', function() { + var heap = new Heap({ + comparator : comparator + }); + var i; + for (i = 0; i < length; ++i) { + heap.insert(Math.random()); + } + var curr = heap.pop(); + var pass = true; + for (i = 0; i < length - 1; ++i) { + var next = heap.pop(); + pass = pass && (comparator(curr, next) <= 0); + curr = next; + } + expect(pass).toBe(true); + }); + + it('insert returns the removed element when maximumLength is set', function() { + var heap = new Heap({ + comparator : comparator + }); + heap.maximumLength = length; + + var i; + var max = 0.0; + var min = 1.0; + var values = new Array(length); + for (i = 0; i < length; ++i) { + var value = Math.random(); + max = Math.max(max, value); + min = Math.min(min, value); + values[i] = value; + } + + // Push 99 values + for (i = 0; i < length - 1; ++i) { + heap.insert(values[i]); + } + + // Push 100th, nothing is removed so it returns undefined + var removed = heap.insert(values[length - 1]); + expect(removed).toBeUndefined(); + + // Insert value, an element is removed + removed = heap.insert(max - 0.1); + expect(removed).toBeDefined(); + + // If this value is the least priority it will be returned + removed = heap.insert(max + 0.1); + expect(removed).toBe(max + 0.1); + }); + + it('resort', function() { + function comparator(a, b) { + return a.distance - b.distance; + } + + var i; + var heap = new Heap({ + comparator : comparator + }); + for (i = 0; i < length; ++i) { + heap.insert({ + distance : i / (length - 1), + id : i + }); + } + + // Check that elements are initially sorted + var element; + var elements = []; + var currentId = 0; + while (heap.length > 0) { + element = heap.pop(); + elements.push(element); + expect(element.id).toBeGreaterThanOrEqualTo(currentId); + currentId = element.id; + } + + // Add back into heap + for (i = 0; i < length; ++i) { + heap.insert(elements[i]); + } + + // Invert priority + for (i = 0; i < length; ++i) { + elements[i].distance = 1.0 - elements[i].distance; + } + + // Resort and check the the elements are popped in the opposite order now + heap.resort(); + while (heap.length > 0) { + element = heap.pop(); + expect(element.id).toBeLessThanOrEqualTo(currentId); + currentId = element.id; + } + }); +}); diff --git a/Specs/Core/RequestQueueSpec.js b/Specs/Core/RequestQueueSpec.js deleted file mode 100644 index a7bb51107e7e..000000000000 --- a/Specs/Core/RequestQueueSpec.js +++ /dev/null @@ -1,189 +0,0 @@ -defineSuite([ - 'Core/RequestQueue', - 'Core/Math', - 'Core/Request' - ], function( - RequestQueue, - CesiumMath, - Request) { - 'use strict'; - - var length = 20; - - function createRequest(distance) { - return new Request({ - priority : distance - }); - } - - function isSorted(queue) { - var distance = Number.NEGATIVE_INFINITY; - for (var i = 0; i < queue.length; ++i) { - var requestDistance = queue.get(i).priority; - if (requestDistance < distance) { - return false; - } - - distance = requestDistance; - } - return true; - } - - it('sets initial values', function() { - var queue = new RequestQueue(length); - expect(queue._array.length).toBe(length); - expect(queue._length).toBe(0); - expect(queue._maximumLength).toBe(length); - }); - - it('gets length', function() { - var queue = new RequestQueue(length); - expect(queue.length).toBe(0); - queue.insert(createRequest(1.0)); - expect(queue.length).toBe(1); - }); - - it('get', function() { - var queue = new RequestQueue(length); - queue.insert(createRequest(1)); - queue.insert(createRequest(0)); - expect(queue.get(0).priority).toBe(0); - expect(queue.get(1).priority).toBe(1); - }); - - it('insert', function() { - var removedRequest; - var request; - var i; - - CesiumMath.setRandomNumberSeed(0.0); - var distances = new Array(length); - for (i = 0; i < length; ++i) { - distances[i] = CesiumMath.nextRandomNumber(); - } - distances.sort(); - var lowestDistance = distances[0]; - var highestDistance = distances[distances.length - 1]; - - var queue = new RequestQueue(length); - for (i = 0; i < length; ++i) { - removedRequest = queue.insert(createRequest(distances[i])); - expect(removedRequest).toBeUndefined(); // Requests are not removed until the queue is full - } - - expect(isSorted(queue)).toBe(true); - - request = createRequest(highestDistance); - expect(queue.insert(request)).toBe(request); - - request = createRequest(highestDistance + 1.0); - expect(queue.insert(request)).toBe(request); - - request = createRequest(lowestDistance); - expect(queue.insert(request).priority).toBe(highestDistance); - expect(queue.get(0).priority).toBe(lowestDistance); - expect(queue.get(1).priority).toBe(lowestDistance); - expect(queue.get(2).priority).toBeGreaterThan(lowestDistance); - - expect(isSorted(queue)).toBe(true); - }); - - it('forEach', function() { - var total = 0; - var queue = new RequestQueue(length); - for (var i = 0; i < length; ++i) { - queue.insert(createRequest(1)); - } - queue.forEach(function(request) { - total += request.priority; - }); - expect(total).toBe(length); - }); - - it('sort', function() { - var i; - CesiumMath.setRandomNumberSeed(0.0); - var queue = new RequestQueue(length); - for (i = 0; i < length / 2; ++i) { - queue.insert(createRequest(CesiumMath.nextRandomNumber())); - } - queue.forEach(function(request) { - request.priority = CesiumMath.nextRandomNumber(); - }); - expect(isSorted(queue)).toBe(false); - queue.sort(); - expect(isSorted(queue)).toBe(true); - }); - - it('remove', function() { - var queue = new RequestQueue(length); - for (var i = 0; i < length; ++i) { - queue.insert(createRequest(i)); - } - queue.remove(0); - expect(queue.get(0).priority).toBe(0); - expect(queue.get(1).priority).toBe(1); - expect(queue.length).toBe(length); - - queue.remove(1); - expect(queue.get(0).priority).toBe(1); - expect(queue.get(1).priority).toBe(2); - expect(queue.length).toBe(length - 1); - - queue.remove(2); - expect(queue.get(0).priority).toBe(3); - expect(queue.get(1).priority).toBe(4); - expect(queue.length).toBe(length - 3); - - queue.remove(17); - expect(queue.length).toBe(0); - }); - - it('throws if maximumLength is undefined', function() { - expect(function() { - return new RequestQueue(); - }).toThrowDeveloperError(); - }); - - it('throws if get index is out of range', function() { - expect(function() { - var queue = new RequestQueue(length); - queue.get(0); - }).toThrowDeveloperError(); - - expect(function() { - var queue = new RequestQueue(length); - queue.insert(createRequest(0.0)); - queue.get(1); - }).toThrowDeveloperError(); - - expect(function() { - var queue = new RequestQueue(length); - queue.insert(createRequest(0.0)); - queue.get(-1); - }).toThrowDeveloperError(); - }); - - it('throws if forEach callback is not a function', function() { - expect(function() { - var queue = new RequestQueue(length); - queue.forEach(); - }).toThrowDeveloperError(); - expect(function() { - var queue = new RequestQueue(length); - queue.forEach(5); - }).toThrowDeveloperError(); - }); - - it('throws if remove length is out of range', function() { - expect(function() { - var queue = new RequestQueue(length); - queue.remove(1); - }).toThrowDeveloperError(); - - expect(function() { - var queue = new RequestQueue(length); - queue.remove(-1); - }).toThrowDeveloperError(); - }); -}); diff --git a/Specs/Core/RequestSchedulerSpec.js b/Specs/Core/RequestSchedulerSpec.js index b5b3b965cc16..55f49fc1492e 100644 --- a/Specs/Core/RequestSchedulerSpec.js +++ b/Specs/Core/RequestSchedulerSpec.js @@ -12,13 +12,13 @@ defineSuite([ var originalMaximumRequests; var originalMaximumRequestsPerServer; - var originalRequestQueueLength; + var originalPriorityHeapLength; var originalRequestsByServer; beforeAll(function() { originalMaximumRequests = RequestScheduler.maximumRequests; originalMaximumRequestsPerServer = RequestScheduler.maximumRequestsPerServer; - originalRequestQueueLength = RequestScheduler.requestQueueLength; + originalPriorityHeapLength = RequestScheduler.priorityHeapLength; originalRequestsByServer = RequestScheduler.requestsByServer; }); @@ -30,7 +30,7 @@ defineSuite([ afterEach(function() { RequestScheduler.maximumRequests = originalMaximumRequests; RequestScheduler.maximumRequestsPerServer = originalMaximumRequestsPerServer; - RequestScheduler.requestQueueLength = originalRequestQueueLength; + RequestScheduler.priorityHeapLength = originalPriorityHeapLength; RequestScheduler.requestsByServer = originalRequestsByServer; }); @@ -208,7 +208,7 @@ defineSuite([ } }); - it('honors requestQueue length', function() { + it('honors priorityHeapLength', function() { var deferreds = []; var requests = []; @@ -229,23 +229,22 @@ defineSuite([ return request; } - RequestScheduler.requestQueueLength = 1; + RequestScheduler.priorityHeapLength = 1; var firstRequest = createRequest(0.0); var promise = RequestScheduler.request(firstRequest); expect(promise).toBeDefined(); promise = RequestScheduler.request(createRequest(1.0)); expect(promise).toBeUndefined(); - RequestScheduler.requestQueueLength = 3; - promise = RequestScheduler.request(createRequest(1.0)); + RequestScheduler.priorityHeapLength = 3; promise = RequestScheduler.request(createRequest(2.0)); promise = RequestScheduler.request(createRequest(3.0)); expect(promise).toBeDefined(); promise = RequestScheduler.request(createRequest(4.0)); expect(promise).toBeUndefined(); - // The requests are cancelled when the queue length changes - RequestScheduler.requestQueueLength = 2; + // A request is cancelled to accommodate the new heap length + RequestScheduler.priorityHeapLength = 2; expect(firstRequest.state).toBe(RequestState.CANCELLED); var length = deferreds.length; @@ -453,7 +452,7 @@ defineSuite([ }); } - var length = RequestScheduler.requestQueueLength; + var length = RequestScheduler.priorityHeapLength; for (var i = 0; i < length; ++i) { var priority = Math.random(); RequestScheduler.request(createRequest(priority)); @@ -490,7 +489,7 @@ defineSuite([ var i; var request; - var length = RequestScheduler.requestQueueLength; + var length = RequestScheduler.priorityHeapLength; for (i = 0; i < length; ++i) { var priority = i / (length - 1); request = createRequest(priority); @@ -502,31 +501,28 @@ defineSuite([ RequestScheduler.maximumRequests = 0; RequestScheduler.update(); - var requestQueue = RequestScheduler.requestQueue; + var requestHeap = RequestScheduler.requestHeap; var requests = []; var currentTestId = 0; - - for (i = 0; i < length; ++i) { - request = requestQueue.get(i); + while (requestHeap.length > 0) { + request = requestHeap.pop(); requests.push(request); expect(request.testId).toBeGreaterThanOrEqualTo(currentTestId); currentTestId = request.testId; } - requestQueue.remove(length); for (i = 0; i < length; ++i) { - requestQueue.insert(requests[i]); + requestHeap.insert(requests[i]); } invertPriority = true; RequestScheduler.update(); - for (i = 0; i < length; ++i) { - request = requestQueue.get(i); + while (requestHeap.length > 0) { + request = requestHeap.pop(); expect(request.testId).toBeLessThanOrEqualTo(currentTestId); currentTestId = request.testId; } - requestQueue.remove(length); }); it('handles low priority requests', function() { @@ -547,7 +543,7 @@ defineSuite([ var mediumPriority = 0.5; var lowPriority = 1.0; - var length = RequestScheduler.requestQueueLength; + var length = RequestScheduler.priorityHeapLength; for (var i = 0; i < length; ++i) { RequestScheduler.request(createRequest(mediumPriority)); } From c8c981eded706a60b0e897bcce231c215ec8459e Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 6 Jul 2018 14:10:40 -0400 Subject: [PATCH 48/53] Added visited frame to tile --- Source/Scene/Cesium3DTile.js | 1 + Source/Scene/Cesium3DTilesetTraversal.js | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 67fdfcb89c32..12eba5f028b1 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -326,6 +326,7 @@ define([ this._updatedVisibilityFrame = 0; this._touchedFrame = 0; + this._visitedFrame = 0; this._selectedFrame = 0; this._requestedFrame = 0; this._ancestorWithContent = undefined; diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index ae8e6a09525a..71bf7bf3e8bd 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -194,8 +194,12 @@ define([ } } - function visitTile(tileset) { + function visitTile(tileset, tile, frameState) { + if (!tileset._passDirty && (tile._visitedFrame === frameState.frameNumber)) { + return; + } ++tileset._statistics.visited; + tile._visitedFrame = frameState.frameNumber; } function touchTile(tileset, tile, frameState) { @@ -507,7 +511,7 @@ define([ } } - visitTile(tileset); + visitTile(tileset, tile, frameState); touchTile(tileset, tile, frameState); tile._refines = refines; } From 1dd5e6828ccd3d5be0fcd5e1a68c4c4983408f58 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 6 Jul 2018 15:56:22 -0400 Subject: [PATCH 49/53] Fix update visibility code again --- Source/Scene/Cesium3DTileset.js | 25 ++++-------------------- Source/Scene/Cesium3DTilesetTraversal.js | 16 +++++---------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 1dfde692f84b..258eb8a41158 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -180,8 +180,6 @@ define([ this._selectedTilesToStyle = []; this._loadTimestamp = undefined; this._timeSinceLoad = 0.0; - this._pass = 0; - this._passDirty = true; this._cullWithChildrenBounds = defaultValue(options.cullWithChildrenBounds, true); this._allTilesAdditive = true; @@ -1808,18 +1806,6 @@ define([ /////////////////////////////////////////////////////////////////////////// - function getPass(frameState) { - // Convert the passes object to a bitfield to determine when the pass has changed. - var passState = 0; - var passes = frameState.passes; - for (var pass in passes) { - if (passes.hasOwnProperty(pass)) { - passState = (passState << 1) | (passes[pass] ? 1 : 0); - } - } - return passState; - } - /** * Called when {@link Viewer} or {@link CesiumWidget} render the scene to * get the draw commands needed to render this primitive. @@ -1849,13 +1835,14 @@ define([ this._timeSinceLoad = Math.max(JulianDate.secondsDifference(frameState.time, this._loadTimestamp) * 1000, 0.0); - this._skipLevelOfDetail = this.skipLevelOfDetail && !defined(this._classificationType) && !this._disableSkipLevelOfDetail; + this._skipLevelOfDetail = this.skipLevelOfDetail && !defined(this._classificationType) && !this._disableSkipLevelOfDetail && !this._allTilesAdditive; // Do not do out-of-core operations (new content requests, cache removal, // process new tiles) during the pick pass. var passes = frameState.passes; - var isPick = (passes.pick && !passes.render); - var outOfCore = !isPick; + var isRender = passes.render; + var isPick = passes.pick; + var outOfCore = isRender; var statistics = this._statistics; statistics.clear(); @@ -1868,10 +1855,6 @@ define([ this._cache.reset(); } - var pass = getPass(frameState); - this._passDirty = this._pass !== pass; - this._pass = pass; - this._requestedTiles.length = 0; Cesium3DTilesetTraversal.selectTiles(this, frameState); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 71bf7bf3e8bd..1131a87955ca 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -117,8 +117,7 @@ define([ } function skipLevelOfDetail(tileset) { - // Optimization: if all tiles are additive turn off skipLevelOfDetail to save some processing - return tileset._skipLevelOfDetail && !tileset._allTilesAdditive; + return tileset._skipLevelOfDetail; } function addEmptyTile(tileset, tile) { @@ -195,9 +194,6 @@ define([ } function visitTile(tileset, tile, frameState) { - if (!tileset._passDirty && (tile._visitedFrame === frameState.frameNumber)) { - return; - } ++tileset._statistics.visited; tile._visitedFrame = frameState.frameNumber; } @@ -275,16 +271,12 @@ define([ } function updateVisibility(tileset, tile, frameState) { - if (!tileset._passDirty && (tile._updatedVisibilityFrame === frameState.frameNumber)) { - // Return early if visibility has already been checked during this pass and this frame. + if (tile._updatedVisibilityFrame === frameState.frameNumber) { + // Return early if visibility has already been checked during the traversal. // The visibility may have already been checked if the cullWithChildrenBounds optimization is used. - // Visibility must be checked again if the pass changes since the pick pass uses - // a smaller frustum than the render pass. return; } - tile._updatedVisibilityFrame = frameState.frameNumber; - var parent = tile.parent; var parentTransform = defined(parent) ? parent.computedTransform : tileset._modelMatrix; var parentVisibilityPlaneMask = defined(parent) ? parent._visibilityPlaneMask : CullingVolume.MASK_INDETERMINATE; @@ -296,6 +288,7 @@ define([ tile._visibilityPlaneMask = tile.visibility(frameState, parentVisibilityPlaneMask); // Use parent's plane mask to speed up visibility test tile._visible = tile._visibilityPlaneMask !== CullingVolume.MASK_OUTSIDE; tile._inRequestVolume = tile.insideViewerRequestVolume(frameState); + tile._updatedVisibilityFrame = frameState.frameNumber; } function anyChildrenVisible(tileset, tile, frameState) { @@ -514,6 +507,7 @@ define([ visitTile(tileset, tile, frameState); touchTile(tileset, tile, frameState); tile._refines = refines; + tile._updatedVisibilityFrame = 0; // Reset so visibility is checked during the next pass } } From 6e8439019b1a0b74b1161b74f2d00ee33157f2b5 Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 6 Jul 2018 17:04:48 -0400 Subject: [PATCH 50/53] Remove TODO --- Source/Scene/Cesium3DTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index da9b5673bcb5..4470ea132699 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -318,7 +318,7 @@ define([ // Members that are updated every frame for tree traversal and rendering optimizations: this._distanceToCamera = 0; - this._centerZDepth = 0; // TODO : remove? + this._centerZDepth = 0; this._screenSpaceError = 0; this._visibilityPlaneMask = 0; this._visible = false; From 8d85e60ef97e9f04d4a531a20192dbc1c5fdbf1c Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 27 Jul 2018 21:21:48 -0400 Subject: [PATCH 51/53] Updates --- Source/Scene/Cesium3DTile.js | 27 ++++++++++++++++++------ Source/Scene/Cesium3DTilesetTraversal.js | 20 ++++++++---------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index fdae1766118b..b1bea8a2c1a1 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -262,7 +262,7 @@ define([ this.hasTilesetContent = false; /** - * The corresponding node in the cache. + * The node in the tileset's LRU cache, used to determine when to unload a tile's content. * * See {@link Cesium3DTilesetCache} * @@ -465,7 +465,7 @@ define([ */ contentAvailable : { get : function() { - return (this.contentReady && this.hasRenderableContent) || (defined(this._expiredContent) && this._contentState !== Cesium3DTileContentState.FAILED); + return (this.contentReady && this.hasRenderableContent) || (defined(this._expiredContent) && !this.contentFailed); } }, @@ -520,6 +520,23 @@ define([ } }, + /** + * Determines if the tile's content failed to load. true if the tile's + * content failed to load; otherwise, false. + * + * @memberof Cesium3DTile.prototype + * + * @type {Boolean} + * @readonly + * + * @private + */ + contentFailed : { + get : function() { + return this._contentState === Cesium3DTileContentState.FAILED; + } + }, + /** * Gets the promise that will be resolved when the tile's content is ready to process. * This happens after the content is downloaded but before the content is ready @@ -1035,12 +1052,10 @@ define([ var color; if (!tile._finalResolution) { color = Color.YELLOW; - } else if (hasContentBoundingVolume) { - color = Color.WHITE; } else if (empty) { - color = Color.GREEN; + color = Color.DARKGRAY; } else { - color = Color.RED; + color = Color.WHITE; } if (!defined(tile._debugBoundingVolume)) { tile._debugBoundingVolume = tile._boundingVolume.createDebugVolume(color); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 1131a87955ca..3a27113a282f 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -76,7 +76,7 @@ define([ return; } - // The SSE of not rendering the tree is small enough that the tree does not need to be rendered + // The tileset doesn't meet the SSE requirement, therefore the tree does not need to be rendered if (getScreenSpaceError(tileset, tileset._geometricError, root, frameState) <= tileset._maximumScreenSpaceError) { return; } @@ -213,17 +213,14 @@ define([ // Replacement tiles are prioritized by screen space error. // A tileset that has both additive and replacement tiles may not prioritize tiles as effectively since SSE and distance // are different types of values. Maybe all priorities need to be normalized to 0-1 range. - var parent = tile.parent; - var replace = tile.refine === Cesium3DTileRefine.REPLACE; - var add = tile.refine === Cesium3DTileRefine.ADD; - if (add) { + if (tile.refine === Cesium3DTileRefine.ADD) { return tile._distanceToCamera; - } else if (replace) { - var useParentScreenSpaceError = defined(parent) && (!skipLevelOfDetail(tileset) || (tile._screenSpaceError === 0.0)); - var screenSpaceError = useParentScreenSpaceError ? parent._screenSpaceError : tile._screenSpaceError; - var rootScreenSpaceError = tileset._root._screenSpaceError; - return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower values (e.g. root tile is highest priority) } + var parent = tile.parent; + var useParentScreenSpaceError = defined(parent) && (!skipLevelOfDetail(tileset) || (tile._screenSpaceError === 0.0)); + var screenSpaceError = useParentScreenSpaceError ? parent._screenSpaceError : tile._screenSpaceError; + var rootScreenSpaceError = tileset._root._screenSpaceError; + return rootScreenSpaceError - screenSpaceError; // Map higher SSE to lower values (e.g. root tile is highest priority) } function loadTile(tileset, tile, frameState) { @@ -478,6 +475,7 @@ define([ if (hasEmptyContent(tile)) { // Add empty tile just to show its debug bounding volume + // If the tile has tileset content load the external tileset addEmptyTile(tileset, tile, frameState); loadTile(tileset, tile, frameState); } else if (add) { @@ -554,7 +552,7 @@ define([ /** * Traverse the tree and check if their selected frame is the current frame. If so, add it to a selection queue. - * Furthermore, this is a preorder traversal so children tiles are selected before ancestor tiles. + * This is a preorder traversal so children tiles are selected before ancestor tiles. * * The reason for the preorder traversal is so that tiles can easily be marked with their * selection depth. A tile's _selectionDepth is its depth in the tree where all non-selected tiles are removed. From 4fc6f7e9652a1535cd37ca4ef1e5014da2495bcc Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 27 Jul 2018 21:41:28 -0400 Subject: [PATCH 52/53] More concise handling of finalResolution --- Source/Scene/Batched3DModel3DTileContent.js | 3 +-- Source/Scene/Cesium3DTileBatchTable.js | 3 ++- Source/Scene/Cesium3DTilesetTraversal.js | 11 +++-------- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js index b89874f33998..02303da0422a 100644 --- a/Source/Scene/Batched3DModel3DTileContent.js +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -482,8 +482,7 @@ define([ // If any commands were pushed, add derived commands var commandEnd = frameState.commandList.length; if ((commandStart < commandEnd) && (frameState.passes.render || frameState.passes.pick) && !defined(tileset.classificationType)) { - var finalResolution = this._tile._finalResolution; - this._batchTable.addDerivedCommands(frameState, commandStart, finalResolution); + this._batchTable.addDerivedCommands(frameState, commandStart); } }; diff --git a/Source/Scene/Cesium3DTileBatchTable.js b/Source/Scene/Cesium3DTileBatchTable.js index f8276570858f..9ad79c24ead0 100644 --- a/Source/Scene/Cesium3DTileBatchTable.js +++ b/Source/Scene/Cesium3DTileBatchTable.js @@ -1210,10 +1210,11 @@ define([ OPAQUE_AND_TRANSLUCENT : 2 }; - Cesium3DTileBatchTable.prototype.addDerivedCommands = function(frameState, commandStart, finalResolution) { + Cesium3DTileBatchTable.prototype.addDerivedCommands = function(frameState, commandStart) { var commandList = frameState.commandList; var commandEnd = commandList.length; var tile = this._content._tile; + var finalResolution = tile._finalResolution; var tileset = tile._tileset; var bivariateVisibilityTest = tileset._skipLevelOfDetail && tileset._hasMixedContent && frameState.context.stencilBuffer; var styleCommandsNeeded = getStyleCommandsNeeded(this); diff --git a/Source/Scene/Cesium3DTilesetTraversal.js b/Source/Scene/Cesium3DTilesetTraversal.js index 3a27113a282f..72447fbf8cda 100644 --- a/Source/Scene/Cesium3DTilesetTraversal.js +++ b/Source/Scene/Cesium3DTilesetTraversal.js @@ -121,7 +121,6 @@ define([ } function addEmptyTile(tileset, tile) { - tile._finalResolution = true; tileset._emptyTiles.push(tile); } @@ -162,7 +161,6 @@ define([ updateTile(tileset, child, frameState); touchTile(tileset, child, frameState); selectTile(tileset, child, frameState); - tile._finalResolution = true; } else if (child._depth - root._depth < descendantSelectionDepth) { // Continue traversing, but not too far stack.push(child); @@ -176,7 +174,6 @@ define([ if (tile.contentAvailable) { // The tile can be selected right away and does not require traverseAndSelect selectTile(tileset, tile, frameState); - tile._finalResolution = true; } return; } @@ -332,7 +329,7 @@ define([ tile.updateExpiration(); tile._shouldSelect = false; - tile._finalResolution = false; + tile._finalResolution = true; tile._ancestorWithContent = undefined; tile._ancestorWithContentAvailable = undefined; @@ -586,8 +583,8 @@ define([ var waitingTile = ancestorStack.peek(); if (waitingTile._stackLength === stack.length) { ancestorStack.pop(); - if (waitingTile === lastAncestor) { - waitingTile._finalResolution = true; + if (waitingTile !== lastAncestor) { + waitingTile._finalResolution = false; } selectTile(tileset, waitingTile, frameState); continue; @@ -609,7 +606,6 @@ define([ if (shouldSelect) { if (add) { selectTile(tileset, tile, frameState); - tile._finalResolution = true; } else { tile._selectionDepth = ancestorStack.length; if (tile._selectionDepth > 0) { @@ -618,7 +614,6 @@ define([ lastAncestor = tile; if (!traverse) { selectTile(tileset, tile, frameState); - tile._finalResolution = true; continue; } ancestorStack.push(tile); From 69911031ab97142a41f845be081b86175c38e5ba Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Fri, 27 Jul 2018 21:58:14 -0400 Subject: [PATCH 53/53] Remove tile.renderableContent --- Source/Scene/Cesium3DTile.js | 18 ++---------------- Source/Scene/Cesium3DTileset.js | 9 ++++----- Specs/Scene/Cesium3DTilesetSpec.js | 1 - 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index b1bea8a2c1a1..95aafb56ae90 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -235,19 +235,6 @@ define([ */ this.hasEmptyContent = hasEmptyContent; - /** - * When true, the tile's content is renderable. - *

    - * This is false until the tile's content is loaded. - *

    - * - * @type {Boolean} - * @readonly - * - * @private - */ - this.hasRenderableContent = false; - /** * When true, the tile's content points to an external tileset. *

    @@ -465,7 +452,7 @@ define([ */ contentAvailable : { get : function() { - return (this.contentReady && this.hasRenderableContent) || (defined(this._expiredContent) && !this.contentFailed); + return (this.contentReady && !this.hasEmptyContent && !this.hasTilesetContent) || (defined(this._expiredContent) && !this.contentFailed); } }, @@ -705,7 +692,6 @@ define([ if (defined(contentFactory)) { content = contentFactory(tileset, that, that._contentResource, arrayBuffer, 0); - that.hasRenderableContent = true; } else { // The content may be json instead content = Cesium3DTileContentFactory.json(tileset, that, that._contentResource, arrayBuffer, 0); @@ -750,7 +736,7 @@ define([ * @private */ Cesium3DTile.prototype.unloadContent = function() { - if (!this.hasRenderableContent) { + if (this.hasEmptyContent || this.hasTilesetContent) { return; } diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 3c69c64e85ee..4b464bd27e21 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -693,7 +693,6 @@ define([ that._url = resource.url; that._basePath = basePath; - // We don't know the distance of the tileset until tileset JSON file is loaded, so use the default distance for now return Cesium3DTileset.loadJson(resource); }) .then(function(tilesetJson) { @@ -1401,11 +1400,11 @@ define([ } if (expired) { - if (tile.hasRenderableContent) { + if (tile.hasTilesetContent) { + destroySubtree(tileset, tile); + } else { statistics.decrementLoadCounts(tile.content); --tileset._statistics.numberOfTilesWithContentReady; - } else if (tile.hasTilesetContent) { - destroySubtree(tileset, tile); } } @@ -1467,7 +1466,7 @@ define([ return function() { --tileset._statistics.numberOfTilesProcessing; - if (tile.hasRenderableContent) { + if (!tile.hasTilesetContent) { // RESEARCH_IDEA: ability to unload tiles (without content) for an // external tileset when all the tiles are unloaded. tileset._statistics.incrementLoadCounts(tile.content); diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index e6f76c631fb0..a0cc8e33b911 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1015,7 +1015,6 @@ defineSuite([ var root = tileset._root; root.refine = Cesium3DTileRefine.REPLACE; root.hasEmptyContent = false; // mock content - root.hasRenderableContent = true; // mock content tileset.maximumScreenSpaceError = 0.0; // Force root tile to always not meet SSE since this is just checking the request volume // Renders all 5 tiles