diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index c70dd3adeaa3..d3bc889c6096 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -86,17 +86,18 @@ define([ */ this.transform = defined(header.transform) ? Matrix4.unpack(header.transform) : Matrix4.clone(Matrix4.IDENTITY); + var parentTransform = defined(parent) ? parent.computedTransform : tileset.modelMatrix; + var computedTransform = Matrix4.multiply(parentTransform, this.transform, new Matrix4()); + /** * The final computed transform of this tile * @type {Matrix4} */ - var parentTransform = defined(parent) ? parent.computedTransform : tileset.modelMatrix; - this.computedTransform = Matrix4.multiply(parentTransform, this.transform, new Matrix4()); - this._computedTransform = Matrix4.clone(this.computedTransform); + this.computedTransform = computedTransform; this._transformDirty = true; - this._boundingVolume = this.createBoundingVolume(header.boundingVolume, this.computedTransform); + this._boundingVolume = this.createBoundingVolume(header.boundingVolume, computedTransform); var contentBoundingVolume; @@ -106,7 +107,7 @@ define([ // but not for culling for traversing the tree since it is not spatial coherence, i.e., // since it only bounds models in the tile, not the entire tile, children may be // outside of this box. - contentBoundingVolume = this.createBoundingVolume(contentHeader.boundingVolume, this.computedTransform); + contentBoundingVolume = this.createBoundingVolume(contentHeader.boundingVolume, computedTransform); } this._contentBoundingVolume = contentBoundingVolume; @@ -605,6 +606,35 @@ define([ } }; + var scratchTransform = new Matrix4(); + + /** + * Update the tile's transform. The transform is applied to the tile's bounding volumes. + * + * @private + */ + Cesium3DTile.prototype.updateTransform = function(parentTransform) { + parentTransform = defaultValue(parentTransform, Matrix4.IDENTITY); + var computedTransform = Matrix4.multiply(parentTransform, this.transform, scratchTransform); + var transformDirty = !Matrix4.equals(computedTransform, this.computedTransform); + if (transformDirty) { + this._transformDirty = true; + Matrix4.clone(computedTransform, this.computedTransform); + + // Update the bounding volumes + var header = this._header; + var content = this._header.content; + this._boundingVolume = this.createBoundingVolume(header.boundingVolume, computedTransform, this._boundingVolume); + if (defined(this._contentBoundingVolume)) { + this._contentBoundingVolume = this.createBoundingVolume(content.boundingVolume, computedTransform, this._contentBoundingVolume); + } + + // Destroy the debug bounding volumes. They will be generated fresh. + this._debugBoundingVolume = this._debugBoundingVolume && this._debugBoundingVolume.destroy(); + this._debugContentBoundingVolume = this._debugContentBoundingVolume && this._debugContentBoundingVolume.destroy(); + } + }; + function applyDebugSettings(tile, tileset, frameState) { // Tiles do not have a content.boundingVolume if it is the same as the tile's boundingVolume. var hasContentBoundingVolume = defined(tile._header.content) && defined(tile._header.content.boundingVolume); @@ -637,35 +667,15 @@ define([ } } - function updateTransform(tile) { - var transformDirty = !Matrix4.equals(tile.computedTransform, tile._computedTransform); - if (transformDirty) { - Matrix4.clone(tile.computedTransform, tile._computedTransform); - - // Update the bounding volumes - var header = tile._header; - var content = tile._header.content; - tile._boundingVolume = tile.createBoundingVolume(header.boundingVolume, tile.computedTransform, tile._boundingVolume); - if (defined(tile._contentBoundingVolume)) { - tile._contentBoundingVolume = tile.createBoundingVolume(content.boundingVolume, tile.computedTransform, tile._contentBoundingVolume); - } - - // Destroy the debug bounding volumes. They will be generated fresh. - tile._debugBoundingVolume = tile._debugBoundingVolume && tile._debugBoundingVolume.destroy(); - tile._debugContentBoundingVolume = tile._debugContentBoundingVolume && tile._debugContentBoundingVolume.destroy(); - } - tile._transformDirty = transformDirty; - } - /** * Get the draw commands needed to render this tile. * * @private */ Cesium3DTile.prototype.update = function(tileset, frameState) { - updateTransform(this); applyDebugSettings(this, tileset, frameState); this._content.update(tileset, frameState); + this._transformDirty = false; }; var scratchCommandList = []; diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 89402c7460d3..e80c8f1d8a8a 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -142,13 +142,7 @@ define([ this._maximumNumberOfLoadedTiles = defaultValue(options.maximumNumberOfLoadedTiles, 256); this._styleEngine = new Cesium3DTileStyleEngine(); - /** - * A 4x4 transformation matrix that transforms the tileset's root tile. - * - * @type {Matrix4} - * @default Matrix4.IDENTITY - */ - this.modelMatrix = defined(options.modelMatrix) ? options.modelMatrix : Matrix4.clone(Matrix4.IDENTITY); + this._modelMatrix = defined(options.modelMatrix) ? Matrix4.clone(options.modelMatrix) : Matrix4.clone(Matrix4.IDENTITY); /** * This property is for debugging only; it is not optimized for production use. @@ -635,6 +629,26 @@ define([ } }, + /** + * A 4x4 transformation matrix that transforms the tileset's root tile. + * + * @type {Matrix4} + * @default Matrix4.IDENTITY + */ + modelMatrix : { + get : function() { + return this._modelMatrix; + }, + set : function(value) { + this._modelMatrix = Matrix4.clone(value, this._modelMatrix); + if (defined(this._root)) { + // Update the root transform right away instead of waiting for the next update loop. + // Useful, for example, when setting the modelMatrix and then having the camera view the tileset. + this._root.updateTransform(this._modelMatrix); + } + } + }, + /** * @private */ @@ -797,6 +811,14 @@ define([ } } + function updateTransforms(children, parentTransform) { + var length = children.length; + for (var i = 0; i < length; ++i) { + var child = children[i]; + child.updateTransform(parentTransform); + } + } + // 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) { @@ -884,6 +906,7 @@ define([ replacementList.splice(replacementList.tail, tileset._replacementSentinel); var root = tileset._root; + root.updateTransform(tileset._modelMatrix); root.distanceToCamera = root.distanceToTile(frameState); if (getScreenSpaceError(tileset._geometricError, root, frameState) <= maximumScreenSpaceError) { @@ -912,9 +935,6 @@ define([ t.replaced = false; ++stats.visited; - var parentTransform = defined(t.parent) ? t.parent.computedTransform : tileset.modelMatrix; - t.computedTransform = Matrix4.multiply(parentTransform, t.transform, t.computedTransform); - var visibilityPlaneMask = t.visibilityPlaneMask; var fullyVisible = (visibilityPlaneMask === CullingVolume.MASK_INSIDE); @@ -938,6 +958,7 @@ define([ child = t.children[0]; child.visibilityPlaneMask = t.visibilityPlaneMask; child.distanceToCamera = t.distanceToCamera; + child.updateTransform(t.computedTransform); if (child.contentUnloaded) { requestContent(tileset, child, outOfCore); } else { @@ -959,6 +980,8 @@ define([ // children are loaded or request slots are available. var anyChildrenLoaded = (t.numberOfChildrenWithoutContent < childrenLength); if (anyChildrenLoaded || t.canRequestContent()) { + updateTransforms(children, t.computedTransform); + // Distance is used for sorting now and for computing SSE when the tile comes off the stack. computeDistanceToCamera(children, frameState); @@ -1004,6 +1027,8 @@ define([ var allChildrenLoaded = t.numberOfChildrenWithoutContent === 0; if (allChildrenLoaded || t.canRequestContent()) { + updateTransforms(children, t.computedTransform); + // Distance is used for sorting now and for computing SSE when the tile comes off the stack. computeDistanceToCamera(children, frameState); @@ -1058,6 +1083,7 @@ define([ var someVisibleChildrenLoaded = false; for (k = 0; k < childrenLength; ++k) { child = children[k]; + child.updateTransform(t.computedTransform); child.visibilityPlaneMask = child.visibility(frameState.cullingVolume, visibilityPlaneMask); if (isVisible(child.visibilityPlaneMask)) { if (child.contentReady) { diff --git a/Specs/Scene/Cesium3DTileSpec.js b/Specs/Scene/Cesium3DTileSpec.js index 59131e0d0bfd..10a64c7e9ace 100644 --- a/Specs/Scene/Cesium3DTileSpec.js +++ b/Specs/Scene/Cesium3DTileSpec.js @@ -256,8 +256,8 @@ defineSuite([ // Change the transform var newLongitude = -1.012; var newLatitude = 0.698874; - tile.computedTransform = getTileTransform(newLongitude, newLatitude); - tile.update(mockTileset); + tile.transform = getTileTransform(newLongitude, newLatitude); + tile.updateTransform(); // Check the new transform var newCenter = Cartesian3.fromRadians(newLongitude, newLatitude); diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 047d737d7767..64986b4f83c4 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1522,6 +1522,7 @@ defineSuite([ it('propagates tile transform down the tree', function() { return Cesium3DTilesTester.loadTileset(scene, tilesetWithTransformsUrl).then(function(tileset) { scene.renderForSpecs(); + var stats = tileset._statistics; var root = tileset._root; var rootTransform = Matrix4.unpack(root._header.transform); @@ -1529,7 +1530,7 @@ defineSuite([ var childTransform = Matrix4.unpack(child._header.transform); var computedTransform = Matrix4.multiply(rootTransform, childTransform, new Matrix4()); - expect(tileset._selectedTiles.length).toBe(2); + expect(stats.numberOfCommands).toBe(2); expect(root.computedTransform).toEqual(rootTransform); expect(child.computedTransform).toEqual(computedTransform); @@ -1539,6 +1540,24 @@ defineSuite([ computedTransform = Matrix4.multiply(tilesetTransform, computedTransform, computedTransform); scene.renderForSpecs(); expect(child.computedTransform).toEqual(computedTransform); + + // Set the modelMatrix somewhere off screen + tileset.modelMatrix = Matrix4.fromTranslation(new Cartesian3(0.0, 100000.0, 0.0)); + scene.renderForSpecs(); + expect(stats.numberOfCommands).toBe(0); + + // Now bring it back + tileset.modelMatrix = Matrix4.IDENTITY; + scene.renderForSpecs(); + expect(stats.numberOfCommands).toBe(2); + + // Do the same steps for a tile transform + child.transform = Matrix4.fromTranslation(new Cartesian3(0.0, 100000.0, 0.0)); + scene.renderForSpecs(); + expect(stats.numberOfCommands).toBe(1); + child.transform = Matrix4.IDENTITY; + scene.renderForSpecs(); + expect(stats.numberOfCommands).toBe(2); }); }); diff --git a/Specs/Scene/PointCloud3DTileContentSpec.js b/Specs/Scene/PointCloud3DTileContentSpec.js index aa3362a4e294..543506714ebf 100644 --- a/Specs/Scene/PointCloud3DTileContentSpec.js +++ b/Specs/Scene/PointCloud3DTileContentSpec.js @@ -226,7 +226,6 @@ defineSuite([ // Update tile transform tileset._root.transform = newTransform; - scene.renderForSpecs(); // Move the camera to the new location setCamera(newLongitude, newLatitude);