diff --git a/Apps/Sandcastle/gallery/Ground Clamping.html b/Apps/Sandcastle/gallery/Ground Clamping.html index f8ee54a9b1dc..bba58ebb09e9 100644 --- a/Apps/Sandcastle/gallery/Ground Clamping.html +++ b/Apps/Sandcastle/gallery/Ground Clamping.html @@ -121,6 +121,30 @@ viewer.zoomTo(e); } }, { + text : 'Draw Textured Polygon', + onselect : function() { + if (!Cesium.Entity.supportsMaterialsforEntitiesOnTerrain(viewer.scene)) { + window.alert('Terrain Entity materials are not supported on this platform'); + return; + } + + var e = viewer.entities.add({ + polygon : { + hierarchy : { + positions : [new Cesium.Cartesian3(-2358138.847340281, -3744072.459541374, 4581158.5714175375), + new Cesium.Cartesian3(-2357231.4925370603, -3745103.7886602185, 4580702.9757762635), + new Cesium.Cartesian3(-2355912.902205431, -3744249.029778454, 4582402.154378103), + new Cesium.Cartesian3(-2357208.0209552636, -3743553.4420488174, 4581961.863286629)] + }, + material : '../images/Cesium_Logo_Color.jpg', + classificationType : Cesium.ClassificationType.TERRAIN, + stRotation : Cesium.Math.toRadians(45) + } + }); + + viewer.zoomTo(e); + } +}, { text : 'Draw Rectangle', onselect : function() { var e = viewer.entities.add({ diff --git a/Apps/Sandcastle/gallery/development/Ground Primitive Materials.html b/Apps/Sandcastle/gallery/development/Ground Primitive Materials.html new file mode 100644 index 000000000000..53990f9fc3b7 --- /dev/null +++ b/Apps/Sandcastle/gallery/development/Ground Primitive Materials.html @@ -0,0 +1,489 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/development/Ground Primitive Materials.jpg b/Apps/Sandcastle/gallery/development/Ground Primitive Materials.jpg new file mode 100644 index 000000000000..0a3cf86f4672 Binary files /dev/null and b/Apps/Sandcastle/gallery/development/Ground Primitive Materials.jpg differ diff --git a/Apps/Sandcastle/gallery/development/Terrain Entity Batching.html b/Apps/Sandcastle/gallery/development/Terrain Entity Batching.html new file mode 100644 index 000000000000..35f819dacf22 --- /dev/null +++ b/Apps/Sandcastle/gallery/development/Terrain Entity Batching.html @@ -0,0 +1,128 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/development/Terrain Entity Batching.jpg b/Apps/Sandcastle/gallery/development/Terrain Entity Batching.jpg new file mode 100644 index 000000000000..b2befd150259 Binary files /dev/null and b/Apps/Sandcastle/gallery/development/Terrain Entity Batching.jpg differ diff --git a/CHANGES.md b/CHANGES.md index 8a1b8a61a900..10adb3be2177 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,16 @@ Change Log * `ProviderViewModel`s with no category are displayed in an untitled group in `BaseLayerPicker` instead of being labeled as `'Other'` [#6574](https://github.com/AnalyticalGraphicsInc/cesium/pull/6574) * Added a workaround for clipping planes causing a picking shader compilation failure for gltf models and 3D Tilesets in Internet Explorer [#6575](https://github.com/AnalyticalGraphicsInc/cesium/issues/6575) +##### Breaking Changes :mega: +* Removed `Scene.copyGlobeDepth`. Globe depth will now be copied by default when supported. [#6393](https://github.com/AnalyticalGraphicsInc/cesium/pull/6393) + +##### Additions :tada: +* Added support for materials on terrain entities (entities with unspecified `height`) and `GroundPrimitives`. [#6393](https://github.com/AnalyticalGraphicsInc/cesium/pull/6393) + * Only available for `ClassificationType.TERRAIN` at this time. Adding a material to a terrain `Entity` will cause it to behave as if it is `ClassificationType.TERRAIN`. + * Requires depth texture support (`WEBGL_depth_texture` or `WEBKIT_WEBGL_depth_texture`), so materials on terrain entities and `GroundPrimitives` are not supported in Internet Explorer. + * Best suited for notational patterns and not intended for precisely mapping textures to terrain - for that use case, use `SingleTileImageryProvider`. +* Added `GroundPrimitive.supportsMaterials` and `Entity.supportsMaterialsforEntitiesOnTerrain`, both of which can be used to check if materials on terrain entities and `GroundPrimitives` is supported. [#6393](https://github.com/AnalyticalGraphicsInc/cesium/pull/6393) + ### 1.45 - 2018-05-01 ##### Major Announcements :loudspeaker: diff --git a/LICENSE.md b/LICENSE.md index 4c062b3fc734..abd8b643d3dc 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -509,6 +509,52 @@ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +### rbush + +https://github.com/mourner/rbush + +> MIT License + +> Copyright (c) 2016 Vladimir Agafonkin + +>Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +> +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +> +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +### quickselect + +https://github.com/mourner/quickselect + +> ISC License + +> Copyright (c) 2018, Vladimir Agafonkin + +>Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. +> +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + ### crunch https://github.com/BinomialLLC/crunch @@ -670,6 +716,20 @@ https://github.com/KhronosGroup/glTF-WebGL-PBR >CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE >OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +### ShaderFastLibs + +https://github.com/michaldrobot/ShaderFastLibs + +> The MIT License (MIT) +> +> Copyright (c) <2014> +> +> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ### Draco https://github.com/google/draco @@ -692,9 +752,9 @@ https://github.com/leizongmin/js-xss > Copyright (c) 2012-2017 Zongmin Lei(雷宗民) > http://ucdok.com -> +> > The MIT License -> +> > Permission is hereby granted, free of charge, to any person obtaining > a copy of this software and associated documentation files (the > "Software"), to deal in the Software without restriction, including @@ -702,10 +762,10 @@ https://github.com/leizongmin/js-xss > distribute, sublicense, and/or sell copies of the Software, and to > permit persons to whom the Software is furnished to do so, subject to > the following conditions: -> +> > The above copyright notice and this permission notice shall be > included in all copies or substantial portions of the Software. -> +> > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND diff --git a/Source/Core/CircleGeometry.js b/Source/Core/CircleGeometry.js index cfc74b95eb5c..1a6df4e04ab0 100644 --- a/Source/Core/CircleGeometry.js +++ b/Source/Core/CircleGeometry.js @@ -184,6 +184,15 @@ define([ get : function() { return this._ellipseGeometry.rectangle; } + }, + /** + * For remapping texture coordinates when rendering CircleGeometries as GroundPrimitives. + * @private + */ + textureCoordinateRotationPoints : { + get : function() { + return this._ellipseGeometry.textureCoordinateRotationPoints; + } } }); diff --git a/Source/Core/CorridorGeometry.js b/Source/Core/CorridorGeometry.js index ae6afca4dade..476a1c3a3058 100644 --- a/Source/Core/CorridorGeometry.js +++ b/Source/Core/CorridorGeometry.js @@ -1045,6 +1045,18 @@ define([ } return this._rectangle; } + }, + /** + * For remapping texture coordinates when rendering CorridorGeometries as GroundPrimitives. + * + * Corridors don't support stRotation, + * so just return the corners of the original system. + * @private + */ + textureCoordinateRotationPoints : { + get : function() { + return [0, 0, 0, 1, 1, 0]; + } } }); diff --git a/Source/Core/EllipseGeometry.js b/Source/Core/EllipseGeometry.js index 76eeab379451..246d41320cbb 100644 --- a/Source/Core/EllipseGeometry.js +++ b/Source/Core/EllipseGeometry.js @@ -755,6 +755,7 @@ define([ this._workerName = 'createEllipseGeometry'; this._rectangle = undefined; + this._textureCoordinateRotationPoints = undefined; } /** @@ -953,6 +954,31 @@ define([ }); }; + function textureCoordinateRotationPoints(ellipseGeometry) { + var stRotation = -ellipseGeometry._stRotation; + if (stRotation === 0.0) { + return [0, 0, 0, 1, 1, 0]; + } + + var cep = EllipseGeometryLibrary.computeEllipsePositions({ + center : ellipseGeometry._center, + semiMajorAxis : ellipseGeometry._semiMajorAxis, + semiMinorAxis : ellipseGeometry._semiMinorAxis, + rotation : ellipseGeometry._rotation, + granularity : ellipseGeometry._granularity + }, false, true); + var positionsFlat = cep.outerPositions; + var positionsCount = positionsFlat.length / 3; + var positions = new Array(positionsCount); + for (var i = 0; i < positionsCount; ++i) { + positions[i] = Cartesian3.fromArray(positionsFlat, i * 3); + } + + var ellipsoid = ellipseGeometry._ellipsoid; + var boundingRectangle = ellipseGeometry.rectangle; + return Geometry._textureCoordinateRotationPoints(positions, stRotation, ellipsoid, boundingRectangle); + } + defineProperties(EllipseGeometry.prototype, { /** * @private @@ -964,6 +990,18 @@ define([ } return this._rectangle; } + }, + /** + * For remapping texture coordinates when rendering EllipseGeometries as GroundPrimitives. + * @private + */ + textureCoordinateRotationPoints : { + get : function() { + if (!defined(this._textureCoordinateRotationPoints)) { + this._textureCoordinateRotationPoints = textureCoordinateRotationPoints(this); + } + return this._textureCoordinateRotationPoints; + } } }); diff --git a/Source/Core/Geometry.js b/Source/Core/Geometry.js index 48e8fdd673d2..7190996c80ed 100644 --- a/Source/Core/Geometry.js +++ b/Source/Core/Geometry.js @@ -1,17 +1,35 @@ define([ + './Cartesian2', + './Cartesian3', + './Cartographic', './Check', './defaultValue', './defined', './DeveloperError', './GeometryType', - './PrimitiveType' + './Matrix2', + './Matrix3', + './Matrix4', + './PrimitiveType', + './Quaternion', + './Rectangle', + './Transforms' ], function( + Cartesian2, + Cartesian3, + Cartographic, Check, defaultValue, defined, DeveloperError, GeometryType, - PrimitiveType) { + Matrix2, + Matrix3, + Matrix4, + PrimitiveType, + Quaternion, + Rectangle, + Transforms) { 'use strict'; /** @@ -195,5 +213,131 @@ define([ return numberOfVertices; }; + var rectangleCenterScratch = new Cartographic(); + var enuCenterScratch = new Cartesian3(); + var fixedFrameToEnuScratch = new Matrix4(); + var boundingRectanglePointsCartographicScratch = [new Cartographic(), new Cartographic(), new Cartographic()]; + var boundingRectanglePointsEnuScratch = [new Cartesian2(), new Cartesian2(), new Cartesian2()]; + var points2DScratch = [new Cartesian2(), new Cartesian2(), new Cartesian2()]; + var pointEnuScratch = new Cartesian3(); + var enuRotationScratch = new Quaternion(); + var enuRotationMatrixScratch = new Matrix4(); + var rotation2DScratch = new Matrix2(); + + /** + * For remapping texture coordinates when rendering GroundPrimitives with materials. + * GroundPrimitive texture coordinates are computed to align with the cartographic coordinate system on the globe. + * However, EllipseGeometry, RectangleGeometry, and PolygonGeometry all bake rotations to per-vertex texture coordinates + * using different strategies. + * + * This method is used by EllipseGeometry and PolygonGeometry to approximate the same visual effect. + * We encapsulate rotation and scale by computing a "transformed" texture coordinate system and computing + * a set of reference points from which "cartographic" texture coordinates can be remapped to the "transformed" + * system using distances to lines in 2D. + * + * This approximation becomes less accurate as the covered area increases, especially for GroundPrimitives near the poles, + * but is generally reasonable for polygons and ellipses around the size of USA states. + * + * RectangleGeometry has its own version of this method that computes remapping coordinates using cartographic space + * as an intermediary instead of local ENU, which is more accurate for large-area rectangles. + * + * @param {Cartesian3[]} positions Array of positions outlining the geometry + * @param {Number} stRotation Texture coordinate rotation. + * @param {Ellipsoid} ellipsoid Ellipsoid for projecting and generating local vectors. + * @param {Rectangle} boundingRectangle Bounding rectangle around the positions. + * @returns {Number[]} An array of 6 numbers specifying [minimum point, u extent, v extent] as points in the "cartographic" system. + * @private + */ + Geometry._textureCoordinateRotationPoints = function(positions, stRotation, ellipsoid, boundingRectangle) { + var i; + + // Create a local east-north-up coordinate system centered on the polygon's bounding rectangle. + // Project the southwest, northwest, and southeast corners of the bounding rectangle into the plane of ENU as 2D points. + // These are the equivalents of (0,0), (0,1), and (1,0) in the texture coordiante system computed in ShadowVolumeAppearanceFS, + // aka "ENU texture space." + var rectangleCenter = Rectangle.center(boundingRectangle, rectangleCenterScratch); + var enuCenter = Cartographic.toCartesian(rectangleCenter, ellipsoid, enuCenterScratch); + var enuToFixedFrame = Transforms.eastNorthUpToFixedFrame(enuCenter, ellipsoid, fixedFrameToEnuScratch); + var fixedFrameToEnu = Matrix4.inverse(enuToFixedFrame, fixedFrameToEnuScratch); + + var boundingPointsEnu = boundingRectanglePointsEnuScratch; + var boundingPointsCarto = boundingRectanglePointsCartographicScratch; + + boundingPointsCarto[0].longitude = boundingRectangle.west; + boundingPointsCarto[0].latitude = boundingRectangle.south; + + boundingPointsCarto[1].longitude = boundingRectangle.west; + boundingPointsCarto[1].latitude = boundingRectangle.north; + + boundingPointsCarto[2].longitude = boundingRectangle.east; + boundingPointsCarto[2].latitude = boundingRectangle.south; + + var posEnu = pointEnuScratch; + + for (i = 0; i < 3; i++) { + Cartographic.toCartesian(boundingPointsCarto[i], ellipsoid, posEnu); + posEnu = Matrix4.multiplyByPointAsVector(fixedFrameToEnu, posEnu, posEnu); + boundingPointsEnu[i].x = posEnu.x; + boundingPointsEnu[i].y = posEnu.y; + } + + // Rotate each point in the polygon around the up vector in the ENU by -stRotation and project into ENU as 2D. + // Compute the bounding box of these rotated points in the 2D ENU plane. + // Rotate the corners back by stRotation, then compute their equivalents in the ENU texture space using the corners computed earlier. + var rotation = Quaternion.fromAxisAngle(Cartesian3.UNIT_Z, -stRotation, enuRotationScratch); + var textureMatrix = Matrix3.fromQuaternion(rotation, enuRotationMatrixScratch); + + var positionsLength = positions.length; + var enuMinX = Number.POSITIVE_INFINITY; + var enuMinY = Number.POSITIVE_INFINITY; + var enuMaxX = Number.NEGATIVE_INFINITY; + var enuMaxY = Number.NEGATIVE_INFINITY; + for (i = 0; i < positionsLength; i++) { + posEnu = Matrix4.multiplyByPointAsVector(fixedFrameToEnu, positions[i], posEnu); + posEnu = Matrix3.multiplyByVector(textureMatrix, posEnu, posEnu); + + enuMinX = Math.min(enuMinX, posEnu.x); + enuMinY = Math.min(enuMinY, posEnu.y); + enuMaxX = Math.max(enuMaxX, posEnu.x); + enuMaxY = Math.max(enuMaxY, posEnu.y); + } + + var toDesiredInComputed = Matrix2.fromRotation(stRotation, rotation2DScratch); + + var points2D = points2DScratch; + points2D[0].x = enuMinX; + points2D[0].y = enuMinY; + + points2D[1].x = enuMinX; + points2D[1].y = enuMaxY; + + points2D[2].x = enuMaxX; + points2D[2].y = enuMinY; + + var boundingEnuMin = boundingPointsEnu[0]; + var boundingPointsWidth = boundingPointsEnu[2].x - boundingEnuMin.x; + var boundingPointsHeight = boundingPointsEnu[1].y - boundingEnuMin.y; + + for (i = 0; i < 3; i++) { + var point2D = points2D[i]; + // rotate back + Matrix2.multiplyByVector(toDesiredInComputed, point2D, point2D); + + // Convert point into east-north texture coordinate space + point2D.x = (point2D.x - boundingEnuMin.x) / boundingPointsWidth; + point2D.y = (point2D.y - boundingEnuMin.y) / boundingPointsHeight; + } + + var minXYCorner = points2D[0]; + var maxYCorner = points2D[1]; + var maxXCorner = points2D[2]; + var result = new Array(6); + Cartesian2.pack(minXYCorner, result); + Cartesian2.pack(maxYCorner, result, 2); + Cartesian2.pack(maxXCorner, result, 4); + + return result; + }; + return Geometry; }); diff --git a/Source/Core/Math.js b/Source/Core/Math.js index b59f9be8f1cc..794b1453c72f 100644 --- a/Source/Core/Math.js +++ b/Source/Core/Math.js @@ -1,10 +1,12 @@ define([ '../ThirdParty/mersenne-twister', + './Check', './defaultValue', './defined', './DeveloperError' ], function( MersenneTwister, + Check, defaultValue, defined, DeveloperError) { @@ -857,5 +859,63 @@ define([ return 1.0 - Math.exp(-(scalar * scalar)); }; + /** + * Computes a fast approximation of Atan for input in the range [-1, 1]. + * + * Based on Michal Drobot's approximation from ShaderFastLibs, + * which in turn is based on "Efficient approximations for the arctangent function," + * Rajan, S. Sichun Wang Inkol, R. Joyal, A., May 2006. + * Adapted from ShaderFastLibs under MIT License. + * + * @param {Number} x An input number in the range [-1, 1] + * @returns {Number} An approximation of atan(x) + */ + CesiumMath.fastApproximateAtan = function(x) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('x', x); + //>>includeEnd('debug'); + + return x * (-0.1784 * Math.abs(x) - 0.0663 * x * x + 1.0301); + }; + + /** + * Computes a fast approximation of Atan2(x, y) for arbitrary input scalars. + * + * Range reduction math based on nvidia's cg reference implementation: http://developer.download.nvidia.com/cg/atan2.html + * + * @param {Number} x An input number that isn't zero if y is zero. + * @param {Number} y An input number that isn't zero if x is zero. + * @returns {Number} An approximation of atan2(x, y) + */ + CesiumMath.fastApproximateAtan2 = function(x, y) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('x', x); + Check.typeOf.number('y', y); + //>>includeEnd('debug'); + + // atan approximations are usually only reliable over [-1, 1] + // So reduce the range by flipping whether x or y is on top based on which is bigger. + var opposite; + var adjacent; + var t = Math.abs(x); // t used as swap and atan result. + opposite = Math.abs(y); + adjacent = Math.max(t, opposite); + opposite = Math.min(t, opposite); + + var oppositeOverAdjacent = opposite / adjacent; + //>>includeStart('debug', pragmas.debug); + if (isNaN(oppositeOverAdjacent)) { + throw new DeveloperError('either x or y must be nonzero'); + } + //>>includeEnd('debug'); + t = CesiumMath.fastApproximateAtan(oppositeOverAdjacent); + + // Undo range reduction + t = Math.abs(y) > Math.abs(x) ? CesiumMath.PI_OVER_TWO - t : t; + t = x < 0.0 ? CesiumMath.PI - t : t; + t = y < 0.0 ? -t : t; + return t; + }; + return CesiumMath; }); diff --git a/Source/Core/PolygonGeometry.js b/Source/Core/PolygonGeometry.js index 4045486b1509..ddd97f6c05a1 100644 --- a/Source/Core/PolygonGeometry.js +++ b/Source/Core/PolygonGeometry.js @@ -18,6 +18,7 @@ define([ './GeometryPipeline', './IndexDatatype', './Math', + './Matrix2', './Matrix3', './PolygonGeometryLibrary', './PolygonPipeline', @@ -45,6 +46,7 @@ define([ GeometryPipeline, IndexDatatype, CesiumMath, + Matrix2, Matrix3, PolygonGeometryLibrary, PolygonPipeline, @@ -595,6 +597,7 @@ define([ this._workerName = 'createPolygonGeometry'; this._rectangle = undefined; + this._textureCoordinateRotationPoints = undefined; /** * The number of elements used to pack the object into an array. @@ -907,6 +910,17 @@ define([ }); }; + function textureCoordinateRotationPoints(polygonGeometry) { + var stRotation = -polygonGeometry._stRotation; + if (stRotation === 0.0) { + return [0, 0, 0, 1, 1, 0]; + } + var ellipsoid = polygonGeometry._ellipsoid; + var positions = polygonGeometry._polygonHierarchy.positions; + var boundingRectangle = polygonGeometry.rectangle; + return Geometry._textureCoordinateRotationPoints(positions, stRotation, ellipsoid, boundingRectangle); + } + defineProperties(PolygonGeometry.prototype, { /** * @private @@ -924,6 +938,18 @@ define([ return this._rectangle; } + }, + /** + * For remapping texture coordinates when rendering PolygonGeometries as GroundPrimitives. + * @private + */ + textureCoordinateRotationPoints : { + get : function() { + if (!defined(this._textureCoordinateRotationPoints)) { + this._textureCoordinateRotationPoints = textureCoordinateRotationPoints(this); + } + return this._textureCoordinateRotationPoints; + } } }); diff --git a/Source/Core/RectangleCollisionChecker.js b/Source/Core/RectangleCollisionChecker.js new file mode 100644 index 000000000000..400942c27b16 --- /dev/null +++ b/Source/Core/RectangleCollisionChecker.js @@ -0,0 +1,90 @@ +define([ + '../ThirdParty/rbush', + './Check' + ], function( + rbush, + Check) { + 'use strict'; + + /** + * Wrapper around rbush for use with Rectangle types. + * @private + */ + function RectangleCollisionChecker() { + this._tree = rbush(); + } + + function RectangleWithId() { + this.minX = 0.0; + this.minY = 0.0; + this.maxX = 0.0; + this.maxY = 0.0; + this.id = ''; + } + + RectangleWithId.fromRectangleAndId = function(id, rectangle, result) { + result.minX = rectangle.west; + result.minY = rectangle.south; + result.maxX = rectangle.east; + result.maxY = rectangle.north; + result.id = id; + return result; + }; + + /** + * Insert a rectangle into the collision checker. + * + * @param {String} id Unique string ID for the rectangle being inserted. + * @param {Rectangle} rectangle A Rectangle + * @private + */ + RectangleCollisionChecker.prototype.insert = function(id, rectangle) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('id', id); + Check.typeOf.object('rectangle', rectangle); + //>>includeEnd('debug'); + + var withId = RectangleWithId.fromRectangleAndId(id, rectangle, new RectangleWithId()); + this._tree.insert(withId); + }; + + function idCompare(a, b) { + return a.id === b.id; + } + + var removalScratch = new RectangleWithId(); + /** + * Remove a rectangle from the collision checker. + * + * @param {String} id Unique string ID for the rectangle being removed. + * @param {Rectangle} rectangle A Rectangle + * @private + */ + RectangleCollisionChecker.prototype.remove = function(id, rectangle) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.string('id', id); + Check.typeOf.object('rectangle', rectangle); + //>>includeEnd('debug'); + + var withId = RectangleWithId.fromRectangleAndId(id, rectangle, removalScratch); + this._tree.remove(withId, idCompare); + }; + + var collisionScratch = new RectangleWithId(); + /** + * Checks if a given rectangle collides with any of the rectangles in the collection. + * + * @param {Rectangle} rectangle A Rectangle that should be checked against the rectangles in the collision checker. + * @returns {Boolean} Whether the rectangle collides with any of the rectangles in the collision checker. + */ + RectangleCollisionChecker.prototype.collides = function(rectangle) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('rectangle', rectangle); + //>>includeEnd('debug'); + + var withId = RectangleWithId.fromRectangleAndId('', rectangle, collisionScratch); + return this._tree.collides(withId); + }; + + return RectangleCollisionChecker; +}); diff --git a/Source/Core/RectangleGeometry.js b/Source/Core/RectangleGeometry.js index 26f1df8f5616..0f5016d3a0c6 100644 --- a/Source/Core/RectangleGeometry.js +++ b/Source/Core/RectangleGeometry.js @@ -17,6 +17,7 @@ define([ './GeometryPipeline', './IndexDatatype', './Math', + './Matrix2', './Matrix3', './PolygonPipeline', './PrimitiveType', @@ -43,6 +44,7 @@ define([ GeometryPipeline, IndexDatatype, CesiumMath, + Matrix2, Matrix3, PolygonPipeline, PrimitiveType, @@ -651,6 +653,8 @@ define([ this._shadowVolume = defaultValue(options.shadowVolume, false); this._workerName = 'createRectangleGeometry'; this._rotatedRectangle = undefined; + + this._textureCoordinateRotationPoints = undefined; } /** @@ -861,6 +865,76 @@ define([ }); }; + var scratchRectangleGeometry = new RectangleGeometry({ + rectangle : new Rectangle() + }); + var unrotatedTextureRectangleScratch = new Rectangle(); + var points2DScratch = [new Cartesian2(), new Cartesian2(), new Cartesian2()]; + var rotation2DScratch = new Matrix2(); + var rectangleCenterScratch = new Cartographic(); + + function textureCoordinateRotationPoints(rectangleGeometry) { + if (rectangleGeometry._stRotation === 0.0) { + return [0, 0, 0, 1, 1, 0]; + } + // Compute rectangle if rectangleGeometry was rotated so that the texture coordinate system lined up with ENU + var rotatedRectangle = scratchRectangleGeometry; + + rotatedRectangle._rectangle = Rectangle.clone(rectangleGeometry._rectangle, rotatedRectangle._rectangle); + rotatedRectangle._granularity = rectangleGeometry._granularity; + rotatedRectangle._ellipsoid = Ellipsoid.clone(rectangleGeometry._ellipsoid, rotatedRectangle._ellipsoid); + rotatedRectangle._surfaceHeight = rectangleGeometry._surfaceHeight; + + // Rotate to align the texture coordinates with ENU + rotatedRectangle._rotation = rectangleGeometry._rotation - rectangleGeometry._stRotation; + + var unrotatedTextureRectangle = computeRectangle(rotatedRectangle, unrotatedTextureRectangleScratch); + + // Assume a computed "east-north" texture coordinate system based on spherical or planar tricks, bounded by `boundingRectangle`. + // The "desired" texture coordinate system forms an oriented rectangle (un-oriented computed) around the geometry that completely and tightly bounds it. + // We want to map from the "east-north" texture coordinate system into the "desired" system using a pair of lines (analagous planes in 2D) + // Compute 3 corners of the "desired" texture coordinate system in "east-north" texture space by the following in cartographic space: + // - rotate 3 of the corners in unrotatedTextureRectangle by stRotation around the center of the bounding rectangle + // - apply the "east-north" system's normalization formula to the rotated cartographics, even though this is likely to produce values outside [0-1]. + // This gives us a set of points in the "east-north" texture coordinate system that can be used to map "east-north" texture coordinates to "desired." + + var points2D = points2DScratch; + points2D[0].x = unrotatedTextureRectangle.west; + points2D[0].y = unrotatedTextureRectangle.south; + + points2D[1].x = unrotatedTextureRectangle.west; + points2D[1].y = unrotatedTextureRectangle.north; + + points2D[2].x = unrotatedTextureRectangle.east; + points2D[2].y = unrotatedTextureRectangle.south; + + var boundingRectangle = rectangleGeometry.rectangle; + var toDesiredInComputed = Matrix2.fromRotation(rectangleGeometry._stRotation, rotation2DScratch); + var boundingRectangleCenter = Rectangle.center(boundingRectangle, rectangleCenterScratch); + + for (var i = 0; i < 3; ++i) { + var point2D = points2D[i]; + point2D.x -= boundingRectangleCenter.longitude; + point2D.y -= boundingRectangleCenter.latitude; + Matrix2.multiplyByVector(toDesiredInComputed, point2D, point2D); + point2D.x += boundingRectangleCenter.longitude; + point2D.y += boundingRectangleCenter.latitude; + + // Convert point into east-north texture coordinate space + point2D.x = (point2D.x - boundingRectangle.west) / boundingRectangle.width; + point2D.y = (point2D.y - boundingRectangle.south) / boundingRectangle.height; + } + + var minXYCorner = points2D[0]; + var maxYCorner = points2D[1]; + var maxXCorner = points2D[2]; + var result = new Array(6); + Cartesian2.pack(minXYCorner, result); + Cartesian2.pack(maxYCorner, result, 2); + Cartesian2.pack(maxXCorner, result, 4); + return result; + } + defineProperties(RectangleGeometry.prototype, { /** * @private @@ -872,6 +946,21 @@ define([ } return this._rotatedRectangle; } + }, + /** + * For remapping texture coordinates when rendering RectangleGeometries as GroundPrimitives. + * This version permits skew in textures by computing offsets directly in cartographic space and + * more accurately approximates rendering RectangleGeometries with height as standard Primitives. + * @see Geometry#_textureCoordinateRotationPoints + * @private + */ + textureCoordinateRotationPoints : { + get : function() { + if (!defined(this._textureCoordinateRotationPoints)) { + this._textureCoordinateRotationPoints = textureCoordinateRotationPoints(this); + } + return this._textureCoordinateRotationPoints; + } } }); diff --git a/Source/DataSources/CorridorGeometryUpdater.js b/Source/DataSources/CorridorGeometryUpdater.js index 338a33baeccd..1351b25b6927 100644 --- a/Source/DataSources/CorridorGeometryUpdater.js +++ b/Source/DataSources/CorridorGeometryUpdater.js @@ -165,10 +165,8 @@ define([ }; CorridorGeometryUpdater.prototype._isOnTerrain = function(entity, corridor) { - var isColorMaterial = this._materialProperty instanceof ColorMaterialProperty; - return this._fillEnabled && !defined(corridor.height) && !defined(corridor.extrudedHeight) && - isColorMaterial && GroundPrimitive.isSupported(this._scene); + GroundPrimitive.isSupported(this._scene); }; CorridorGeometryUpdater.prototype._getIsClosed = function(options) { diff --git a/Source/DataSources/DynamicGeometryUpdater.js b/Source/DataSources/DynamicGeometryUpdater.js index 672a5db01d4d..163eaaab442f 100644 --- a/Source/DataSources/DynamicGeometryUpdater.js +++ b/Source/DataSources/DynamicGeometryUpdater.js @@ -100,32 +100,33 @@ define([ var shadows = this._geometryUpdater.shadowsProperty.getValue(time); var options = this._options; if (!defined(geometry.fill) || geometry.fill.getValue(time)) { + var fillMaterialProperty = geometryUpdater.fillMaterialProperty; + var isColorAppearance = fillMaterialProperty instanceof ColorMaterialProperty; + var appearance; + var closed = geometryUpdater._getIsClosed(options); + if (isColorAppearance) { + appearance = new PerInstanceColorAppearance({ + closed: closed + }); + } else { + var material = MaterialProperty.getValue(time, fillMaterialProperty, this._material); + this._material = material; + appearance = new MaterialAppearance({ + material : material, + translucent : material.isTranslucent(), + closed : closed + }); + } + if (onTerrain) { options.vertexFormat = PerInstanceColorAppearance.VERTEX_FORMAT; this._primitive = groundPrimitives.add(new GroundPrimitive({ geometryInstances : this._geometryUpdater.createFillGeometryInstance(time), + appearance : appearance, asynchronous : false, shadows : shadows })); } else { - var fillMaterialProperty = geometryUpdater.fillMaterialProperty; - var isColorAppearance = fillMaterialProperty instanceof ColorMaterialProperty; - var appearance; - var closed = geometryUpdater._getIsClosed(options); - if (isColorAppearance) { - appearance = new PerInstanceColorAppearance({ - closed: closed - }); - } else { - var material = MaterialProperty.getValue(time, fillMaterialProperty, this._material); - this._material = material; - appearance = new MaterialAppearance({ - material : material, - translucent : material.isTranslucent(), - closed : closed - }); - } - options.vertexFormat = appearance.vertexFormat; var fillInstance = this._geometryUpdater.createFillGeometryInstance(time); diff --git a/Source/DataSources/EllipseGeometryUpdater.js b/Source/DataSources/EllipseGeometryUpdater.js index 45c1eec20ee9..1da875c75e6a 100644 --- a/Source/DataSources/EllipseGeometryUpdater.js +++ b/Source/DataSources/EllipseGeometryUpdater.js @@ -172,9 +172,7 @@ define([ }; EllipseGeometryUpdater.prototype._isOnTerrain = function(entity, ellipse) { - var isColorMaterial = this._materialProperty instanceof ColorMaterialProperty; - - return this._fillEnabled && !defined(ellipse.height) && !defined(ellipse.extrudedHeight) && isColorMaterial && GroundPrimitive.isSupported(this._scene); + return this._fillEnabled && !defined(ellipse.height) && !defined(ellipse.extrudedHeight) && GroundPrimitive.isSupported(this._scene); }; EllipseGeometryUpdater.prototype._isDynamic = function(entity, ellipse) { diff --git a/Source/DataSources/Entity.js b/Source/DataSources/Entity.js index 8588ecbec11e..b9d400c66935 100644 --- a/Source/DataSources/Entity.js +++ b/Source/DataSources/Entity.js @@ -11,6 +11,7 @@ define([ '../Core/Matrix4', '../Core/Quaternion', '../Core/Transforms', + '../Scene/GroundPrimitive', './BillboardGraphics', './BoxGraphics', './ConstantPositionProperty', @@ -45,6 +46,7 @@ define([ Matrix4, Quaternion, Transforms, + GroundPrimitive, BillboardGraphics, BoxGraphics, ConstantPositionProperty, @@ -617,5 +619,17 @@ define([ return result; }; + /** + * Checks if the given Scene supports materials besides Color on Entities draped on terrain. + * If this feature is not supported, Entities with non-color materials but no `height` will + * instead be rendered as if height is 0. + * + * @param {Scene} scene The current scene. + * @returns {Boolean} Whether or not the current scene supports materials for entities on terrain. + */ + Entity.supportsMaterialsforEntitiesOnTerrain = function(scene) { + return GroundPrimitive.supportsMaterials(scene); + }; + return Entity; }); diff --git a/Source/DataSources/GeometryUpdater.js b/Source/DataSources/GeometryUpdater.js index 30a639413de0..4c44cee19e79 100644 --- a/Source/DataSources/GeometryUpdater.js +++ b/Source/DataSources/GeometryUpdater.js @@ -14,6 +14,7 @@ define([ '../Scene/ShadowMode', './ColorMaterialProperty', './ConstantProperty', + './Entity', './Property' ], function( Check, @@ -31,6 +32,7 @@ define([ ShadowMode, ColorMaterialProperty, ConstantProperty, + Entity, Property) { 'use strict'; @@ -88,6 +90,7 @@ define([ this._geometryPropertyName = geometryPropertyName; this._id = geometryPropertyName + '-' + entity.id; this._observedPropertyNames = options.observedPropertyNames; + this._supportsMaterialsforEntitiesOnTerrain = Entity.supportsMaterialsforEntitiesOnTerrain(options.scene); this._onEntityPropertyChanged(entity, geometryPropertyName, entity[geometryPropertyName], undefined); } @@ -460,7 +463,9 @@ define([ this._fillEnabled = fillEnabled; - var onTerrain = this._isOnTerrain(entity, geometry); + var onTerrain = this._isOnTerrain(entity, geometry) && + (this._supportsMaterialsforEntitiesOnTerrain || this._materialProperty instanceof ColorMaterialProperty); + if (outlineEnabled && onTerrain) { oneTimeWarning(oneTimeWarning.geometryOutlines); outlineEnabled = false; diff --git a/Source/DataSources/GeometryVisualizer.js b/Source/DataSources/GeometryVisualizer.js index 0ef53d74fc83..c1da09f0e21d 100644 --- a/Source/DataSources/GeometryVisualizer.js +++ b/Source/DataSources/GeometryVisualizer.js @@ -19,6 +19,7 @@ define([ './DynamicGeometryBatch', './EllipseGeometryUpdater', './EllipsoidGeometryUpdater', + './Entity', './PlaneGeometryUpdater', './PolygonGeometryUpdater', './PolylineVolumeGeometryUpdater', @@ -26,6 +27,7 @@ define([ './StaticGeometryColorBatch', './StaticGeometryPerMaterialBatch', './StaticGroundGeometryColorBatch', + './StaticGroundGeometryPerMaterialBatch', './StaticOutlineGeometryBatch', './WallGeometryUpdater' ], function( @@ -49,6 +51,7 @@ define([ DynamicGeometryBatch, EllipseGeometryUpdater, EllipsoidGeometryUpdater, + Entity, PlaneGeometryUpdater, PolygonGeometryUpdater, PolylineVolumeGeometryUpdater, @@ -56,6 +59,7 @@ define([ StaticGeometryColorBatch, StaticGeometryPerMaterialBatch, StaticGroundGeometryColorBatch, + StaticGroundGeometryPerMaterialBatch, StaticOutlineGeometryBatch, WallGeometryUpdater) { 'use strict'; @@ -144,6 +148,9 @@ define([ this._openColorBatches = new Array(numberOfShadowModes); this._openMaterialBatches = new Array(numberOfShadowModes); + var supportsMaterialsforEntitiesOnTerrain = Entity.supportsMaterialsforEntitiesOnTerrain(scene); + this._supportsMaterialsforEntitiesOnTerrain = supportsMaterialsforEntitiesOnTerrain; + var i; for (i = 0; i < numberOfShadowModes; ++i) { this._outlineBatches[i] = new StaticOutlineGeometryBatch(primitives, scene, i); @@ -155,15 +162,30 @@ define([ } var numberOfClassificationTypes = ClassificationType.NUMBER_OF_CLASSIFICATION_TYPES; - this._groundColorBatches = new Array(numberOfClassificationTypes); - - for (i = 0; i < numberOfClassificationTypes; ++i) { - this._groundColorBatches[i] = new StaticGroundGeometryColorBatch(groundPrimitives, i); + var groundColorBatches = new Array(numberOfClassificationTypes); + var groundmaterialBatches = []; + if (supportsMaterialsforEntitiesOnTerrain) { + // Culling, phong shading only supported for ClassificationType.TERRAIN at the moment because + // tileset depth information not yet available. + groundColorBatches[ClassificationType.TERRAIN] = new StaticGroundGeometryPerMaterialBatch(groundPrimitives, PerInstanceColorAppearance); + for (i = 0; i < numberOfClassificationTypes; ++i) { + if (i !== ClassificationType.TERRAIN) { + groundColorBatches[i] = new StaticGroundGeometryColorBatch(groundPrimitives, i); + } + } + groundmaterialBatches[0] = new StaticGroundGeometryPerMaterialBatch(groundPrimitives, MaterialAppearance); + this._groundTerrainMaterialBatch = groundmaterialBatches[0]; + } else { + for (i = 0; i < numberOfClassificationTypes; ++i) { + groundColorBatches[i] = new StaticGroundGeometryColorBatch(groundPrimitives, i); + } } + this._groundColorBatches = groundColorBatches; + this._dynamicBatch = new DynamicGeometryBatch(primitives, groundPrimitives); - this._batches = this._outlineBatches.concat(this._closedColorBatches, this._closedMaterialBatches, this._openColorBatches, this._openMaterialBatches, this._groundColorBatches, this._dynamicBatch); + this._batches = this._outlineBatches.concat(this._closedColorBatches, this._closedMaterialBatches, this._openColorBatches, this._openMaterialBatches, this._groundColorBatches, groundmaterialBatches, this._dynamicBatch); this._subscriptions = new AssociativeArray(); this._updaterSets = new AssociativeArray(); @@ -375,7 +397,15 @@ define([ if (updater.fillEnabled) { if (updater.onTerrain) { var classificationType = updater.classificationTypeProperty.getValue(time); - this._groundColorBatches[classificationType].add(time, updater); + if (updater.fillMaterialProperty instanceof ColorMaterialProperty) { + this._groundColorBatches[classificationType].add(time, updater); + } else { + // If unsupported, updater will not be on terrain. + // If the updater has a material, ignore input ClassificationType for now and only classify terrain. + // Culling, phong shading only supported for ClassificationType.TERRAIN at the moment because + // tileset depth information not yet available. + this._groundTerrainMaterialBatch.add(time, updater); + } } else if (updater.isClosed) { if (updater.fillMaterialProperty instanceof ColorMaterialProperty) { this._closedColorBatches[shadows].add(time, updater); diff --git a/Source/DataSources/PolygonGeometryUpdater.js b/Source/DataSources/PolygonGeometryUpdater.js index 293d463812d3..cf537ebd95f8 100644 --- a/Source/DataSources/PolygonGeometryUpdater.js +++ b/Source/DataSources/PolygonGeometryUpdater.js @@ -173,10 +173,9 @@ define([ }; PolygonGeometryUpdater.prototype._isOnTerrain = function(entity, polygon) { - var isColorMaterial = this._materialProperty instanceof ColorMaterialProperty; var perPositionHeightProperty = polygon.perPositionHeight; var perPositionHeightEnabled = defined(perPositionHeightProperty) && (perPositionHeightProperty.isConstant ? perPositionHeightProperty.getValue(Iso8601.MINIMUM_VALUE) : true); - return this._fillEnabled && !defined(polygon.height) && !defined(polygon.extrudedHeight) && isColorMaterial && + return this._fillEnabled && !defined(polygon.height) && !defined(polygon.extrudedHeight) && !perPositionHeightEnabled && GroundPrimitive.isSupported(this._scene); }; diff --git a/Source/DataSources/RectangleGeometryUpdater.js b/Source/DataSources/RectangleGeometryUpdater.js index 5ebe539f5df3..256b17a1cb6f 100644 --- a/Source/DataSources/RectangleGeometryUpdater.js +++ b/Source/DataSources/RectangleGeometryUpdater.js @@ -167,9 +167,7 @@ define([ }; RectangleGeometryUpdater.prototype._isOnTerrain = function(entity, rectangle) { - var isColorMaterial = this._materialProperty instanceof ColorMaterialProperty; - - return this._fillEnabled && !defined(rectangle.height) && !defined(rectangle.extrudedHeight) && isColorMaterial && GroundPrimitive.isSupported(this._scene); + return this._fillEnabled && !defined(rectangle.height) && !defined(rectangle.extrudedHeight) && GroundPrimitive.isSupported(this._scene); }; RectangleGeometryUpdater.prototype._isDynamic = function(entity, rectangle) { diff --git a/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js b/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js new file mode 100644 index 000000000000..01eee22b114a --- /dev/null +++ b/Source/DataSources/StaticGroundGeometryPerMaterialBatch.js @@ -0,0 +1,374 @@ +define([ + '../Core/AssociativeArray', + '../Core/ColorGeometryInstanceAttribute', + '../Core/defined', + '../Core/DistanceDisplayCondition', + '../Core/DistanceDisplayConditionGeometryInstanceAttribute', + '../Core/ShowGeometryInstanceAttribute', + '../Core/RectangleCollisionChecker', + '../Scene/ClassificationType', + '../Scene/GroundPrimitive', + '../Scene/ShadowVolumeAppearance', + './BoundingSphereState', + './ColorMaterialProperty', + './MaterialProperty', + './Property' + ], function( + AssociativeArray, + ColorGeometryInstanceAttribute, + defined, + DistanceDisplayCondition, + DistanceDisplayConditionGeometryInstanceAttribute, + ShowGeometryInstanceAttribute, + RectangleCollisionChecker, + ClassificationType, + GroundPrimitive, + ShadowVolumeAppearance, + BoundingSphereState, + ColorMaterialProperty, + MaterialProperty, + Property) { + 'use strict'; + + var distanceDisplayConditionScratch = new DistanceDisplayCondition(); + var defaultDistanceDisplayCondition = new DistanceDisplayCondition(); + + // Encapsulates a Primitive and all the entities that it represents. + function Batch(primitives, appearanceType, materialProperty, usingSphericalTextureCoordinates) { + this.primitives = primitives; // scene level primitive collection + this.appearanceType = appearanceType; + this.materialProperty = materialProperty; + this.updaters = new AssociativeArray(); + this.createPrimitive = true; + this.primitive = undefined; // a GroundPrimitive encapsulating all the entities + this.oldPrimitive = undefined; + this.geometry = new AssociativeArray(); + this.material = undefined; + this.updatersWithAttributes = new AssociativeArray(); + this.attributes = new AssociativeArray(); + this.invalidated = false; + this.removeMaterialSubscription = materialProperty.definitionChanged.addEventListener(Batch.prototype.onMaterialChanged, this); + this.subscriptions = new AssociativeArray(); + this.showsUpdated = new AssociativeArray(); + this.usingSphericalTextureCoordinates = usingSphericalTextureCoordinates; + this.rectangleCollisionCheck = new RectangleCollisionChecker(); + } + + Batch.prototype.onMaterialChanged = function() { + this.invalidated = true; + }; + + Batch.prototype.overlapping = function(rectangle) { + return this.rectangleCollisionCheck.collides(rectangle); + }; + + // Check if the given updater's material is compatible with this batch + Batch.prototype.isMaterial = function(updater) { + var material = this.materialProperty; + var updaterMaterial = updater.fillMaterialProperty; + + if (updaterMaterial === material || + (updaterMaterial instanceof ColorMaterialProperty && material instanceof ColorMaterialProperty)) { + return true; + } + return defined(material) && material.equals(updaterMaterial); + }; + + Batch.prototype.add = function(time, updater, geometryInstance) { + var id = updater.id; + this.updaters.set(id, updater); + this.geometry.set(id, geometryInstance); + this.rectangleCollisionCheck.insert(id, geometryInstance.geometry.rectangle); + // Updaters with dynamic attributes must be tracked separately, may exit the batch + if (!updater.hasConstantFill || !updater.fillMaterialProperty.isConstant || !Property.isConstant(updater.distanceDisplayConditionProperty)) { + this.updatersWithAttributes.set(id, updater); + } else { + var that = this; + // Listen for show changes. These will be synchronized in updateShows. + this.subscriptions.set(id, updater.entity.definitionChanged.addEventListener(function(entity, propertyName, newValue, oldValue) { + if (propertyName === 'isShowing') { + that.showsUpdated.set(updater.id, updater); + } + })); + } + this.createPrimitive = true; + }; + + Batch.prototype.remove = function(updater) { + var id = updater.id; + var geometryInstance = this.geometry.get(id); + this.createPrimitive = this.geometry.remove(id) || this.createPrimitive; + if (this.updaters.remove(id)) { + this.rectangleCollisionCheck.remove(id, geometryInstance.geometry.rectangle); + this.updatersWithAttributes.remove(id); + var unsubscribe = this.subscriptions.get(id); + if (defined(unsubscribe)) { + unsubscribe(); + this.subscriptions.remove(id); + } + } + return this.createPrimitive; + }; + + Batch.prototype.update = function(time) { + var isUpdated = true; + var primitive = this.primitive; + var primitives = this.primitives; + var geometries = this.geometry.values; + var attributes; + var i; + + if (this.createPrimitive) { + var geometriesLength = geometries.length; + if (geometriesLength > 0) { + if (defined(primitive)) { + // Keep a handle to the old primitive so it can be removed when the updated version is ready. + if (!defined(this.oldPrimitive)) { + this.oldPrimitive = primitive; + } else { + // For if the new primitive changes again before it is ready. + primitives.remove(primitive); + } + } + + for (i = 0; i < geometriesLength; i++) { + var geometry = geometries[i]; + var originalAttributes = geometry.attributes; + attributes = this.attributes.get(geometry.id.id); + + if (defined(attributes)) { + if (defined(originalAttributes.show)) { + originalAttributes.show.value = attributes.show; + } + if (defined(originalAttributes.color)) { + originalAttributes.color.value = attributes.color; + } + } + } + + this.material = MaterialProperty.getValue(time, this.materialProperty, this.material); + + primitive = new GroundPrimitive({ + show : false, + asynchronous : true, + geometryInstances : geometries, + appearance : new this.appearanceType({ + material : this.material + // translucent and closed properties overridden + }), + classificationType : ClassificationType.TERRAIN + }); + + primitives.add(primitive); + isUpdated = false; + } else { + if (defined(primitive)) { + primitives.remove(primitive); + primitive = undefined; + } + var oldPrimitive = this.oldPrimitive; + if (defined(oldPrimitive)) { + primitives.remove(oldPrimitive); + this.oldPrimitive = undefined; + } + } + + this.attributes.removeAll(); + this.primitive = primitive; + this.createPrimitive = false; + } else if (defined(primitive) && primitive.ready) { + primitive.show = true; + if (defined(this.oldPrimitive)) { + primitives.remove(this.oldPrimitive); + this.oldPrimitive = undefined; + } + + this.material = MaterialProperty.getValue(time, this.materialProperty, this.material); + this.primitive.appearance.material = this.material; + + var updatersWithAttributes = this.updatersWithAttributes.values; + var length = updatersWithAttributes.length; + for (i = 0; i < length; i++) { + var updater = updatersWithAttributes[i]; + var entity = updater.entity; + var instance = this.geometry.get(updater.id); + + attributes = this.attributes.get(instance.id.id); + if (!defined(attributes)) { + attributes = primitive.getGeometryInstanceAttributes(instance.id); + this.attributes.set(instance.id.id, attributes); + } + + var show = entity.isShowing && (updater.hasConstantFill || updater.isFilled(time)); + var currentShow = attributes.show[0] === 1; + if (show !== currentShow) { + attributes.show = ShowGeometryInstanceAttribute.toValue(show, attributes.show); + } + + var distanceDisplayConditionProperty = updater.distanceDisplayConditionProperty; + if (!Property.isConstant(distanceDisplayConditionProperty)) { + var distanceDisplayCondition = Property.getValueOrDefault(distanceDisplayConditionProperty, time, defaultDistanceDisplayCondition, distanceDisplayConditionScratch); + if (!DistanceDisplayCondition.equals(distanceDisplayCondition, attributes._lastDistanceDisplayCondition)) { + attributes._lastDistanceDisplayCondition = DistanceDisplayCondition.clone(distanceDisplayCondition, attributes._lastDistanceDisplayCondition); + attributes.distanceDisplayCondition = DistanceDisplayConditionGeometryInstanceAttribute.toValue(distanceDisplayCondition, attributes.distanceDisplayCondition); + } + } + } + + this.updateShows(primitive); + } else if (defined(primitive) && !primitive.ready) { + isUpdated = false; + } + return isUpdated; + }; + + Batch.prototype.updateShows = function(primitive) { + var showsUpdated = this.showsUpdated.values; + var length = showsUpdated.length; + for (var i = 0; i < length; i++) { + var updater = showsUpdated[i]; + var entity = updater.entity; + var instance = this.geometry.get(updater.id); + + var attributes = this.attributes.get(instance.id.id); + if (!defined(attributes)) { + attributes = primitive.getGeometryInstanceAttributes(instance.id); + this.attributes.set(instance.id.id, attributes); + } + + var show = entity.isShowing; + var currentShow = attributes.show[0] === 1; + if (show !== currentShow) { + attributes.show = ShowGeometryInstanceAttribute.toValue(show, attributes.show); + } + } + this.showsUpdated.removeAll(); + }; + + Batch.prototype.contains = function(updater) { + return this.updaters.contains(updater.id); + }; + + Batch.prototype.getBoundingSphere = function(updater, result) { + var primitive = this.primitive; + if (!primitive.ready) { + return BoundingSphereState.PENDING; + } + var attributes = primitive.getGeometryInstanceAttributes(updater.entity); + if (!defined(attributes) || !defined(attributes.boundingSphere) || + (defined(attributes.show) && attributes.show[0] === 0)) { + return BoundingSphereState.FAILED; + } + attributes.boundingSphere.clone(result); + return BoundingSphereState.DONE; + }; + + Batch.prototype.destroy = function() { + var primitive = this.primitive; + var primitives = this.primitives; + if (defined(primitive)) { + primitives.remove(primitive); + } + var oldPrimitive = this.oldPrimitive; + if (defined(oldPrimitive)) { + primitives.remove(oldPrimitive); + } + this.removeMaterialSubscription(); + }; + + /** + * @private + */ + function StaticGroundGeometryPerMaterialBatch(primitives, appearanceType) { + this._items = []; + this._primitives = primitives; + this._appearanceType = appearanceType; + } + + StaticGroundGeometryPerMaterialBatch.prototype.add = function(time, updater) { + var items = this._items; + var length = items.length; + var geometryInstance = updater.createFillGeometryInstance(time); + var usingSphericalTextureCoordinates = ShadowVolumeAppearance.shouldUseSphericalCoordinates(geometryInstance.geometry.rectangle); + // Check if the Entity represented by the updater can be placed in an existing batch. Requirements: + // * compatible material (same material or same color) + // * same type of texture coordinates (spherical vs. planar) + // * conservatively non-overlapping with any entities in the existing batch + for (var i = 0; i < length; ++i) { + var item = items[i]; + if (item.isMaterial(updater) && + !item.overlapping(geometryInstance.geometry.rectangle) && + item.usingSphericalTextureCoordinates === usingSphericalTextureCoordinates) { + item.add(time, updater, geometryInstance); + return; + } + } + // If a compatible batch wasn't found, create a new batch. + var batch = new Batch(this._primitives, this._appearanceType, updater.fillMaterialProperty, usingSphericalTextureCoordinates); + batch.add(time, updater, geometryInstance); + items.push(batch); + }; + + StaticGroundGeometryPerMaterialBatch.prototype.remove = function(updater) { + var items = this._items; + var length = items.length; + for (var i = length - 1; i >= 0; i--) { + var item = items[i]; + if (item.remove(updater)) { + if (item.updaters.length === 0) { + items.splice(i, 1); + item.destroy(); + } + break; + } + } + }; + + StaticGroundGeometryPerMaterialBatch.prototype.update = function(time) { + var i; + var items = this._items; + var length = items.length; + + for (i = length - 1; i >= 0; i--) { + var item = items[i]; + if (item.invalidated) { + items.splice(i, 1); + var updaters = item.updaters.values; + var updatersLength = updaters.length; + for (var h = 0; h < updatersLength; h++) { + this.add(time, updaters[h]); + } + item.destroy(); + } + } + + var isUpdated = true; + for (i = 0; i < length; i++) { + isUpdated = items[i].update(time) && isUpdated; + } + return isUpdated; + }; + + StaticGroundGeometryPerMaterialBatch.prototype.getBoundingSphere = function(updater, result) { + var items = this._items; + var length = items.length; + for (var i = 0; i < length; i++) { + var item = items[i]; + if (item.contains(updater)){ + return item.getBoundingSphere(updater, result); + } + } + return BoundingSphereState.FAILED; + }; + + StaticGroundGeometryPerMaterialBatch.prototype.removeAllPrimitives = function() { + var items = this._items; + var length = items.length; + for (var i = 0; i < length; i++) { + items[i].destroy(); + } + this._items.length = 0; + }; + + return StaticGroundGeometryPerMaterialBatch; +}); diff --git a/Source/Renderer/AutomaticUniforms.js b/Source/Renderer/AutomaticUniforms.js index 1556c5b2ad9c..55fd1836622a 100644 --- a/Source/Renderer/AutomaticUniforms.js +++ b/Source/Renderer/AutomaticUniforms.js @@ -1,9 +1,11 @@ define([ '../Core/Cartesian3', + '../Core/Math', '../Core/Matrix4', '../Core/WebGLConstants' ], function( Cartesian3, + CesiumMath, Matrix4, WebGLConstants) { 'use strict'; @@ -1136,18 +1138,48 @@ define([ }), /** - * The log of the current frustums far plane. Used for computing the log depth. + * The log2 of the current frustums far plane. Used for computing the log depth. * - * @alias czm_logFarDistance + * @alias czm_log2FarDistance * @glslUniform * * @private */ - czm_logFarDistance : new AutomaticUniform({ + czm_log2FarDistance : new AutomaticUniform({ size : 1, datatype : WebGLConstants.FLOAT, getValue : function(uniformState) { - return uniformState.logFarDistance; + return uniformState.log2FarDistance; + } + }), + + /** + * An automatic GLSL uniform containing log2 of the far distance + 1.0. + * This is used when reversing log depth computations. + * + * @alias czm_log2FarPlusOne + * @glslUniform + */ + czm_log2FarPlusOne : new AutomaticUniform({ + size : 1, + datatype : WebGLConstants.FLOAT, + getValue : function(uniformState) { + return uniformState.log2FarPlusOne; + } + }), + + /** + * An automatic GLSL uniform containing log2 of the near distance. + * This is used when writing log depth in the fragment shader. + * + * @alias czm_log2NearDistance + * @glslUniform + */ + czm_log2NearDistance : new AutomaticUniform({ + size : 1, + datatype : WebGLConstants.FLOAT, + getValue : function(uniformState) { + return uniformState.log2NearDistance; } }), diff --git a/Source/Renderer/UniformState.js b/Source/Renderer/UniformState.js index 1c6d1a405a27..0097e65d3dde 100644 --- a/Source/Renderer/UniformState.js +++ b/Source/Renderer/UniformState.js @@ -61,7 +61,9 @@ define([ this._entireFrustum = new Cartesian2(); this._currentFrustum = new Cartesian2(); this._frustumPlanes = new Cartesian4(); - this._logFarDistance = undefined; + this._log2FarDistance = undefined; + this._log2FarPlusOne = undefined; + this._log2NearDistance = undefined; this._frameState = undefined; this._temeToPseudoFixed = Matrix3.clone(Matrix4.IDENTITY); @@ -643,13 +645,35 @@ define([ }, /** - * The log of the current frustum's far distance. Used to compute the log depth. + * The log2 of the current frustum's far distance. Used to compute the log depth. * @memberof UniformState.prototype * @type {Number} */ - logFarDistance : { + log2FarDistance : { get : function() { - return this._logFarDistance; + return this._log2FarDistance; + } + }, + + /** + * The log2 of 1 + the current frustum's far distance. Used to reverse log depth. + * @memberof UniformState.prototype + * @type {Number} + */ + log2FarPlusOne : { + get : function() { + return this._log2FarPlusOne; + } + }, + + /** + * The log2 current frustum's near distance. Used when writing log depth in the fragment shader. + * @memberof UniformState.prototype + * @type {Number} + */ + log2NearDistance : { + get : function() { + return this._log2NearDistance; } }, @@ -999,7 +1023,9 @@ define([ this._currentFrustum.x = frustum.near; this._currentFrustum.y = frustum.far; - this._logFarDistance = 2.0 / CesiumMath.log2(frustum.far + 1.0); + this._log2FarDistance = 2.0 / CesiumMath.log2(frustum.far + 1.0); + this._log2FarPlusOne = CesiumMath.log2(frustum.far + 1.0); + this._log2NearDistance = CesiumMath.log2(frustum.near); if (defined(frustum._offCenterFrustum)) { frustum = frustum._offCenterFrustum; diff --git a/Source/Scene/ClassificationPrimitive.js b/Source/Scene/ClassificationPrimitive.js index 743b3182ea9d..675e2f9d5ef5 100644 --- a/Source/Scene/ClassificationPrimitive.js +++ b/Source/Scene/ClassificationPrimitive.js @@ -1,5 +1,6 @@ define([ '../Core/ColorGeometryInstanceAttribute', + '../Core/combine', '../Core/defaultValue', '../Core/defined', '../Core/defineProperties', @@ -13,7 +14,7 @@ define([ '../Renderer/ShaderProgram', '../Renderer/ShaderSource', '../Shaders/ShadowVolumeFS', - '../Shaders/ShadowVolumeVS', + '../Shaders/ShadowVolumeAppearanceVS', '../ThirdParty/when', './BlendingState', './ClassificationType', @@ -21,10 +22,12 @@ define([ './PerInstanceColorAppearance', './Primitive', './SceneMode', + './ShadowVolumeAppearance', './StencilFunction', './StencilOperation' ], function( ColorGeometryInstanceAttribute, + combine, defaultValue, defined, defineProperties, @@ -38,7 +41,7 @@ define([ ShaderProgram, ShaderSource, ShadowVolumeFS, - ShadowVolumeVS, + ShadowVolumeAppearanceVS, when, BlendingState, ClassificationType, @@ -46,6 +49,7 @@ define([ PerInstanceColorAppearance, Primitive, SceneMode, + ShadowVolumeAppearance, StencilFunction, StencilOperation) { 'use strict'; @@ -53,14 +57,16 @@ define([ var ClassificationPrimitiveReadOnlyInstanceAttributes = ['color']; /** - * A classification primitive represents a volume enclosing geometry in the {@link Scene} to be highlighted. The geometry must be from a single {@link GeometryInstance}. - * Batching multiple geometries is not yet supported. + * A classification primitive represents a volume enclosing geometry in the {@link Scene} to be highlighted. *

- * A primitive combines the geometry instance with an {@link Appearance} that describes the full shading, including + * A primitive combines geometry instances with an {@link Appearance} that describes the full shading, including * {@link Material} and {@link RenderState}. Roughly, the geometry instance defines the structure and placement, * and the appearance defines the visual characteristics. Decoupling geometry and appearance allows us to mix - * and match most of them and add a new geometry or appearance independently of each other. Only the {@link PerInstanceColorAppearance} - * is supported at this time. + * and match most of them and add a new geometry or appearance independently of each other. + * Only {@link PerInstanceColorAppearance} with the same color across all instances is supported at this time when using + * ClassificationPrimitive directly. + * For full {@link Appearance} support when classifying terrain use {@link GroundPrimitive} instead. + * *

*

* For correct rendering, this feature requires the EXT_frag_depth WebGL extension. For hardware that do not support this extension, there @@ -79,6 +85,7 @@ define([ * * @param {Object} [options] Object with the following properties: * @param {Array|GeometryInstance} [options.geometryInstances] The geometry instances to render. This can either be a single instance or an array of length one. + * @param {Appearance} [options.appearance] The appearance used to render the primitive. Defaults to PerInstanceColorAppearance when GeometryInstances have a color attribute. * @param {Boolean} [options.show=true] Determines if this primitive will be shown. * @param {Boolean} [options.vertexCacheOptimize=false] When true, geometry vertices are optimized for the pre and post-vertex-shader caches. * @param {Boolean} [options.interleave=false] When true, geometry vertex attributes are interleaved, which can slightly improve rendering performance but increases load time. @@ -98,6 +105,7 @@ define([ */ function ClassificationPrimitive(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); + var geometryInstances = options.geometryInstances; /** * The geometry instance rendered with this primitive. This may @@ -117,7 +125,7 @@ define([ * * @default undefined */ - this.geometryInstances = options.geometryInstances; + this.geometryInstances = geometryInstances; /** * Determines if the primitive will be shown. This affects all geometry * instances in the primitive. @@ -166,6 +174,10 @@ define([ this._sp = undefined; this._spStencil = undefined; this._spPick = undefined; + this._spColor = undefined; + + this._spPick2D = undefined; // only derived if necessary + this._spColor2D = undefined; // only derived if necessary this._rsStencilPreloadPass = undefined; this._rsStencilDepthPass = undefined; @@ -180,21 +192,26 @@ define([ this._primitive = undefined; this._pickPrimitive = options._pickPrimitive; - var appearance = new PerInstanceColorAppearance({ - flat : true - }); + // Set in update + this._hasSphericalExtentsAttribute = false; + this._hasPlanarExtentsAttributes = false; + this._hasPerColorAttribute = false; + + this.appearance = options.appearance; var readOnlyAttributes; - if (defined(this.geometryInstances) && isArray(this.geometryInstances) && this.geometryInstances.length > 1) { + if (defined(geometryInstances) && isArray(geometryInstances) && geometryInstances.length > 1) { readOnlyAttributes = ClassificationPrimitiveReadOnlyInstanceAttributes; } this._createBoundingVolumeFunction = options._createBoundingVolumeFunction; this._updateAndQueueCommandsFunction = options._updateAndQueueCommandsFunction; + this._usePickOffsets = false; + this._primitiveOptions = { geometryInstances : undefined, - appearance : appearance, + appearance : undefined, vertexCacheOptimize : defaultValue(options.vertexCacheOptimize, false), interleave : defaultValue(options.interleave, false), releaseGeometryInstances : defaultValue(options.releaseGeometryInstances, true), @@ -334,6 +351,21 @@ define([ get : function() { return this._readyPromise.promise; } + }, + + /** + * Returns true if the ClassificationPrimitive needs a separate shader and commands for 2D. + * This is because texture coordinates on ClassificationPrimitives are computed differently, + * and are used for culling when multiple GeometryInstances are batched in one ClassificationPrimitive. + * @memberof ClassificationPrimitive.prototype + * @type {Boolean} + * @readonly + * @private + */ + _needs2DShader : { + get : function() { + return this._hasPlanarExtentsAttributes || this._hasSphericalExtentsAttribute; + } } }); @@ -509,19 +541,18 @@ define([ } } - function createShaderProgram(classificationPrimitive, frameState, appearance) { - if (defined(classificationPrimitive._sp)) { - return; - } - + function createShaderProgram(classificationPrimitive, frameState) { var context = frameState.context; var primitive = classificationPrimitive._primitive; - var vs = ShadowVolumeVS; + var vs = ShadowVolumeAppearanceVS; vs = classificationPrimitive._primitive._batchTable.getVertexShaderCallback()(vs); vs = Primitive._appendDistanceDisplayConditionToShader(primitive, vs); vs = Primitive._modifyShaderPosition(classificationPrimitive, vs, frameState.scene3DOnly); vs = Primitive._updateColorAttribute(primitive, vs); + var planarExtents = classificationPrimitive._hasPlanarExtentsAttributes; + var cullFragmentsUsingExtents = planarExtents || classificationPrimitive._hasSphericalExtentsAttribute; + if (classificationPrimitive._extruded) { vs = modifyForEncodedNormals(primitive, vs); } @@ -542,6 +573,8 @@ define([ }); var attributeLocations = classificationPrimitive._primitive._attributeLocations; + var shadowVolumeAppearance = new ShadowVolumeAppearance(cullFragmentsUsingExtents, planarExtents, classificationPrimitive.appearance); + classificationPrimitive._spStencil = ShaderProgram.replaceCache({ context : context, shaderProgram : classificationPrimitive._spStencil, @@ -555,23 +588,33 @@ define([ vsPick = Primitive._appendShowToShader(primitive, vsPick); vsPick = Primitive._updatePickColorAttribute(vsPick); - var pickVS = new ShaderSource({ - defines : [extrudedDefine, disableGlPositionLogDepth], - sources : [vsPick] - }); - - var pickFS = new ShaderSource({ - sources : [ShadowVolumeFS], - pickColorQualifier : 'varying' - }); + var pickFS3D = shadowVolumeAppearance.createPickFragmentShader(false); + var pickVS3D = shadowVolumeAppearance.createPickVertexShader([extrudedDefine, disableGlPositionLogDepth], vsPick, false); classificationPrimitive._spPick = ShaderProgram.replaceCache({ context : context, shaderProgram : classificationPrimitive._spPick, - vertexShaderSource : pickVS, - fragmentShaderSource : pickFS, + vertexShaderSource : pickVS3D, + fragmentShaderSource : pickFS3D, attributeLocations : attributeLocations }); + + // Derive a 2D pick shader if the primitive uses texture coordinate-based fragment culling, + // since texture coordinates are computed differently in 2D. + if (cullFragmentsUsingExtents) { + var pickProgram2D = context.shaderCache.getDerivedShaderProgram(classificationPrimitive._spPick, '2dPick'); + if (!defined(pickProgram2D)) { + var pickFS2D = shadowVolumeAppearance.createPickFragmentShader(true); + var pickVS2D = shadowVolumeAppearance.createPickVertexShader([extrudedDefine, disableGlPositionLogDepth], vsPick, true); + + pickProgram2D = context.shaderCache.createDerivedShaderProgram(classificationPrimitive._spPick, '2dPick', { + vertexShaderSource : pickVS2D, + fragmentShaderSource : pickFS2D, + attributeLocations : attributeLocations + }); + } + classificationPrimitive._spPick2D = pickProgram2D; + } } else { classificationPrimitive._spPick = ShaderProgram.fromCache({ context : context, @@ -594,11 +637,41 @@ define([ fragmentShaderSource : fsSource, attributeLocations : attributeLocations }); + + // Create a fragment shader that computes only required material hookups using screen space techniques + var fsColorSource = shadowVolumeAppearance.createFragmentShader(false); + var vsColorSource = shadowVolumeAppearance.createVertexShader([extrudedDefine, disableGlPositionLogDepth], vs, false); + + classificationPrimitive._spColor = ShaderProgram.replaceCache({ + context : context, + shaderProgram : classificationPrimitive._spColor, + vertexShaderSource : vsColorSource, + fragmentShaderSource : fsColorSource, + attributeLocations : attributeLocations + }); + + // Derive a 2D shader if the primitive uses texture coordinate-based fragment culling, + // since texture coordinates are computed differently in 2D. + // Any material that uses texture coordinates will also equip texture coordinate-based fragment culling. + if (cullFragmentsUsingExtents) { + var colorProgram2D = context.shaderCache.getDerivedShaderProgram(classificationPrimitive._spColor, '2dColor'); + if (!defined(colorProgram2D)) { + var fsColorSource2D = shadowVolumeAppearance.createFragmentShader(true); + var vsColorSource2D = shadowVolumeAppearance.createVertexShader([extrudedDefine, disableGlPositionLogDepth], vs, true); + + colorProgram2D = context.shaderCache.createDerivedShaderProgram(classificationPrimitive._spColor, '2dColor', { + vertexShaderSource : vsColorSource2D, + fragmentShaderSource : fsColorSource2D, + attributeLocations : attributeLocations + }); + } + classificationPrimitive._spColor2D = colorProgram2D; + } } function createColorCommands(classificationPrimitive, colorCommands) { var primitive = classificationPrimitive._primitive; - var length = primitive._va.length * 3; + var length = primitive._va.length * 3; // each geometry (pack of vertex attributes) needs 3 commands: front/back stencils and fill colorCommands.length = length; var i; @@ -606,6 +679,8 @@ define([ var vaIndex = 0; var uniformMap = primitive._batchTable.getUniformMapCallback()(classificationPrimitive._uniformMap); + var needs2DShader = classificationPrimitive._needs2DShader; + for (i = 0; i < length; i += 3) { var vertexArray = primitive._va[vaIndex++]; @@ -648,8 +723,28 @@ define([ command.vertexArray = vertexArray; command.renderState = classificationPrimitive._rsColorPass; - command.shaderProgram = classificationPrimitive._sp; + command.shaderProgram = classificationPrimitive._spColor; + + var appearance = classificationPrimitive.appearance; + var material = appearance.material; + if (defined(material)) { + uniformMap = combine(uniformMap, material._uniforms); + } + command.uniformMap = uniformMap; + + // derive for 2D if texture coordinates are ever computed + if (needs2DShader) { + var derivedColorCommand = command.derivedCommands.appearance2D; + if (!defined(derivedColorCommand)) { + derivedColorCommand = DrawCommand.shallowClone(command); + command.derivedCommands.appearance2D = derivedColorCommand; + } + derivedColorCommand.vertexArray = vertexArray; + derivedColorCommand.renderState = classificationPrimitive._rsColorPass; + derivedColorCommand.shaderProgram = classificationPrimitive._spColor2D; + derivedColorCommand.uniformMap = uniformMap; + } } var commandsIgnoreShow = classificationPrimitive._commandsIgnoreShow; @@ -672,22 +767,35 @@ define([ } function createPickCommands(classificationPrimitive, pickCommands) { + var usePickOffsets = classificationPrimitive._usePickOffsets; + var primitive = classificationPrimitive._primitive; - var pickOffsets = primitive._pickOffsets; - var length = pickOffsets.length * 3; + var length = primitive._va.length * 3; // each geometry (pack of vertex attributes) needs 3 commands: front/back stencils and fill + + // Fallback for batching same-color geometry instances + var pickOffsets; + var pickIndex = 0; + var pickOffset; + if (usePickOffsets) { + pickOffsets = primitive._pickOffsets; + length = pickOffsets.length * 3; + } + pickCommands.length = length; var j; var command; - var pickIndex = 0; + var vaIndex = 0; var uniformMap = primitive._batchTable.getUniformMapCallback()(classificationPrimitive._uniformMap); - for (j = 0; j < length; j += 3) { - var pickOffset = pickOffsets[pickIndex++]; + var needs2DShader = classificationPrimitive._needs2DShader; - var offset = pickOffset.offset; - var count = pickOffset.count; - var vertexArray = primitive._va[pickOffset.index]; + for (j = 0; j < length; j += 3) { + var vertexArray = primitive._va[vaIndex++]; + if (usePickOffsets) { + pickOffset = pickOffsets[pickIndex++]; + vertexArray = primitive._va[pickOffset.index]; + } // stencil preload command command = pickCommands[j]; @@ -699,11 +807,13 @@ define([ } command.vertexArray = vertexArray; - command.offset = offset; - command.count = count; command.renderState = classificationPrimitive._rsStencilPreloadPass; - command.shaderProgram = classificationPrimitive._spStencil; + command.shaderProgram = classificationPrimitive._sp; command.uniformMap = uniformMap; + if (usePickOffsets) { + command.offset = pickOffset.offset; + command.count = pickOffset.count; + } // stencil depth command command = pickCommands[j + 1]; @@ -715,13 +825,15 @@ define([ } command.vertexArray = vertexArray; - command.offset = offset; - command.count = count; command.renderState = classificationPrimitive._rsStencilDepthPass; - command.shaderProgram = classificationPrimitive._spStencil; + command.shaderProgram = classificationPrimitive._sp; command.uniformMap = uniformMap; + if (usePickOffsets) { + command.offset = pickOffset.offset; + command.count = pickOffset.count; + } - // color command + // pick color command command = pickCommands[j + 2]; if (!defined(command)) { command = pickCommands[j + 2] = new DrawCommand({ @@ -731,11 +843,26 @@ define([ } command.vertexArray = vertexArray; - command.offset = offset; - command.count = count; command.renderState = classificationPrimitive._rsPickPass; command.shaderProgram = classificationPrimitive._spPick; command.uniformMap = uniformMap; + if (usePickOffsets) { + command.offset = pickOffset.offset; + command.count = pickOffset.count; + } + + // derive for 2D if texture coordinates are ever computed + if (needs2DShader) { + var derivedPickCommand = command.derivedCommands.pick2D; + if (!defined(derivedPickCommand)) { + derivedPickCommand = DrawCommand.shallowClone(command); + command.derivedCommands.pick2D = derivedPickCommand; + } + derivedPickCommand.vertexArray = vertexArray; + derivedPickCommand.renderState = classificationPrimitive._rsPickPass; + derivedPickCommand.shaderProgram = classificationPrimitive._spPick2D; + derivedPickCommand.uniformMap = uniformMap; + } } } @@ -843,6 +970,11 @@ define([ return; } + var appearance = this.appearance; + if (defined(appearance) && defined(appearance.material)) { + appearance.material.update(frameState.context); + } + var that = this; var primitiveOptions = this._primitiveOptions; @@ -852,21 +984,66 @@ define([ var i; var instance; - //>>includeStart('debug', pragmas.debug); - var color; - for (i = 0; i < length; ++i) { + var attributes; + + var hasPerColorAttribute = false; + var allColorsSame = true; + var firstColor; + var hasSphericalExtentsAttribute = false; + var hasPlanarExtentsAttributes = false; + + if (length > 0) { + attributes = instances[0].attributes; + // Not expecting these to be set by users, should only be set via GroundPrimitive. + // So don't check for mismatch. + hasSphericalExtentsAttribute = ShadowVolumeAppearance.hasAttributesForSphericalExtents(attributes); + hasPlanarExtentsAttributes = ShadowVolumeAppearance.hasAttributesForTextureCoordinatePlanes(attributes); + firstColor = attributes.color; + } + + for (i = 0; i < length; i++) { instance = instances[i]; - var attributes = instance.attributes; - if (!defined(attributes) || !defined(attributes.color)) { - throw new DeveloperError('Not all of the geometry instances have the same color attribute.'); - } else if (defined(color) && !ColorGeometryInstanceAttribute.equals(color, attributes.color)) { - throw new DeveloperError('Not all of the geometry instances have the same color attribute.'); - } else if (!defined(color)) { - color = attributes.color; + var color = instance.attributes.color; + if (defined(color)) { + hasPerColorAttribute = true; } + //>>includeStart('debug', pragmas.debug); + else if (hasPerColorAttribute) { + throw new DeveloperError('All GeometryInstances must have color attributes to use per-instance color.'); + } + //>>includeEnd('debug'); + + allColorsSame = allColorsSame && defined(color) && ColorGeometryInstanceAttribute.equals(firstColor, color); + } + + // If no attributes exist for computing spherical extents or fragment culling, + // throw if the colors aren't all the same. + if (!allColorsSame && !hasSphericalExtentsAttribute && !hasPlanarExtentsAttributes) { + throw new DeveloperError('All GeometryInstances must have the same color attribute except via GroundPrimitives'); + } + + // default to a color appearance + if (hasPerColorAttribute && !defined(appearance)) { + appearance = new PerInstanceColorAppearance({ + flat : true + }); + this.appearance = appearance; + } + + //>>includeStart('debug', pragmas.debug); + if (!hasPerColorAttribute && appearance instanceof PerInstanceColorAppearance) { + throw new DeveloperError('PerInstanceColorAppearance requires color GeometryInstanceAttributes on all GeometryInstances'); + } + if (defined(appearance.material) && !hasSphericalExtentsAttribute && !hasPlanarExtentsAttributes) { + throw new DeveloperError('Materials on ClassificationPrimitives are not supported except via GroundPrimitives'); } //>>includeEnd('debug'); + this._usePickOffsets = !hasSphericalExtentsAttribute && !hasPlanarExtentsAttributes; + this._hasSphericalExtentsAttribute = hasSphericalExtentsAttribute; + this._hasPlanarExtentsAttributes = hasPlanarExtentsAttributes; + this._hasPerColorAttribute = hasPerColorAttribute; + var geometryInstances = new Array(length); for (i = 0; i < length; ++i) { instance = instances[i]; @@ -879,6 +1056,7 @@ define([ }); } + primitiveOptions.appearance = appearance; primitiveOptions.geometryInstances = geometryInstances; if (defined(this._createBoundingVolumeFunction)) { @@ -935,6 +1113,19 @@ define([ this._rsStencilDepthPass = RenderState.fromCache(getStencilDepthRenderState(true)); this._rsColorPass = RenderState.fromCache(getColorRenderState(true)); } + // Update primitive appearance + if (this._primitive.appearance !== appearance) { + //>>includeStart('debug', pragmas.debug); + // Check if the appearance is supported by the geometry attributes + if (!this._hasSphericalExtentsAttribute && !this._hasPlanarExtentsAttributes && defined(appearance.material)) { + throw new DeveloperError('Materials on ClassificationPrimitives are not supported except via GroundPrimitive'); + } + if (!this._hasPerColorAttribute && appearance instanceof PerInstanceColorAppearance) { + throw new DeveloperError('PerInstanceColorAppearance requires color GeometryInstanceAttribute'); + } + //>>includeEnd('debug'); + this._primitive.appearance = appearance; + } this._primitive.show = this.show; this._primitive.debugShowBoundingVolume = this.debugShowBoundingVolume; @@ -998,6 +1189,11 @@ define([ this._primitive = this._primitive && this._primitive.destroy(); this._sp = this._sp && this._sp.destroy(); this._spPick = this._spPick && this._spPick.destroy(); + this._spColor = this._spColor && this._spColor.destroy(); + + // Derived programs, destroyed above if they existed. + this._spPick2D = undefined; + this._spColor2D = undefined; return destroyObject(this); }; diff --git a/Source/Scene/GroundPrimitive.js b/Source/Scene/GroundPrimitive.js index 6374117d3126..e6d1fd2fc149 100644 --- a/Source/Scene/GroundPrimitive.js +++ b/Source/Scene/GroundPrimitive.js @@ -4,6 +4,8 @@ define([ '../Core/Cartesian2', '../Core/Cartesian3', '../Core/Cartographic', + '../Core/Check', + '../Core/ColorGeometryInstanceAttribute', '../Core/defaultValue', '../Core/defined', '../Core/defineProperties', @@ -15,18 +17,24 @@ define([ '../Core/Math', '../Core/OrientedBoundingBox', '../Core/Rectangle', + '../Core/RectangleGeometry', '../Core/Resource', + '../Renderer/DrawCommand', '../Renderer/Pass', '../ThirdParty/when', './ClassificationPrimitive', './ClassificationType', - './SceneMode' + './PerInstanceColorAppearance', + './SceneMode', + './ShadowVolumeAppearance' ], function( BoundingSphere, buildModuleUrl, Cartesian2, Cartesian3, Cartographic, + Check, + ColorGeometryInstanceAttribute, defaultValue, defined, defineProperties, @@ -38,12 +46,16 @@ define([ CesiumMath, OrientedBoundingBox, Rectangle, + RectangleGeometry, Resource, + DrawCommand, Pass, when, ClassificationPrimitive, ClassificationType, - SceneMode) { + PerInstanceColorAppearance, + SceneMode, + ShadowVolumeAppearance) { 'use strict'; var GroundPrimitiveUniformMap = { @@ -53,14 +65,22 @@ define([ }; /** - * A ground primitive represents geometry draped over the terrain in the {@link Scene}. The geometry must be from a single {@link GeometryInstance}. - * Batching multiple geometries is not yet supported. + * A ground primitive represents geometry draped over the terrain in the {@link Scene}. *

- * A primitive combines the geometry instance with an {@link Appearance} that describes the full shading, including + * A primitive combines geometry instances with an {@link Appearance} that describes the full shading, including * {@link Material} and {@link RenderState}. Roughly, the geometry instance defines the structure and placement, * and the appearance defines the visual characteristics. Decoupling geometry and appearance allows us to mix - * and match most of them and add a new geometry or appearance independently of each other. Only the {@link PerInstanceColorAppearance} - * is supported at this time. + * and match most of them and add a new geometry or appearance independently of each other. + * + * Only {@link PerInstanceColorAppearance} with the same color across all instances is supported at this time when + * classifying {@link ClassificationType}.CESIUM_3D_TILE and {@link ClassificationType}.BOTH. + * + * Support for the WEBGL_depth_texture extension is required to use GeometryInstances with different PerInstanceColors + * or materials besides PerInstanceColorAppearance. + * + * Textured GroundPrimitives were designed for notional patterns and are not meant for precisely mapping + * textures to terrain - for that use case, use {@link SingleTileImageryProvider}. + * *

*

* For correct rendering, this feature requires the EXT_frag_depth WebGL extension. For hardware that do not support this extension, there @@ -75,6 +95,7 @@ define([ * * @param {Object} [options] Object with the following properties: * @param {Array|GeometryInstance} [options.geometryInstances] The geometry instances to render. + * @param {Appearance} [options.appearance] The appearance used to render the primitive. Defaults to a flat PerInstanceColorAppearance when GeometryInstances have a color attribute. * @param {Boolean} [options.show=true] Determines if this primitive will be shown. * @param {Boolean} [options.vertexCacheOptimize=false] When true, geometry vertices are optimized for the pre and post-vertex-shader caches. * @param {Boolean} [options.interleave=false] When true, geometry vertex attributes are interleaved, which can slightly improve rendering performance but increases load time. @@ -136,6 +157,24 @@ define([ function GroundPrimitive(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); + var appearance = options.appearance; + var geometryInstances = options.geometryInstances; + if (!defined(appearance) && defined(geometryInstances)) { + var geometryInstancesArray = isArray(geometryInstances) ? geometryInstances : [geometryInstances]; + var geometryInstanceCount = geometryInstancesArray.length; + for (var i = 0; i < geometryInstanceCount; i++) { + var attributes = geometryInstancesArray[i].attributes; + if (defined(attributes) && defined(attributes.color)) { + appearance = new PerInstanceColorAppearance({ + flat : true + }); + break; + } + } + } + + this.appearance = appearance; + /** * The geometry instance rendered with this primitive. This may * be undefined if options.releaseGeometryInstances @@ -213,9 +252,12 @@ define([ this._boundingSpheresKeys = []; this._boundingSpheres = []; + this._useFragmentCulling = false; + var that = this; - this._primitiveOptions = { + this._classificationPrimitiveOptions = { geometryInstances : undefined, + appearance : undefined, vertexCacheOptimize : defaultValue(options.vertexCacheOptimize, false), interleave : defaultValue(options.interleave, false), releaseGeometryInstances : defaultValue(options.releaseGeometryInstances, true), @@ -243,7 +285,7 @@ define([ */ vertexCacheOptimize : { get : function() { - return this._primitiveOptions.vertexCacheOptimize; + return this._classificationPrimitiveOptions.vertexCacheOptimize; } }, @@ -259,7 +301,7 @@ define([ */ interleave : { get : function() { - return this._primitiveOptions.interleave; + return this._classificationPrimitiveOptions.interleave; } }, @@ -275,7 +317,7 @@ define([ */ releaseGeometryInstances : { get : function() { - return this._primitiveOptions.releaseGeometryInstances; + return this._classificationPrimitiveOptions.releaseGeometryInstances; } }, @@ -291,7 +333,7 @@ define([ */ allowPicking : { get : function() { - return this._primitiveOptions.allowPicking; + return this._classificationPrimitiveOptions.allowPicking; } }, @@ -307,7 +349,7 @@ define([ */ asynchronous : { get : function() { - return this._primitiveOptions.asynchronous; + return this._classificationPrimitiveOptions.asynchronous; } }, @@ -323,7 +365,7 @@ define([ */ compressVertices : { get : function() { - return this._primitiveOptions.compressVertices; + return this._classificationPrimitiveOptions.compressVertices; } }, @@ -593,6 +635,7 @@ define([ var commandList = frameState.commandList; var passes = frameState.passes; + var classificationPrimitive = groundPrimitive._primitive; if (passes.render) { var colorLength = colorCommands.length; var i; @@ -600,6 +643,14 @@ define([ for (i = 0; i < colorLength; ++i) { colorCommand = colorCommands[i]; + + // derive a separate appearance command for 2D if needed + if (frameState.mode !== SceneMode.SCENE3D && + colorCommand.shaderProgram === classificationPrimitive._spColor && + classificationPrimitive._needs2DShader) { + colorCommand = colorCommand.derivedCommands.appearance2D; + } + colorCommand.owner = groundPrimitive; colorCommand.modelMatrix = modelMatrix; colorCommand.boundingVolume = boundingVolumes[boundingVolumeIndex(i, colorLength)]; @@ -611,7 +662,7 @@ define([ } if (frameState.invertClassification) { - var ignoreShowCommands = groundPrimitive._primitive._commandsIgnoreShow; + var ignoreShowCommands = classificationPrimitive._commandsIgnoreShow; var ignoreShowCommandsLength = ignoreShowCommands.length; for (i = 0; i < ignoreShowCommandsLength; ++i) { @@ -629,13 +680,28 @@ define([ if (passes.pick) { var pickLength = pickCommands.length; - var primitive = groundPrimitive._primitive._primitive; - var pickOffsets = primitive._pickOffsets; - for (var j = 0; j < pickLength; ++j) { - var pickOffset = pickOffsets[boundingVolumeIndex(j, pickLength)]; - var bv = boundingVolumes[pickOffset.index]; + var pickOffsets; + if (!groundPrimitive._useFragmentCulling) { + // Must be using pick offsets + classificationPrimitive = groundPrimitive._primitive; + pickOffsets = classificationPrimitive._primitive._pickOffsets; + } + for (var j = 0; j < pickLength; ++j) { var pickCommand = pickCommands[j]; + + // derive a separate appearance command for 2D if needed + if (frameState.mode !== SceneMode.SCENE3D && + pickCommand.shaderProgram === classificationPrimitive._spPick && + classificationPrimitive._needs2DShader) { + pickCommand = pickCommand.derivedCommands.pick2D; + } + var bv = boundingVolumes[boundingVolumeIndex(j, pickLength)]; + if (!groundPrimitive._useFragmentCulling) { + var pickOffset = pickOffsets[boundingVolumeIndex(j, pickLength)]; + bv = boundingVolumes[pickOffset.index]; + } + pickCommand.owner = groundPrimitive; pickCommand.modelMatrix = modelMatrix; pickCommand.boundingVolume = bv; @@ -699,8 +765,14 @@ define([ return; } + //>>includeStart('debug', pragmas.debug); + if (this.classificationType !== ClassificationType.TERRAIN && !(this.appearance instanceof PerInstanceColorAppearance)) { + throw new DeveloperError('GroundPrimitives with Materials can only classify ClassificationType.TERRAIN at this time.'); + } + //>>includeEnd('debug'); + var that = this; - var primitiveOptions = this._primitiveOptions; + var primitiveOptions = this._classificationPrimitiveOptions; if (!defined(this._primitive)) { var ellipsoid = frameState.mapProjection.ellipsoid; @@ -720,7 +792,7 @@ define([ geometry = instance.geometry; var instanceRectangle = getRectangle(frameState, geometry); if (!defined(rectangle)) { - rectangle = instanceRectangle; + rectangle = Rectangle.clone(instanceRectangle); } else if (defined(instanceRectangle)) { Rectangle.union(rectangle, instanceRectangle, rectangle); } @@ -741,24 +813,74 @@ define([ } // Now compute the min/max heights for the primitive - setMinMaxTerrainHeights(this, rectangle, frameState.mapProjection.ellipsoid); + setMinMaxTerrainHeights(this, rectangle, ellipsoid); var exaggeration = frameState.terrainExaggeration; this._minHeight = this._minTerrainHeight * exaggeration; this._maxHeight = this._maxTerrainHeight * exaggeration; - for (i = 0; i < length; ++i) { - instance = instances[i]; - geometry = instance.geometry; - instanceType = geometry.constructor; - groundInstances[i] = new GeometryInstance({ - geometry : instanceType.createShadowVolume(geometry, getComputeMinimumHeightFunction(this), - getComputeMaximumHeightFunction(this)), - attributes : instance.attributes, - id : instance.id - }); + var useFragmentCulling = GroundPrimitive._supportsMaterials(frameState.context) && this.classificationType === ClassificationType.TERRAIN; + this._useFragmentCulling = useFragmentCulling; + + if (useFragmentCulling) { + // Determine whether to add spherical or planar extent attributes for computing texture coordinates. + // This depends on the size of the GeometryInstances. + var attributes; + var usePlanarExtents = true; + for (i = 0; i < length; ++i) { + instance = instances[i]; + geometry = instance.geometry; + rectangle = getRectangle(frameState, geometry); + if (ShadowVolumeAppearance.shouldUseSphericalCoordinates(rectangle)) { + usePlanarExtents = false; + break; + } + } + + for (i = 0; i < length; ++i) { + instance = instances[i]; + geometry = instance.geometry; + instanceType = geometry.constructor; + + var boundingRectangle = getRectangle(frameState, geometry); + var textureCoordinateRotationPoints = geometry.textureCoordinateRotationPoints; + + if (usePlanarExtents) { + attributes = ShadowVolumeAppearance.getPlanarTextureCoordinateAttributes(boundingRectangle, textureCoordinateRotationPoints, ellipsoid, frameState.mapProjection, this._maxHeight); + } else { + attributes = ShadowVolumeAppearance.getSphericalExtentGeometryInstanceAttributes(boundingRectangle, textureCoordinateRotationPoints, ellipsoid, frameState.mapProjection); + } + + var instanceAttributes = instance.attributes; + for (var attributeKey in instanceAttributes) { + if (instanceAttributes.hasOwnProperty(attributeKey)) { + attributes[attributeKey] = instanceAttributes[attributeKey]; + } + } + + groundInstances[i] = new GeometryInstance({ + geometry : instanceType.createShadowVolume(geometry, getComputeMinimumHeightFunction(this), + getComputeMaximumHeightFunction(this)), + attributes : attributes, + id : instance.id + }); + } + } else { + // ClassificationPrimitive will check if the colors are all the same if it detects lack of fragment culling attributes + for (i = 0; i < length; ++i) { + instance = instances[i]; + geometry = instance.geometry; + instanceType = geometry.constructor; + groundInstances[i] = new GeometryInstance({ + geometry : instanceType.createShadowVolume(geometry, getComputeMinimumHeightFunction(this), + getComputeMaximumHeightFunction(this)), + attributes : instance.attributes, + id : instance.id + }); + } } primitiveOptions.geometryInstances = groundInstances; + primitiveOptions.appearance = this.appearance; primitiveOptions._createBoundingVolumeFunction = function(frameState, geometry) { createBoundingVolume(that, frameState, geometry); @@ -784,6 +906,7 @@ define([ }); } + this._primitive.appearance = this.appearance; this._primitive.show = this.show; this._primitive.debugShowShadowVolume = this.debugShowShadowVolume; this._primitive.debugShowBoundingVolume = this.debugShowBoundingVolume; @@ -860,5 +983,31 @@ define([ return destroyObject(this); }; + /** + * Exposed for testing. + * + * @param {Context} context Rendering context + * @returns {Boolean} Whether or not the current context supports materials on GroundPrimitives. + * @private + */ + GroundPrimitive._supportsMaterials = function(context) { + return context.depthTexture; + }; + + /** + * Checks if the given Scene supports materials on GroundPrimitives. + * Materials on GroundPrimitives require support for the WEBGL_depth_texture extension. + * + * @param {Scene} scene The current scene. + * @returns {Boolean} Whether or not the current scene supports materials on GroundPrimitives. + */ + GroundPrimitive.supportsMaterials = function(scene) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('scene', scene); + //>>includeEnd('debug'); + + return GroundPrimitive._supportsMaterials(scene.frameState.context); + }; + return GroundPrimitive; }); diff --git a/Source/Scene/Scene.js b/Source/Scene/Scene.js index e492b4adb3b4..bef9dbfccadd 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -654,14 +654,6 @@ define([ */ this.cameraEventWaitTime = 500.0; - /** - * Set to true to copy the depth texture after rendering the globe. Makes czm_globeDepthTexture valid. - * @type {Boolean} - * @default false - * @private - */ - this.copyGlobeDepth = false; - /** * Blends the atmosphere to geometry far from the camera for horizon views. Allows for additional * performance improvements by rendering less geometry and dispatching less terrain requests. @@ -2248,7 +2240,7 @@ define([ executeCommand(commands[j], scene, context, passState); } - if (defined(globeDepth) && environmentState.useGlobeDepthFramebuffer && (scene.copyGlobeDepth || scene.debugShowGlobeDepth)) { + if (defined(globeDepth) && environmentState.useGlobeDepthFramebuffer) { globeDepth.update(context, passState); globeDepth.executeCopyDepth(context, passState); } @@ -2925,7 +2917,8 @@ define([ clear.execute(context, passState); // Update globe depth rendering based on the current context and clear the globe depth framebuffer. - var useGlobeDepthFramebuffer = environmentState.useGlobeDepthFramebuffer = !picking && defined(scene._globeDepth); + // Globe depth needs is copied for Pick to support picking batched geometries in GroundPrimitives. + var useGlobeDepthFramebuffer = environmentState.useGlobeDepthFramebuffer = defined(scene._globeDepth); if (useGlobeDepthFramebuffer) { scene._globeDepth.update(context, passState); scene._globeDepth.clear(context, passState, clearColor); diff --git a/Source/Scene/ShadowVolumeAppearance.js b/Source/Scene/ShadowVolumeAppearance.js new file mode 100644 index 000000000000..e18161cbc253 --- /dev/null +++ b/Source/Scene/ShadowVolumeAppearance.js @@ -0,0 +1,707 @@ +define([ + '../Core/Cartographic', + '../Core/Cartesian2', + '../Core/Cartesian3', + '../Core/Math', + '../Core/Check', + '../Core/ComponentDatatype', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/EncodedCartesian3', + '../Core/GeometryInstanceAttribute', + '../Core/Matrix4', + '../Core/Rectangle', + '../Core/Transforms', + '../Renderer/ShaderSource', + '../Scene/PerInstanceColorAppearance', + '../Shaders/ShadowVolumeAppearanceFS' +], function( + Cartographic, + Cartesian2, + Cartesian3, + CesiumMath, + Check, + ComponentDatatype, + defaultValue, + defined, + defineProperties, + EncodedCartesian3, + GeometryInstanceAttribute, + Matrix4, + Rectangle, + Transforms, + ShaderSource, + PerInstanceColorAppearance, + ShadowVolumeAppearanceFS) { + 'use strict'; + + /** + * Creates shaders for a ClassificationPrimitive to use a given Appearance, as well as for picking. + * + * @param {Boolean} extentsCulling Discard fragments outside the instance's texture coordinate extents. + * @param {Boolean} planarExtents If true, texture coordinates will be computed using planes instead of spherical coordinates. + * @param {Appearance} appearance An Appearance to be used with a ClassificationPrimitive via GroundPrimitive. + * @private + */ + function ShadowVolumeAppearance(extentsCulling, planarExtents, appearance) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bool('extentsCulling', extentsCulling); + Check.typeOf.bool('planarExtents', planarExtents); + Check.typeOf.object('appearance', appearance); + //>>includeEnd('debug'); + + // Compute shader dependencies + var colorShaderDependencies = new ShaderDependencies(); + colorShaderDependencies.requiresTextureCoordinates = extentsCulling; + colorShaderDependencies.requiresEC = !appearance.flat; + + var pickShaderDependencies = new ShaderDependencies(); + pickShaderDependencies.requiresTextureCoordinates = extentsCulling; + + if (appearance instanceof PerInstanceColorAppearance) { + // PerInstanceColorAppearance doesn't have material.shaderSource, instead it has its own vertex and fragment shaders + colorShaderDependencies.requiresNormalEC = !appearance.flat; + } else { + // Scan material source for what hookups are needed. Assume czm_materialInput materialInput. + var materialShaderSource = appearance.material.shaderSource + '\n' + appearance.fragmentShaderSource; + + colorShaderDependencies.normalEC = materialShaderSource.indexOf('materialInput.normalEC') !== -1 || materialShaderSource.indexOf('czm_getDefaultMaterial') !== -1; + colorShaderDependencies.positionToEyeEC = materialShaderSource.indexOf('materialInput.positionToEyeEC') !== -1; + colorShaderDependencies.tangentToEyeMatrix = materialShaderSource.indexOf('materialInput.tangentToEyeMatrix') !== -1; + colorShaderDependencies.st = materialShaderSource.indexOf('materialInput.st') !== -1; + } + + this._colorShaderDependencies = colorShaderDependencies; + this._pickShaderDependencies = pickShaderDependencies; + this._appearance = appearance; + this._extentsCulling = extentsCulling; + this._planarExtents = planarExtents; + } + + /** + * Create the fragment shader for a ClassificationPrimitive's color pass when rendering for color. + * + * @param {Boolean} columbusView2D Whether the shader will be used for Columbus View or 2D. + * @returns {ShaderSource} Shader source for the fragment shader. + */ + ShadowVolumeAppearance.prototype.createFragmentShader = function(columbusView2D) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bool('columbusView2D', columbusView2D); + //>>includeEnd('debug'); + + var appearance = this._appearance; + var dependencies = this._colorShaderDependencies; + + var defines = []; + if (!columbusView2D && !this._planarExtents) { + defines.push('SPHERICAL'); + } + if (dependencies.requiresEC) { + defines.push('REQUIRES_EC'); + } + if (dependencies.requiresWC) { + defines.push('REQUIRES_WC'); + } + if (dependencies.requiresTextureCoordinates) { + defines.push('TEXTURE_COORDINATES'); + } + if (this._extentsCulling) { + defines.push('CULL_FRAGMENTS'); + } + if (dependencies.requiresNormalEC) { + defines.push('NORMAL_EC'); + } + if (appearance instanceof PerInstanceColorAppearance) { + defines.push('PER_INSTANCE_COLOR'); + } + + // Material inputs. Use of parameters in the material is different + // from requirement of the parameters in the overall shader, for example, + // texture coordinates may be used for fragment culling but not for the material itself. + if (dependencies.normalEC) { + defines.push('USES_NORMAL_EC'); + } + if (dependencies.positionToEyeEC) { + defines.push('USES_POSITION_TO_EYE_EC'); + } + if (dependencies.tangentToEyeMatrix) { + defines.push('USES_TANGENT_TO_EYE'); + } + if (dependencies.st) { + defines.push('USES_ST'); + } + + if (appearance.flat) { + defines.push('FLAT'); + } + + var materialSource = ''; + if (!(appearance instanceof PerInstanceColorAppearance)) { + materialSource = appearance.material.shaderSource; + } + + return new ShaderSource({ + defines : defines, + sources : [materialSource, ShadowVolumeAppearanceFS] + }); + }; + + ShadowVolumeAppearance.prototype.createPickFragmentShader = function(columbusView2D) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.bool('columbusView2D', columbusView2D); + //>>includeEnd('debug'); + + var dependencies = this._pickShaderDependencies; + + var defines = ['PICK']; + if (!columbusView2D && !this._planarExtents) { + defines.push('SPHERICAL'); + } + if (dependencies.requiresEC) { + defines.push('REQUIRES_EC'); + } + if (dependencies.requiresWC) { + defines.push('REQUIRES_WC'); + } + if (dependencies.requiresTextureCoordinates) { + defines.push('TEXTURE_COORDINATES'); + } + if (this._extentsCulling) { + defines.push('CULL_FRAGMENTS'); + } + return new ShaderSource({ + defines : defines, + sources : [ShadowVolumeAppearanceFS], + pickColorQualifier : 'varying' + }); + }; + + /** + * Create the vertex shader for a ClassificationPrimitive's color pass on the final of 3 shadow volume passes + * + * @param {String[]} defines External defines to pass to the vertex shader. + * @param {String} vertexShaderSource ShadowVolumeAppearanceVS with any required modifications for computing position. + * @param {Boolean} columbusView2D Whether the shader will be used for Columbus View or 2D. + * @returns {String} Shader source for the vertex shader. + */ + ShadowVolumeAppearance.prototype.createVertexShader = function(defines, vertexShaderSource, columbusView2D) { + //>>includeStart('debug', pragmas.debug); + Check.defined('defines', defines); + Check.typeOf.string('vertexShaderSource', vertexShaderSource); + Check.typeOf.bool('columbusView2D', columbusView2D); + //>>includeEnd('debug'); + return createShadowVolumeAppearanceVS(this._colorShaderDependencies, this._planarExtents, columbusView2D, defines, vertexShaderSource, this._appearance); + }; + + /** + * Create the vertex shader for a ClassificationPrimitive's pick pass on the final of 3 shadow volume passes + * + * @param {String[]} defines External defines to pass to the vertex shader. + * @param {String} vertexShaderSource ShadowVolumeAppearanceVS with any required modifications for computing position and picking. + * @param {Boolean} columbusView2D Whether the shader will be used for Columbus View or 2D. + * @returns {String} Shader source for the vertex shader. + */ + ShadowVolumeAppearance.prototype.createPickVertexShader = function(defines, vertexShaderSource, columbusView2D) { + //>>includeStart('debug', pragmas.debug); + Check.defined('defines', defines); + Check.typeOf.string('vertexShaderSource', vertexShaderSource); + Check.typeOf.bool('columbusView2D', columbusView2D); + //>>includeEnd('debug'); + return createShadowVolumeAppearanceVS(this._pickShaderDependencies, this._planarExtents, columbusView2D, defines, vertexShaderSource); + }; + + function createShadowVolumeAppearanceVS(shaderDependencies, planarExtents, columbusView2D, defines, vertexShaderSource, appearance) { + var allDefines = defines.slice(); + + if (defined(appearance) && appearance instanceof PerInstanceColorAppearance) { + allDefines.push('PER_INSTANCE_COLOR'); + } + if (shaderDependencies.requiresTextureCoordinates) { + allDefines.push('TEXTURE_COORDINATES'); + if (!(planarExtents || columbusView2D)) { + allDefines.push('SPHERICAL'); + } + if (columbusView2D) { + allDefines.push('COLUMBUS_VIEW_2D'); + } + } + + return new ShaderSource({ + defines : allDefines, + sources : [vertexShaderSource] + }); + } + + /** + * Tracks shader dependencies. + * @private + */ + function ShaderDependencies() { + this._requiresEC = false; + this._requiresWC = false; // depends on eye coordinates, needed for material and for phong + this._requiresNormalEC = false; // depends on eye coordinates, needed for material + this._requiresTextureCoordinates = false; // depends on world coordinates, needed for material and for culling + + this._usesNormalEC = false; + this._usesPositionToEyeEC = false; + this._usesTangentToEyeMat = false; + this._usesSt = false; + } + + defineProperties(ShaderDependencies.prototype, { + // Set when assessing final shading (flat vs. phong) and culling using computed texture coordinates + requiresEC : { + get : function() { + return this._requiresEC; + }, + set : function(value) { + this._requiresEC = value || this._requiresEC; + } + }, + requiresWC : { + get : function() { + return this._requiresWC; + }, + set : function(value) { + this._requiresWC = value || this._requiresWC; + this.requiresEC = this._requiresWC; + } + }, + requiresNormalEC : { + get : function() { + return this._requiresNormalEC; + }, + set : function(value) { + this._requiresNormalEC = value || this._requiresNormalEC; + this.requiresEC = this._requiresNormalEC; + } + }, + requiresTextureCoordinates : { + get : function() { + return this._requiresTextureCoordinates; + }, + set : function(value) { + this._requiresTextureCoordinates = value || this._requiresTextureCoordinates; + this.requiresWC = this._requiresTextureCoordinates; + } + }, + // Get/Set when assessing material hookups + normalEC : { + set : function(value) { + this.requiresNormalEC = value; + this._usesNormalEC = value; + }, + get : function() { + return this._usesNormalEC; + } + }, + tangentToEyeMatrix : { + set : function(value) { + this.requiresWC = value; + this.requiresNormalEC = value; + this._usesTangentToEyeMat = value; + }, + get : function() { + return this._usesTangentToEyeMat; + } + }, + positionToEyeEC : { + set : function(value) { + this.requiresEC = value; + this._usesPositionToEyeEC = value; + }, + get : function() { + return this._usesPositionToEyeEC; + } + }, + st : { + set : function(value) { + this.requiresTextureCoordinates = value; + this._usesSt = value; + }, + get : function() { + return this._usesSt; + } + } + }); + + function pointLineDistance(point1, point2, point) { + return Math.abs((point2.y - point1.y) * point.x - (point2.x - point1.x) * point.y + point2.x * point1.y - point2.y * point1.x) / Cartesian2.distance(point2, point1); + } + + var points2DScratch = [new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2()]; + + // textureCoordinateRotationPoints form 2 lines in the computed UV space that remap to desired texture coordinates. + // This allows simulation of baked texture coordinates for EllipseGeometry, RectangleGeometry, and PolygonGeometry. + function addTextureCoordinateRotationAttributes(attributes, textureCoordinateRotationPoints) { + var points2D = points2DScratch; + + var minXYCorner = Cartesian2.unpack(textureCoordinateRotationPoints, 0, points2D[0]); + var maxYCorner = Cartesian2.unpack(textureCoordinateRotationPoints, 2, points2D[1]); + var maxXCorner = Cartesian2.unpack(textureCoordinateRotationPoints, 4, points2D[2]); + + attributes.uMaxVmax = new GeometryInstanceAttribute({ + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + normalize: false, + value : [maxYCorner.x, maxYCorner.y, maxXCorner.x, maxXCorner.y] + }); + + var inverseExtentX = 1.0 / pointLineDistance(minXYCorner, maxYCorner, maxXCorner); + var inverseExtentY = 1.0 / pointLineDistance(minXYCorner, maxXCorner, maxYCorner); + + attributes.uvMinAndExtents = new GeometryInstanceAttribute({ + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + normalize: false, + value : [minXYCorner.x, minXYCorner.y, inverseExtentX, inverseExtentY] + }); + } + + var cartographicScratch = new Cartographic(); + var cornerScratch = new Cartesian3(); + var northWestScratch = new Cartesian3(); + var southEastScratch = new Cartesian3(); + var highLowScratch = {high : 0.0, low : 0.0}; + function add2DTextureCoordinateAttributes(rectangle, projection, attributes) { + // Compute corner positions in double precision + var carto = cartographicScratch; + carto.height = 0.0; + + carto.longitude = rectangle.west; + carto.latitude = rectangle.south; + + var southWestCorner = projection.project(carto, cornerScratch); + + carto.latitude = rectangle.north; + var northWest = projection.project(carto, northWestScratch); + + carto.longitude = rectangle.east; + carto.latitude = rectangle.south; + var southEast = projection.project(carto, southEastScratch); + + // Since these positions are all in the 2D plane, there's a lot of zeros + // and a lot of repetition. So we only need to encode 4 values. + // Encode: + // x: x value for southWestCorner + // y: y value for southWestCorner + // z: y value for northWest + // w: x value for southEast + var valuesHigh = [0, 0, 0, 0]; + var valuesLow = [0, 0, 0, 0]; + var encoded = EncodedCartesian3.encode(southWestCorner.x, highLowScratch); + valuesHigh[0] = encoded.high; + valuesLow[0] = encoded.low; + + encoded = EncodedCartesian3.encode(southWestCorner.y, highLowScratch); + valuesHigh[1] = encoded.high; + valuesLow[1] = encoded.low; + + encoded = EncodedCartesian3.encode(northWest.y, highLowScratch); + valuesHigh[2] = encoded.high; + valuesLow[2] = encoded.low; + + encoded = EncodedCartesian3.encode(southEast.x, highLowScratch); + valuesHigh[3] = encoded.high; + valuesLow[3] = encoded.low; + + attributes.planes2D_HIGH = new GeometryInstanceAttribute({ + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + normalize: false, + value : valuesHigh + }); + + attributes.planes2D_LOW = new GeometryInstanceAttribute({ + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + normalize: false, + value : valuesLow + }); + } + + var enuMatrixScratch = new Matrix4(); + var inverseEnuScratch = new Matrix4(); + var rectanglePointCartesianScratch = new Cartesian3(); + var rectangleCenterScratch = new Cartographic(); + var pointsCartographicScratch = [ + new Cartographic(), + new Cartographic(), + new Cartographic(), + new Cartographic(), + new Cartographic(), + new Cartographic(), + new Cartographic(), + new Cartographic() + ]; + /** + * When computing planes to bound the rectangle, + * need to factor in "bulge" and other distortion. + * Flatten the ellipsoid-centered corners and edge-centers of the rectangle + * into the plane of the local ENU system, compute bounds in 2D, and + * project back to ellipsoid-centered. + */ + function computeRectangleBounds(rectangle, ellipsoid, height, southWestCornerResult, eastVectorResult, northVectorResult) { + // Compute center of rectangle + var centerCartographic = Rectangle.center(rectangle, rectangleCenterScratch); + centerCartographic.height = height; + var centerCartesian = Cartographic.toCartesian(centerCartographic, ellipsoid, rectanglePointCartesianScratch); + var enuMatrix = Transforms.eastNorthUpToFixedFrame(centerCartesian, ellipsoid, enuMatrixScratch); + var inverseEnu = Matrix4.inverse(enuMatrix, inverseEnuScratch); + + var west = rectangle.west; + var east = rectangle.east; + var north = rectangle.north; + var south = rectangle.south; + + var cartographics = pointsCartographicScratch; + cartographics[0].latitude = south; + cartographics[0].longitude = west; + cartographics[1].latitude = north; + cartographics[1].longitude = west; + cartographics[2].latitude = north; + cartographics[2].longitude = east; + cartographics[3].latitude = south; + cartographics[3].longitude = east; + + var longitudeCenter = (west + east) * 0.5; + var latitudeCenter = (north + south) * 0.5; + + cartographics[4].latitude = south; + cartographics[4].longitude = longitudeCenter; + cartographics[5].latitude = north; + cartographics[5].longitude = longitudeCenter; + cartographics[6].latitude = latitudeCenter; + cartographics[6].longitude = west; + cartographics[7].latitude = latitudeCenter; + cartographics[7].longitude = east; + + var minX = Number.POSITIVE_INFINITY; + var maxX = Number.NEGATIVE_INFINITY; + var minY = Number.POSITIVE_INFINITY; + var maxY = Number.NEGATIVE_INFINITY; + for (var i = 0; i < 8; i++) { + cartographics[i].height = height; + var pointCartesian = Cartographic.toCartesian(cartographics[i], ellipsoid, rectanglePointCartesianScratch); + Matrix4.multiplyByPoint(inverseEnu, pointCartesian, pointCartesian); + pointCartesian.z = 0.0; // flatten into XY plane of ENU coordinate system + minX = Math.min(minX, pointCartesian.x); + maxX = Math.max(maxX, pointCartesian.x); + minY = Math.min(minY, pointCartesian.y); + maxY = Math.max(maxY, pointCartesian.y); + } + + var southWestCorner = southWestCornerResult; + southWestCorner.x = minX; + southWestCorner.y = minY; + southWestCorner.z = 0.0; + Matrix4.multiplyByPoint(enuMatrix, southWestCorner, southWestCorner); + + var southEastCorner = eastVectorResult; + southEastCorner.x = maxX; + southEastCorner.y = minY; + southEastCorner.z = 0.0; + Matrix4.multiplyByPoint(enuMatrix, southEastCorner, southEastCorner); + // make eastward vector + Cartesian3.subtract(southEastCorner, southWestCorner, eastVectorResult); + + var northWestCorner = northVectorResult; + northWestCorner.x = minX; + northWestCorner.y = maxY; + northWestCorner.z = 0.0; + Matrix4.multiplyByPoint(enuMatrix, northWestCorner, northWestCorner); + // make eastward vector + Cartesian3.subtract(northWestCorner, southWestCorner, northVectorResult); + } + + var eastwardScratch = new Cartesian3(); + var northwardScratch = new Cartesian3(); + var encodeScratch = new EncodedCartesian3(); + /** + * Gets an attributes object containing: + * - 3 high-precision points as 6 GeometryInstanceAttributes. These points are used to compute eye-space planes. + * - 1 texture coordinate rotation GeometryInstanceAttributes + * - 2 GeometryInstanceAttributes used to compute high-precision points in 2D and Columbus View. + * These points are used to compute eye-space planes like above. + * + * Used to compute texture coordinates for small-area ClassificationPrimitives with materials or multiple non-overlapping instances. + * + * @see ShadowVolumeAppearance + * @private + * + * @param {Rectangle} boundingRectangle Rectangle object that the points will approximately bound + * @param {Number[]} textureCoordinateRotationPoints Points in the computed texture coordinate system for remapping texture coordinates + * @param {Ellipsoid} ellipsoid Ellipsoid for converting Rectangle points to world coordinates + * @param {MapProjection} projection The MapProjection used for 2D and Columbus View. + * @param {Number} [height=0] The maximum height for the shadow volume. + * @returns {Object} An attributes dictionary containing planar texture coordinate attributes. + */ + ShadowVolumeAppearance.getPlanarTextureCoordinateAttributes = function(boundingRectangle, textureCoordinateRotationPoints, ellipsoid, projection, height) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('boundingRectangle', boundingRectangle); + Check.defined('textureCoordinateRotationPoints', textureCoordinateRotationPoints); + Check.typeOf.object('ellipsoid', ellipsoid); + Check.typeOf.object('projection', projection); + //>>includeEnd('debug'); + + var corner = cornerScratch; + var eastward = eastwardScratch; + var northward = northwardScratch; + computeRectangleBounds(boundingRectangle, ellipsoid, defaultValue(height, 0.0), corner, eastward, northward); + + var attributes = {}; + addTextureCoordinateRotationAttributes(attributes, textureCoordinateRotationPoints); + + var encoded = EncodedCartesian3.fromCartesian(corner, encodeScratch); + attributes.southWest_HIGH = new GeometryInstanceAttribute({ + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 3, + normalize: false, + value : Cartesian3.pack(encoded.high, [0, 0, 0]) + }); + attributes.southWest_LOW = new GeometryInstanceAttribute({ + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 3, + normalize: false, + value : Cartesian3.pack(encoded.low, [0, 0, 0]) + }); + attributes.eastward = new GeometryInstanceAttribute({ + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 3, + normalize: false, + value : Cartesian3.pack(eastward, [0, 0, 0]) + }); + attributes.northward = new GeometryInstanceAttribute({ + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 3, + normalize: false, + value : Cartesian3.pack(northward, [0, 0, 0]) + }); + + add2DTextureCoordinateAttributes(boundingRectangle, projection, attributes); + return attributes; + }; + + var spherePointScratch = new Cartesian3(); + function latLongToSpherical(latitude, longitude, ellipsoid, result) { + var cartographic = cartographicScratch; + cartographic.latitude = latitude; + cartographic.longitude = longitude; + cartographic.height = 0.0; + + var spherePoint = Cartographic.toCartesian(cartographic, ellipsoid, spherePointScratch); + + // Project into plane with vertical for latitude + var magXY = Math.sqrt(spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y); + + // Use fastApproximateAtan2 for alignment with shader + var sphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z); + var sphereLongitude = CesiumMath.fastApproximateAtan2(spherePoint.x, spherePoint.y); + + result.x = sphereLatitude; + result.y = sphereLongitude; + + return result; + } + + var sphericalScratch = new Cartesian2(); + /** + * Gets an attributes object containing: + * - the southwest corner of a rectangular area in spherical coordinates, as well as the inverse of the latitude/longitude range. + * These are computed using the same atan2 approximation used in the shader. + * - 1 texture coordinate rotation GeometryInstanceAttributes + * - 2 GeometryInstanceAttributes used to compute high-precision points in 2D and Columbus View. + * These points are used to compute eye-space planes like above. + * + * Used when computing texture coordinates for large-area ClassificationPrimitives with materials or + * multiple non-overlapping instances. + * @see ShadowVolumeAppearance + * @private + * + * @param {Rectangle} boundingRectangle Rectangle object that the spherical extents will approximately bound + * @param {Number[]} textureCoordinateRotationPoints Points in the computed texture coordinate system for remapping texture coordinates + * @param {Ellipsoid} ellipsoid Ellipsoid for converting Rectangle points to world coordinates + * @param {MapProjection} projection The MapProjection used for 2D and Columbus View. + * @returns {Object} An attributes dictionary containing spherical texture coordinate attributes. + */ + ShadowVolumeAppearance.getSphericalExtentGeometryInstanceAttributes = function(boundingRectangle, textureCoordinateRotationPoints, ellipsoid, projection) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('boundingRectangle', boundingRectangle); + Check.defined('textureCoordinateRotationPoints', textureCoordinateRotationPoints); + Check.typeOf.object('ellipsoid', ellipsoid); + Check.typeOf.object('projection', projection); + //>>includeEnd('debug'); + + // rectangle cartographic coords !== spherical because it's on an ellipsoid + var southWestExtents = latLongToSpherical(boundingRectangle.south, boundingRectangle.west, ellipsoid, sphericalScratch); + + // Slightly pad extents to avoid floating point error when fragment culling at edges. + var south = southWestExtents.x - CesiumMath.EPSILON5; + var west = southWestExtents.y - CesiumMath.EPSILON5; + + var northEastExtents = latLongToSpherical(boundingRectangle.north, boundingRectangle.east, ellipsoid, sphericalScratch); + var north = northEastExtents.x + CesiumMath.EPSILON5; + var east = northEastExtents.y + CesiumMath.EPSILON5; + + var longitudeRangeInverse = 1.0 / (east - west); + var latitudeRangeInverse = 1.0 / (north - south); + + var attributes = { + sphericalExtents : new GeometryInstanceAttribute({ + componentDatatype: ComponentDatatype.FLOAT, + componentsPerAttribute: 4, + normalize: false, + value : [south, west, latitudeRangeInverse, longitudeRangeInverse] + }) + }; + + addTextureCoordinateRotationAttributes(attributes, textureCoordinateRotationPoints); + add2DTextureCoordinateAttributes(boundingRectangle, projection, attributes); + return attributes; + }; + + ShadowVolumeAppearance.hasAttributesForTextureCoordinatePlanes = function(attributes) { + return defined(attributes.southWest_HIGH) && defined(attributes.southWest_LOW) && + defined(attributes.northward) && defined(attributes.eastward) && + defined(attributes.planes2D_HIGH) && defined(attributes.planes2D_LOW) && + defined(attributes.uMaxVmax) && defined(attributes.uvMinAndExtents); + }; + + ShadowVolumeAppearance.hasAttributesForSphericalExtents = function(attributes) { + return defined(attributes.sphericalExtents) && + defined(attributes.planes2D_HIGH) && defined(attributes.planes2D_LOW) && + defined(attributes.uMaxVmax) && defined(attributes.uvMinAndExtents); + }; + + function shouldUseSpherical(rectangle) { + return Math.max(rectangle.width, rectangle.height) > ShadowVolumeAppearance.MAX_WIDTH_FOR_PLANAR_EXTENTS; + } + + /** + * Computes whether the given rectangle is wide enough that texture coordinates + * over its area should be computed using spherical extents instead of distance to planes. + * + * @param {Rectangle} rectangle A rectangle + * @private + */ + ShadowVolumeAppearance.shouldUseSphericalCoordinates = function(rectangle) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('rectangle', rectangle); + //>>includeEnd('debug'); + + return shouldUseSpherical(rectangle); + }; + + /** + * Texture coordinates for ground primitives are computed either using spherical coordinates for large areas or + * using distance from planes for small areas. + * + * @type {Number} + * @constant + * @private + */ + ShadowVolumeAppearance.MAX_WIDTH_FOR_PLANAR_EXTENTS = CesiumMath.toRadians(1.0); + + return ShadowVolumeAppearance; +}); diff --git a/Source/Scene/Vector3DTilePrimitive.js b/Source/Scene/Vector3DTilePrimitive.js index 112035c91107..b9cbb67804bc 100644 --- a/Source/Scene/Vector3DTilePrimitive.js +++ b/Source/Scene/Vector3DTilePrimitive.js @@ -18,7 +18,7 @@ define([ '../Renderer/ShaderSource', '../Renderer/VertexArray', '../Shaders/ShadowVolumeFS', - '../Shaders/ShadowVolumeVS', + '../Shaders/VectorTileVS', './BlendingState', './Cesium3DTileFeature', './ClassificationType', @@ -47,7 +47,7 @@ define([ ShaderSource, VertexArray, ShadowVolumeFS, - ShadowVolumeVS, + VectorTileVS, BlendingState, Cesium3DTileFeature, ClassificationType, @@ -297,11 +297,10 @@ define([ return; } - var vsSource = batchTable.getVertexShaderCallback(false, 'a_batchId', undefined)(ShadowVolumeVS); + var vsSource = batchTable.getVertexShaderCallback(false, 'a_batchId', undefined)(VectorTileVS); var fsSource = batchTable.getFragmentShaderCallback()(ShadowVolumeFS, false, undefined); var vs = new ShaderSource({ - defines : ['VECTOR_TILE'], sources : [vsSource] }); var fs = new ShaderSource({ @@ -317,8 +316,7 @@ define([ }); vs = new ShaderSource({ - defines : ['VECTOR_TILE'], - sources : [ShadowVolumeVS] + sources : [VectorTileVS] }); fs = new ShaderSource({ defines : ['VECTOR_TILE'], @@ -332,11 +330,10 @@ define([ attributeLocations : attributeLocations }); - vsSource = batchTable.getPickVertexShaderCallbackIgnoreShow('a_batchId')(ShadowVolumeVS); + vsSource = batchTable.getPickVertexShaderCallbackIgnoreShow('a_batchId')(VectorTileVS); fsSource = batchTable.getPickFragmentShaderCallbackIgnoreShow()(ShadowVolumeFS); var pickVS = new ShaderSource({ - defines : ['VECTOR_TILE'], sources : [vsSource] }); var pickFS = new ShaderSource({ diff --git a/Source/Shaders/Builtin/Functions/approximateSphericalCoordinates.glsl b/Source/Shaders/Builtin/Functions/approximateSphericalCoordinates.glsl new file mode 100644 index 000000000000..90fe79393a3d --- /dev/null +++ b/Source/Shaders/Builtin/Functions/approximateSphericalCoordinates.glsl @@ -0,0 +1,51 @@ +// Based on Michal Drobot's approximation from ShaderFastLibs, which in turn is based on +// "Efficient approximations for the arctangent function," Rajan, S. Sichun Wang Inkol, R. Joyal, A., May 2006. +// Adapted from ShaderFastLibs under MIT License. +// +// Chosen for the following characteristics over range [0, 1]: +// - basically no error at 0 and 1, important for getting around range limit (naive atan2 via atan requires infinite range atan) +// - no visible artifacts from first-derivative discontinuities, unlike latitude via range-reduced sqrt asin approximations (at equator) +// +// The original code is x * (-0.1784 * abs(x) - 0.0663 * x * x + 1.0301); +// Removed the abs() in here because it isn't needed, the input range is guaranteed as [0, 1] by how we're approximating atan2. +float fastApproximateAtan01(float x) { + return x * (-0.1784 * x - 0.0663 * x * x + 1.0301); +} + +// Range reduction math based on nvidia's cg reference implementation for atan2: http://developer.download.nvidia.com/cg/atan2.html +// However, we replaced their atan curve with Michael Drobot's. +float fastApproximateAtan2(float x, float y) { + // atan approximations are usually only reliable over [-1, 1], or, in our case, [0, 1] due to modifications. + // So range-reduce using abs and by flipping whether x or y is on top. + float t = abs(x); // t used as swap and atan result. + float opposite = abs(y); + float adjacent = max(t, opposite); + opposite = min(t, opposite); + + t = fastApproximateAtan01(opposite / adjacent); + + // Undo range reduction + t = czm_branchFreeTernaryFloat(abs(y) > abs(x), czm_piOverTwo - t, t); + t = czm_branchFreeTernaryFloat(x < 0.0, czm_pi - t, t); + t = czm_branchFreeTernaryFloat(y < 0.0, -t, t); + return t; +} + +/** + * Approximately computes spherical coordinates given a normal. + * Uses approximate inverse trigonometry for speed and consistency, + * since inverse trigonometry can differ from vendor-to-vendor and when compared with the CPU. + * + * @name czm_approximateSphericalCoordinates + * @glslFunction + * + * @param {vec3} normal arbitrary-length normal. + * + * @returns {vec2} Approximate latitude and longitude spherical coordinates. + */ +vec2 czm_approximateSphericalCoordinates(vec3 normal) { + // Project into plane with vertical for latitude + float latitudeApproximation = fastApproximateAtan2(sqrt(normal.x * normal.x + normal.y * normal.y), normal.z); + float longitudeApproximation = fastApproximateAtan2(normal.x, normal.y); + return vec2(latitudeApproximation, longitudeApproximation); +} diff --git a/Source/Shaders/Builtin/Functions/branchFreeTernaryFloat.glsl b/Source/Shaders/Builtin/Functions/branchFreeTernaryFloat.glsl new file mode 100644 index 000000000000..951f2b155cee --- /dev/null +++ b/Source/Shaders/Builtin/Functions/branchFreeTernaryFloat.glsl @@ -0,0 +1,17 @@ +/** + * Branchless ternary operator to be used when it's inexpensive to explicitly + * evaluate both possibilities for a float expression. + * + * @name czm_branchFreeTernaryFloat + * @glslFunction + * + * @param {bool} comparison A comparison statement + * @param {float} a Value to return if the comparison is true. + * @param {float} b Value to return if the comparison is false. + * + * @returns {float} equivalent of comparison ? a : b + */ +float czm_branchFreeTernaryFloat(bool comparison, float a, float b) { + float useA = float(comparison); + return a * useA + b * (1.0 - useA); +} diff --git a/Source/Shaders/Builtin/Functions/getDefaultMaterial.glsl b/Source/Shaders/Builtin/Functions/getDefaultMaterial.glsl index 409d00bf3e72..14ade737e033 100644 --- a/Source/Shaders/Builtin/Functions/getDefaultMaterial.glsl +++ b/Source/Shaders/Builtin/Functions/getDefaultMaterial.glsl @@ -4,10 +4,10 @@ * The default normal value is given by materialInput.normalEC. * * @name czm_getDefaultMaterial - * @glslFunction + * @glslFunction * * @param {czm_materialInput} input The input used to construct the default material. - * + * * @returns {czm_material} The default material. * * @see czm_materialInput diff --git a/Source/Shaders/Builtin/Functions/lineDistance.glsl b/Source/Shaders/Builtin/Functions/lineDistance.glsl new file mode 100644 index 000000000000..4a239d93339f --- /dev/null +++ b/Source/Shaders/Builtin/Functions/lineDistance.glsl @@ -0,0 +1,14 @@ +/** + * Computes distance from an point in 2D to a line in 2D. + * + * @name czm_lineDistance + * @glslFunction + * + * param {vec2} point1 A point along the line. + * param {vec2} point2 A point along the line. + * param {vec2} point A point that may or may not be on the line. + * returns {float} The distance from the point to the line. + */ +float czm_lineDistance(vec2 point1, vec2 point2, vec2 point) { + return abs((point2.y - point1.y) * point.x - (point2.x - point1.x) * point.y + point2.x * point1.y - point2.y * point1.x) / distance(point2, point1); +} diff --git a/Source/Shaders/Builtin/Functions/planeDistance.glsl b/Source/Shaders/Builtin/Functions/planeDistance.glsl new file mode 100644 index 000000000000..38db05a173c6 --- /dev/null +++ b/Source/Shaders/Builtin/Functions/planeDistance.glsl @@ -0,0 +1,13 @@ +/** + * Computes distance from an point to a plane, typically in eye space. + * + * @name czm_planeDistance + * @glslFunction + * + * param {vec4} plane A Plane in Hessian Normal Form. See Plane.js + * param {vec3} point A point in the same space as the plane. + * returns {float} The distance from the point to the plane. + */ +float czm_planeDistance(vec4 plane, vec3 point) { + return (dot(plane.xyz, point) + plane.w); +} diff --git a/Source/Shaders/Builtin/Functions/reverseLogDepth.glsl b/Source/Shaders/Builtin/Functions/reverseLogDepth.glsl index e8719f24a9ba..c6033d6d8782 100644 --- a/Source/Shaders/Builtin/Functions/reverseLogDepth.glsl +++ b/Source/Shaders/Builtin/Functions/reverseLogDepth.glsl @@ -3,7 +3,7 @@ float czm_reverseLogDepth(float logZ) #ifdef LOG_DEPTH float near = czm_currentFrustum.x; float far = czm_currentFrustum.y; - logZ = pow(2.0, logZ * log2(far + 1.0)) - 1.0; + logZ = pow(2.0, logZ * czm_log2FarPlusOne) - 1.0; logZ = far * (1.0 - near / logZ) / (far - near); #endif return logZ; diff --git a/Source/Shaders/Builtin/Functions/vertexLogDepth.glsl b/Source/Shaders/Builtin/Functions/vertexLogDepth.glsl index fffc82156b36..fb527f1d45d6 100644 --- a/Source/Shaders/Builtin/Functions/vertexLogDepth.glsl +++ b/Source/Shaders/Builtin/Functions/vertexLogDepth.glsl @@ -14,7 +14,7 @@ void czm_updatePositionDepth() { } #endif - gl_Position.z = log2(max(1e-6, 1.0 + gl_Position.w)) * czm_logFarDistance - 1.0; + gl_Position.z = log2(max(1e-6, 1.0 + gl_Position.w)) * czm_log2FarDistance - 1.0; gl_Position.z *= gl_Position.w; #endif } diff --git a/Source/Shaders/Builtin/Functions/windowToEyeCoordinates.glsl b/Source/Shaders/Builtin/Functions/windowToEyeCoordinates.glsl index 91bd5e3164f6..c95f4f04739e 100644 --- a/Source/Shaders/Builtin/Functions/windowToEyeCoordinates.glsl +++ b/Source/Shaders/Builtin/Functions/windowToEyeCoordinates.glsl @@ -53,3 +53,40 @@ vec4 czm_windowToEyeCoordinates(vec4 fragmentCoordinate) return q; } + +/** + * Transforms a position given as window x/y and a depth or a log depth from window to eye coordinates. + * This function produces more accurate results for window positions with log depth than + * conventionally unpacking the log depth using czm_reverseLogDepth and using the standard version + * of czm_windowToEyeCoordinates. + * + * @name czm_windowToEyeCoordinates + * @glslFunction + * + * @param {vec2} fragmentCoordinateXY The XY position in window coordinates to transform. + * @param {float} depthOrLogDepth A depth or log depth for the fragment. + * + * @see czm_modelToWindowCoordinates + * @see czm_eyeToWindowCoordinates + * @see czm_inverseProjection + * @see czm_viewport + * @see czm_viewportTransformation + * + * @returns {vec4} The transformed position in eye coordinates. + */ +vec4 czm_windowToEyeCoordinates(vec2 fragmentCoordinateXY, float depthOrLogDepth) +{ + // See reverseLogDepth.glsl. This is separate to re-use the pow. +#ifdef LOG_DEPTH + float near = czm_currentFrustum.x; + float far = czm_currentFrustum.y; + float unscaledDepth = pow(2.0, depthOrLogDepth * czm_log2FarPlusOne) - 1.0; + vec4 windowCoord = vec4(fragmentCoordinateXY, far * (1.0 - near / unscaledDepth) / (far - near), 1.0); + vec4 eyeCoordinate = czm_windowToEyeCoordinates(windowCoord); + eyeCoordinate.w = 1.0 / unscaledDepth;\n // Better precision +#else + vec4 windowCoord = vec4(fragmentCoordinateXY, depthOrLogDepth, 1.0); + vec4 eyeCoordinate = czm_windowToEyeCoordinates(windowCoord); +#endif + return eyeCoordinate; +} diff --git a/Source/Shaders/Builtin/Functions/writeLogDepth.glsl b/Source/Shaders/Builtin/Functions/writeLogDepth.glsl index 49d01bd2e72f..b9511ef4b8e7 100644 --- a/Source/Shaders/Builtin/Functions/writeLogDepth.glsl +++ b/Source/Shaders/Builtin/Functions/writeLogDepth.glsl @@ -19,9 +19,9 @@ varying float v_logZ; void czm_writeLogDepth(float logZ) { #if defined(GL_EXT_frag_depth) && defined(LOG_DEPTH) && !defined(DISABLE_LOG_DEPTH_FRAGMENT_WRITE) - float halfLogFarDistance = czm_logFarDistance * 0.5; + float halfLogFarDistance = czm_log2FarDistance * 0.5; float depth = log2(logZ); - if (depth < log2(czm_currentFrustum.x)) { + if (depth < czm_log2NearDistance) { discard; } gl_FragDepthEXT = depth * halfLogFarDistance; diff --git a/Source/Shaders/Materials/BumpMapMaterial.glsl b/Source/Shaders/Materials/BumpMapMaterial.glsl index 350f6a7c3e8f..2316a17fa045 100644 --- a/Source/Shaders/Materials/BumpMapMaterial.glsl +++ b/Source/Shaders/Materials/BumpMapMaterial.glsl @@ -7,23 +7,23 @@ czm_material czm_getMaterial(czm_materialInput materialInput) czm_material material = czm_getDefaultMaterial(materialInput); vec2 st = materialInput.st; - + vec2 centerPixel = fract(repeat * st); float centerBump = texture2D(image, centerPixel).channel; - + float imageWidth = float(imageDimensions.x); vec2 rightPixel = fract(repeat * (st + vec2(1.0 / imageWidth, 0.0))); float rightBump = texture2D(image, rightPixel).channel; - + float imageHeight = float(imageDimensions.y); vec2 leftPixel = fract(repeat * (st + vec2(0.0, 1.0 / imageHeight))); float topBump = texture2D(image, leftPixel).channel; - + vec3 normalTangentSpace = normalize(vec3(centerBump - rightBump, centerBump - topBump, clamp(1.0 - strength, 0.1, 1.0))); vec3 normalEC = materialInput.tangentToEyeMatrix * normalTangentSpace; - + material.normal = normalEC; material.diffuse = vec3(0.01); - + return material; } diff --git a/Source/Shaders/Materials/CheckerboardMaterial.glsl b/Source/Shaders/Materials/CheckerboardMaterial.glsl index 56d1fdb230aa..5c7a566c3132 100644 --- a/Source/Shaders/Materials/CheckerboardMaterial.glsl +++ b/Source/Shaders/Materials/CheckerboardMaterial.glsl @@ -7,22 +7,22 @@ czm_material czm_getMaterial(czm_materialInput materialInput) czm_material material = czm_getDefaultMaterial(materialInput); vec2 st = materialInput.st; - + // From Stefan Gustavson's Procedural Textures in GLSL in OpenGL Insights float b = mod(floor(repeat.s * st.s) + floor(repeat.t * st.t), 2.0); // 0.0 or 1.0 - + // Find the distance from the closest separator (region between two colors) float scaledWidth = fract(repeat.s * st.s); scaledWidth = abs(scaledWidth - floor(scaledWidth + 0.5)); float scaledHeight = fract(repeat.t * st.t); scaledHeight = abs(scaledHeight - floor(scaledHeight + 0.5)); float value = min(scaledWidth, scaledHeight); - + vec4 currentColor = mix(lightColor, darkColor, b); vec4 color = czm_antialias(lightColor, darkColor, currentColor, value, 0.03); - + material.diffuse = color.rgb; material.alpha = color.a; - + return material; } diff --git a/Source/Shaders/ShadowVolumeAppearanceFS.glsl b/Source/Shaders/ShadowVolumeAppearanceFS.glsl new file mode 100644 index 000000000000..5fb3f7c4f049 --- /dev/null +++ b/Source/Shaders/ShadowVolumeAppearanceFS.glsl @@ -0,0 +1,149 @@ +#ifdef GL_EXT_frag_depth +#extension GL_EXT_frag_depth : enable +#endif + +#ifdef TEXTURE_COORDINATES +#ifdef SPHERICAL +varying vec4 v_sphericalExtents; +#else // SPHERICAL +varying vec2 v_inversePlaneExtents; +varying vec4 v_westPlane; +varying vec4 v_southPlane; +#endif // SPHERICAL +varying vec2 v_uvMin; +varying vec3 v_uMaxAndInverseDistance; +varying vec3 v_vMaxAndInverseDistance; +#endif // TEXTURE_COORDINATES + +#ifdef PER_INSTANCE_COLOR +varying vec4 v_color; +#endif + +#ifdef NORMAL_EC +vec3 getEyeCoordinate3FromWindowCoordinate(vec2 fragCoord, float logDepthOrDepth) { + vec4 eyeCoordinate = czm_windowToEyeCoordinates(fragCoord, logDepthOrDepth); + return eyeCoordinate.xyz / eyeCoordinate.w; +} + +vec3 vectorFromOffset(vec4 eyeCoordinate, vec2 positiveOffset) { + vec2 glFragCoordXY = gl_FragCoord.xy; + // Sample depths at both offset and negative offset + float upOrRightLogDepth = czm_unpackDepth(texture2D(czm_globeDepthTexture, (glFragCoordXY + positiveOffset) / czm_viewport.zw)); + float downOrLeftLogDepth = czm_unpackDepth(texture2D(czm_globeDepthTexture, (glFragCoordXY - positiveOffset) / czm_viewport.zw)); + // Explicitly evaluate both paths + // Necessary for multifrustum and for edges of the screen + bvec2 upOrRightInBounds = lessThan(glFragCoordXY + positiveOffset, czm_viewport.zw); + float useUpOrRight = float(upOrRightLogDepth > 0.0 && upOrRightInBounds.x && upOrRightInBounds.y); + float useDownOrLeft = float(useUpOrRight == 0.0); + vec3 upOrRightEC = getEyeCoordinate3FromWindowCoordinate(glFragCoordXY + positiveOffset, upOrRightLogDepth); + vec3 downOrLeftEC = getEyeCoordinate3FromWindowCoordinate(glFragCoordXY - positiveOffset, downOrLeftLogDepth); + return (upOrRightEC - (eyeCoordinate.xyz / eyeCoordinate.w)) * useUpOrRight + ((eyeCoordinate.xyz / eyeCoordinate.w) - downOrLeftEC) * useDownOrLeft; +} +#endif // NORMAL_EC + +void main(void) +{ +#ifdef REQUIRES_EC + float logDepthOrDepth = czm_unpackDepth(texture2D(czm_globeDepthTexture, gl_FragCoord.xy / czm_viewport.zw)); + vec4 eyeCoordinate = czm_windowToEyeCoordinates(gl_FragCoord.xy, logDepthOrDepth); +#endif + +#ifdef REQUIRES_WC + vec4 worldCoordinate4 = czm_inverseView * eyeCoordinate; + vec3 worldCoordinate = worldCoordinate4.xyz / worldCoordinate4.w; +#endif + +#ifdef TEXTURE_COORDINATES + vec2 uv; +#ifdef SPHERICAL + // Treat world coords as a sphere normal for spherical coordinates + vec2 sphericalLatLong = czm_approximateSphericalCoordinates(worldCoordinate); + uv.x = (sphericalLatLong.y - v_sphericalExtents.y) * v_sphericalExtents.w; + uv.y = (sphericalLatLong.x - v_sphericalExtents.x) * v_sphericalExtents.z; +#else // SPHERICAL + // Unpack planes and transform to eye space + uv.x = czm_planeDistance(v_westPlane, eyeCoordinate.xyz / eyeCoordinate.w) * v_inversePlaneExtents.x; + uv.y = czm_planeDistance(v_southPlane, eyeCoordinate.xyz / eyeCoordinate.w) * v_inversePlaneExtents.y; +#endif // SPHERICAL +#endif // TEXTURE_COORDINATES + +#ifdef PICK +#ifdef CULL_FRAGMENTS + if (0.0 <= uv.x && uv.x <= 1.0 && 0.0 <= uv.y && uv.y <= 1.0) { + gl_FragColor.a = 1.0; // 0.0 alpha leads to discard from ShaderSource.createPickFragmentShaderSource + czm_writeDepthClampedToFarPlane(); + } +#else // CULL_FRAGMENTS + gl_FragColor.a = 1.0; +#endif // CULL_FRAGMENTS +#else // PICK + +#ifdef CULL_FRAGMENTS + if (uv.x <= 0.0 || 1.0 <= uv.x || uv.y <= 0.0 || 1.0 <= uv.y) { + discard; + } +#endif + +#ifdef NORMAL_EC + // Compute normal by sampling adjacent pixels in 2x2 block in screen space + vec3 downUp = vectorFromOffset(eyeCoordinate, vec2(0.0, 1.0)); + vec3 leftRight = vectorFromOffset(eyeCoordinate, vec2(1.0, 0.0)); + vec3 normalEC = normalize(cross(leftRight, downUp)); +#endif + + +#ifdef PER_INSTANCE_COLOR + +#ifdef FLAT + gl_FragColor = v_color; +#else // FLAT + czm_materialInput materialInput; + materialInput.normalEC = normalEC; + materialInput.positionToEyeEC = -eyeCoordinate.xyz; + czm_material material = czm_getDefaultMaterial(materialInput); + material.diffuse = v_color.rgb; + material.alpha = v_color.a; + + gl_FragColor = czm_phong(normalize(-eyeCoordinate.xyz), material); +#endif // FLAT + +#else // PER_INSTANCE_COLOR + + // Material support. + // USES_ is distinct from REQUIRES_, because some things are dependencies of each other or + // dependencies for culling but might not actually be used by the material. + + czm_materialInput materialInput; + +#ifdef USES_NORMAL_EC + materialInput.normalEC = normalEC; +#endif + +#ifdef USES_POSITION_TO_EYE_EC + materialInput.positionToEyeEC = -eyeCoordinate.xyz; +#endif + +#ifdef USES_TANGENT_TO_EYE + materialInput.tangentToEyeMatrix = czm_eastNorthUpToEyeCoordinates(worldCoordinate, normalEC); +#endif + +#ifdef USES_ST + // Remap texture coordinates from computed (approximately aligned with cartographic space) to the desired + // texture coordinate system, which typically forms a tight oriented bounding box around the geometry. + // Shader is provided a set of reference points for remapping. + materialInput.st.x = czm_lineDistance(v_uvMin, v_uMaxAndInverseDistance.xy, uv) * v_uMaxAndInverseDistance.z; + materialInput.st.y = czm_lineDistance(v_uvMin, v_vMaxAndInverseDistance.xy, uv) * v_vMaxAndInverseDistance.z; +#endif + + czm_material material = czm_getMaterial(materialInput); + +#ifdef FLAT + gl_FragColor = vec4(material.diffuse + material.emission, material.alpha); +#else // FLAT + gl_FragColor = czm_phong(normalize(-eyeCoordinate.xyz), material); +#endif // FLAT + +#endif // PER_INSTANCE_COLOR + czm_writeDepthClampedToFarPlane(); +#endif // PICK +} diff --git a/Source/Shaders/ShadowVolumeAppearanceVS.glsl b/Source/Shaders/ShadowVolumeAppearanceVS.glsl new file mode 100644 index 000000000000..d15f79cd25dc --- /dev/null +++ b/Source/Shaders/ShadowVolumeAppearanceVS.glsl @@ -0,0 +1,82 @@ +attribute vec3 position3DHigh; +attribute vec3 position3DLow; +attribute float batchId; + +#ifdef EXTRUDED_GEOMETRY +attribute vec3 extrudeDirection; + +uniform float u_globeMinimumAltitude; +#endif // EXTRUDED_GEOMETRY + +#ifdef PER_INSTANCE_COLOR +varying vec4 v_color; +#endif // PER_INSTANCE_COLOR + +#ifdef TEXTURE_COORDINATES +#ifdef SPHERICAL +varying vec4 v_sphericalExtents; +#else // SPHERICAL +varying vec2 v_inversePlaneExtents; +varying vec4 v_westPlane; +varying vec4 v_southPlane; +#endif // SPHERICAL +varying vec2 v_uvMin; +varying vec3 v_uMaxAndInverseDistance; +varying vec3 v_vMaxAndInverseDistance; +#endif // TEXTURE_COORDINATES + +void main() +{ + vec4 position = czm_computePosition(); + +#ifdef EXTRUDED_GEOMETRY + float delta = min(u_globeMinimumAltitude, czm_geometricToleranceOverMeter * length(position.xyz)); + delta *= czm_sceneMode == czm_sceneMode3D ? 1.0 : 0.0; + + //extrudeDirection is zero for the top layer + position = position + vec4(extrudeDirection * delta, 0.0); +#endif + +#ifdef TEXTURE_COORDINATES +#ifdef SPHERICAL + v_sphericalExtents = czm_batchTable_sphericalExtents(batchId); +#else // SPHERICAL +#ifdef COLUMBUS_VIEW_2D + vec4 planes2D_high = czm_batchTable_planes2D_HIGH(batchId); + vec4 planes2D_low = czm_batchTable_planes2D_LOW(batchId); + vec3 southWestCorner = (czm_modelViewRelativeToEye * czm_translateRelativeToEye(vec3(0.0, planes2D_high.xy), vec3(0.0, planes2D_low.xy))).xyz; + vec3 northWestCorner = (czm_modelViewRelativeToEye * czm_translateRelativeToEye(vec3(0.0, planes2D_high.x, planes2D_high.z), vec3(0.0, planes2D_low.x, planes2D_low.z))).xyz; + vec3 southEastCorner = (czm_modelViewRelativeToEye * czm_translateRelativeToEye(vec3(0.0, planes2D_high.w, planes2D_high.y), vec3(0.0, planes2D_low.w, planes2D_low.y))).xyz; +#else // COLUMBUS_VIEW_2D + // 3D case has smaller "plane extents," so planes encoded as a 64 bit position and 2 vec3s for distances/direction + vec3 southWestCorner = (czm_modelViewRelativeToEye * czm_translateRelativeToEye(czm_batchTable_southWest_HIGH(batchId), czm_batchTable_southWest_LOW(batchId))).xyz; + vec3 northWestCorner = czm_normal * czm_batchTable_northward(batchId) + southWestCorner; + vec3 southEastCorner = czm_normal * czm_batchTable_eastward(batchId) + southWestCorner; +#endif // COLUMBUS_VIEW_2D + + vec3 eastWard = southEastCorner - southWestCorner; + float eastExtent = length(eastWard); + eastWard /= eastExtent; + + vec3 northWard = northWestCorner - southWestCorner; + float northExtent = length(northWard); + northWard /= northExtent; + + v_westPlane = vec4(eastWard, -dot(eastWard, southWestCorner)); + v_southPlane = vec4(northWard, -dot(northWard, southWestCorner)); + v_inversePlaneExtents = vec2(1.0 / eastExtent, 1.0 / northExtent); +#endif // SPHERICAL + vec4 uvMinAndExtents = czm_batchTable_uvMinAndExtents(batchId); + vec4 uMaxVmax = czm_batchTable_uMaxVmax(batchId); + + v_uMaxAndInverseDistance = vec3(uMaxVmax.xy, uvMinAndExtents.z); + v_vMaxAndInverseDistance = vec3(uMaxVmax.zw, uvMinAndExtents.w); + v_uvMin = uvMinAndExtents.xy; +#endif // TEXTURE_COORDINATES + +#ifdef PER_INSTANCE_COLOR + v_color = czm_batchTable_color(batchId); +#endif + + gl_Position = czm_depthClampFarPlane(czm_modelViewProjectionRelativeToEye * position); +} diff --git a/Source/Shaders/ShadowVolumeFS.glsl b/Source/Shaders/ShadowVolumeFS.glsl index 51e951b07b99..981c6f56fddf 100644 --- a/Source/Shaders/ShadowVolumeFS.glsl +++ b/Source/Shaders/ShadowVolumeFS.glsl @@ -4,8 +4,6 @@ #ifdef VECTOR_TILE uniform vec4 u_highlightColor; -#else -varying vec4 v_color; #endif void main(void) @@ -13,7 +11,7 @@ void main(void) #ifdef VECTOR_TILE gl_FragColor = u_highlightColor; #else - gl_FragColor = v_color; + gl_FragColor = vec4(1.0); #endif czm_writeDepthClampedToFarPlane(); } diff --git a/Source/Shaders/ShadowVolumeVS.glsl b/Source/Shaders/ShadowVolumeVS.glsl deleted file mode 100644 index 9d3c550b3cd5..000000000000 --- a/Source/Shaders/ShadowVolumeVS.glsl +++ /dev/null @@ -1,41 +0,0 @@ -#ifdef VECTOR_TILE -attribute vec3 position; -attribute float a_batchId; - -uniform mat4 u_modifiedModelViewProjection; -#else -attribute vec3 position3DHigh; -attribute vec3 position3DLow; -attribute vec4 color; -attribute float batchId; -#endif - -#ifdef EXTRUDED_GEOMETRY -attribute vec3 extrudeDirection; - -uniform float u_globeMinimumAltitude; -#endif - -#ifndef VECTOR_TILE -varying vec4 v_color; -#endif - -void main() -{ -#ifdef VECTOR_TILE - gl_Position = czm_depthClampFarPlane(u_modifiedModelViewProjection * vec4(position, 1.0)); -#else - v_color = color; - - vec4 position = czm_computePosition(); - -#ifdef EXTRUDED_GEOMETRY - float delta = min(u_globeMinimumAltitude, czm_geometricToleranceOverMeter * length(position.xyz)); - delta *= czm_sceneMode == czm_sceneMode3D ? 1.0 : 0.0; - - //extrudeDirection is zero for the top layer - position = position + vec4(extrudeDirection * delta, 0.0); -#endif - gl_Position = czm_depthClampFarPlane(czm_modelViewProjectionRelativeToEye * position); -#endif -} diff --git a/Source/Shaders/VectorTileVS.glsl b/Source/Shaders/VectorTileVS.glsl new file mode 100644 index 000000000000..6c7a5278e8a3 --- /dev/null +++ b/Source/Shaders/VectorTileVS.glsl @@ -0,0 +1,9 @@ +attribute vec3 position; +attribute float a_batchId; + +uniform mat4 u_modifiedModelViewProjection; + +void main() +{ + gl_Position = czm_depthClampFarPlane(u_modifiedModelViewProjection * vec4(position, 1.0)); +} diff --git a/Source/ThirdParty/quickselect.js b/Source/ThirdParty/quickselect.js new file mode 100644 index 000000000000..a474563cc4fd --- /dev/null +++ b/Source/ThirdParty/quickselect.js @@ -0,0 +1,59 @@ +define([], function() { +'use strict'; + +function quickselect(arr, k, left, right, compare) { + quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare); +}; + +function quickselectStep(arr, k, left, right, compare) { + + while (right > left) { + if (right - left > 600) { + var n = right - left + 1; + var m = k - left + 1; + var z = Math.log(n); + var s = 0.5 * Math.exp(2 * z / 3); + var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1); + var newLeft = Math.max(left, Math.floor(k - m * s / n + sd)); + var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd)); + quickselectStep(arr, k, newLeft, newRight, compare); + } + + var t = arr[k]; + var i = left; + var j = right; + + swap(arr, left, k); + if (compare(arr[right], t) > 0) swap(arr, left, right); + + while (i < j) { + swap(arr, i, j); + i++; + j--; + while (compare(arr[i], t) < 0) i++; + while (compare(arr[j], t) > 0) j--; + } + + if (compare(arr[left], t) === 0) swap(arr, left, j); + else { + j++; + swap(arr, j, right); + } + + if (j <= k) left = j + 1; + if (k <= j) right = j - 1; + } +} + +function swap(arr, i, j) { + var tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; +} + +function defaultCompare(a, b) { + return a < b ? -1 : a > b ? 1 : 0; +} + +return quickselect; +}); diff --git a/Source/ThirdParty/rbush.js b/Source/ThirdParty/rbush.js new file mode 100644 index 000000000000..bbb7b6fbc61b --- /dev/null +++ b/Source/ThirdParty/rbush.js @@ -0,0 +1,561 @@ +define(['./quickselect'], function(quickselect) { +'use strict'; + +function rbush(maxEntries, format) { + if (!(this instanceof rbush)) return new rbush(maxEntries, format); + + // max entries in a node is 9 by default; min node fill is 40% for best performance + this._maxEntries = Math.max(4, maxEntries || 9); + this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4)); + + if (format) { + this._initFormat(format); + } + + this.clear(); +} + +rbush.prototype = { + + all: function () { + return this._all(this.data, []); + }, + + search: function (bbox) { + + var node = this.data, + result = [], + toBBox = this.toBBox; + + if (!intersects(bbox, node)) return result; + + var nodesToSearch = [], + i, len, child, childBBox; + + while (node) { + for (i = 0, len = node.children.length; i < len; i++) { + + child = node.children[i]; + childBBox = node.leaf ? toBBox(child) : child; + + if (intersects(bbox, childBBox)) { + if (node.leaf) result.push(child); + else if (contains(bbox, childBBox)) this._all(child, result); + else nodesToSearch.push(child); + } + } + node = nodesToSearch.pop(); + } + + return result; + }, + + collides: function (bbox) { + + var node = this.data, + toBBox = this.toBBox; + + if (!intersects(bbox, node)) return false; + + var nodesToSearch = [], + i, len, child, childBBox; + + while (node) { + for (i = 0, len = node.children.length; i < len; i++) { + + child = node.children[i]; + childBBox = node.leaf ? toBBox(child) : child; + + if (intersects(bbox, childBBox)) { + if (node.leaf || contains(bbox, childBBox)) return true; + nodesToSearch.push(child); + } + } + node = nodesToSearch.pop(); + } + + return false; + }, + + load: function (data) { + if (!(data && data.length)) return this; + + if (data.length < this._minEntries) { + for (var i = 0, len = data.length; i < len; i++) { + this.insert(data[i]); + } + return this; + } + + // recursively build the tree with the given data from scratch using OMT algorithm + var node = this._build(data.slice(), 0, data.length - 1, 0); + + if (!this.data.children.length) { + // save as is if tree is empty + this.data = node; + + } else if (this.data.height === node.height) { + // split root if trees have the same height + this._splitRoot(this.data, node); + + } else { + if (this.data.height < node.height) { + // swap trees if inserted one is bigger + var tmpNode = this.data; + this.data = node; + node = tmpNode; + } + + // insert the small tree into the large tree at appropriate level + this._insert(node, this.data.height - node.height - 1, true); + } + + return this; + }, + + insert: function (item) { + if (item) this._insert(item, this.data.height - 1); + return this; + }, + + clear: function () { + this.data = createNode([]); + return this; + }, + + remove: function (item, equalsFn) { + if (!item) return this; + + var node = this.data, + bbox = this.toBBox(item), + path = [], + indexes = [], + i, parent, index, goingUp; + + // depth-first iterative tree traversal + while (node || path.length) { + + if (!node) { // go up + node = path.pop(); + parent = path[path.length - 1]; + i = indexes.pop(); + goingUp = true; + } + + if (node.leaf) { // check current node + index = findItem(item, node.children, equalsFn); + + if (index !== -1) { + // item found, remove the item and condense tree upwards + node.children.splice(index, 1); + path.push(node); + this._condense(path); + return this; + } + } + + if (!goingUp && !node.leaf && contains(node, bbox)) { // go down + path.push(node); + indexes.push(i); + i = 0; + parent = node; + node = node.children[0]; + + } else if (parent) { // go right + i++; + node = parent.children[i]; + goingUp = false; + + } else node = null; // nothing found + } + + return this; + }, + + toBBox: function (item) { return item; }, + + compareMinX: compareNodeMinX, + compareMinY: compareNodeMinY, + + toJSON: function () { return this.data; }, + + fromJSON: function (data) { + this.data = data; + return this; + }, + + _all: function (node, result) { + var nodesToSearch = []; + while (node) { + if (node.leaf) result.push.apply(result, node.children); + else nodesToSearch.push.apply(nodesToSearch, node.children); + + node = nodesToSearch.pop(); + } + return result; + }, + + _build: function (items, left, right, height) { + + var N = right - left + 1, + M = this._maxEntries, + node; + + if (N <= M) { + // reached leaf level; return leaf + node = createNode(items.slice(left, right + 1)); + calcBBox(node, this.toBBox); + return node; + } + + if (!height) { + // target height of the bulk-loaded tree + height = Math.ceil(Math.log(N) / Math.log(M)); + + // target number of root entries to maximize storage utilization + M = Math.ceil(N / Math.pow(M, height - 1)); + } + + node = createNode([]); + node.leaf = false; + node.height = height; + + // split the items into M mostly square tiles + + var N2 = Math.ceil(N / M), + N1 = N2 * Math.ceil(Math.sqrt(M)), + i, j, right2, right3; + + multiSelect(items, left, right, N1, this.compareMinX); + + for (i = left; i <= right; i += N1) { + + right2 = Math.min(i + N1 - 1, right); + + multiSelect(items, i, right2, N2, this.compareMinY); + + for (j = i; j <= right2; j += N2) { + + right3 = Math.min(j + N2 - 1, right2); + + // pack each entry recursively + node.children.push(this._build(items, j, right3, height - 1)); + } + } + + calcBBox(node, this.toBBox); + + return node; + }, + + _chooseSubtree: function (bbox, node, level, path) { + + var i, len, child, targetNode, area, enlargement, minArea, minEnlargement; + + while (true) { + path.push(node); + + if (node.leaf || path.length - 1 === level) break; + + minArea = minEnlargement = Infinity; + + for (i = 0, len = node.children.length; i < len; i++) { + child = node.children[i]; + area = bboxArea(child); + enlargement = enlargedArea(bbox, child) - area; + + // choose entry with the least area enlargement + if (enlargement < minEnlargement) { + minEnlargement = enlargement; + minArea = area < minArea ? area : minArea; + targetNode = child; + + } else if (enlargement === minEnlargement) { + // otherwise choose one with the smallest area + if (area < minArea) { + minArea = area; + targetNode = child; + } + } + } + + node = targetNode || node.children[0]; + } + + return node; + }, + + _insert: function (item, level, isNode) { + + var toBBox = this.toBBox, + bbox = isNode ? item : toBBox(item), + insertPath = []; + + // find the best node for accommodating the item, saving all nodes along the path too + var node = this._chooseSubtree(bbox, this.data, level, insertPath); + + // put the item into the node + node.children.push(item); + extend(node, bbox); + + // split on node overflow; propagate upwards if necessary + while (level >= 0) { + if (insertPath[level].children.length > this._maxEntries) { + this._split(insertPath, level); + level--; + } else break; + } + + // adjust bboxes along the insertion path + this._adjustParentBBoxes(bbox, insertPath, level); + }, + + // split overflowed node into two + _split: function (insertPath, level) { + + var node = insertPath[level], + M = node.children.length, + m = this._minEntries; + + this._chooseSplitAxis(node, m, M); + + var splitIndex = this._chooseSplitIndex(node, m, M); + + var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex)); + newNode.height = node.height; + newNode.leaf = node.leaf; + + calcBBox(node, this.toBBox); + calcBBox(newNode, this.toBBox); + + if (level) insertPath[level - 1].children.push(newNode); + else this._splitRoot(node, newNode); + }, + + _splitRoot: function (node, newNode) { + // split root node + this.data = createNode([node, newNode]); + this.data.height = node.height + 1; + this.data.leaf = false; + calcBBox(this.data, this.toBBox); + }, + + _chooseSplitIndex: function (node, m, M) { + + var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index; + + minOverlap = minArea = Infinity; + + for (i = m; i <= M - m; i++) { + bbox1 = distBBox(node, 0, i, this.toBBox); + bbox2 = distBBox(node, i, M, this.toBBox); + + overlap = intersectionArea(bbox1, bbox2); + area = bboxArea(bbox1) + bboxArea(bbox2); + + // choose distribution with minimum overlap + if (overlap < minOverlap) { + minOverlap = overlap; + index = i; + + minArea = area < minArea ? area : minArea; + + } else if (overlap === minOverlap) { + // otherwise choose distribution with minimum area + if (area < minArea) { + minArea = area; + index = i; + } + } + } + + return index; + }, + + // sorts node children by the best axis for split + _chooseSplitAxis: function (node, m, M) { + + var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX, + compareMinY = node.leaf ? this.compareMinY : compareNodeMinY, + xMargin = this._allDistMargin(node, m, M, compareMinX), + yMargin = this._allDistMargin(node, m, M, compareMinY); + + // if total distributions margin value is minimal for x, sort by minX, + // otherwise it's already sorted by minY + if (xMargin < yMargin) node.children.sort(compareMinX); + }, + + // total margin of all possible split distributions where each node is at least m full + _allDistMargin: function (node, m, M, compare) { + + node.children.sort(compare); + + var toBBox = this.toBBox, + leftBBox = distBBox(node, 0, m, toBBox), + rightBBox = distBBox(node, M - m, M, toBBox), + margin = bboxMargin(leftBBox) + bboxMargin(rightBBox), + i, child; + + for (i = m; i < M - m; i++) { + child = node.children[i]; + extend(leftBBox, node.leaf ? toBBox(child) : child); + margin += bboxMargin(leftBBox); + } + + for (i = M - m - 1; i >= m; i--) { + child = node.children[i]; + extend(rightBBox, node.leaf ? toBBox(child) : child); + margin += bboxMargin(rightBBox); + } + + return margin; + }, + + _adjustParentBBoxes: function (bbox, path, level) { + // adjust bboxes along the given tree path + for (var i = level; i >= 0; i--) { + extend(path[i], bbox); + } + }, + + _condense: function (path) { + // go through the path, removing empty nodes and updating bboxes + for (var i = path.length - 1, siblings; i >= 0; i--) { + if (path[i].children.length === 0) { + if (i > 0) { + siblings = path[i - 1].children; + siblings.splice(siblings.indexOf(path[i]), 1); + + } else this.clear(); + + } else calcBBox(path[i], this.toBBox); + } + }, + + _initFormat: function (format) { + // data format (minX, minY, maxX, maxY accessors) + + // uses eval-type function compilation instead of just accepting a toBBox function + // because the algorithms are very sensitive to sorting functions performance, + // so they should be dead simple and without inner calls + + var compareArr = ['return a', ' - b', ';']; + + this.compareMinX = new Function('a', 'b', compareArr.join(format[0])); + this.compareMinY = new Function('a', 'b', compareArr.join(format[1])); + + this.toBBox = new Function('a', + 'return {minX: a' + format[0] + + ', minY: a' + format[1] + + ', maxX: a' + format[2] + + ', maxY: a' + format[3] + '};'); + } +}; + +function findItem(item, items, equalsFn) { + if (!equalsFn) return items.indexOf(item); + + for (var i = 0; i < items.length; i++) { + if (equalsFn(item, items[i])) return i; + } + return -1; +} + +// calculate node's bbox from bboxes of its children +function calcBBox(node, toBBox) { + distBBox(node, 0, node.children.length, toBBox, node); +} + +// min bounding rectangle of node children from k to p-1 +function distBBox(node, k, p, toBBox, destNode) { + if (!destNode) destNode = createNode(null); + destNode.minX = Infinity; + destNode.minY = Infinity; + destNode.maxX = -Infinity; + destNode.maxY = -Infinity; + + for (var i = k, child; i < p; i++) { + child = node.children[i]; + extend(destNode, node.leaf ? toBBox(child) : child); + } + + return destNode; +} + +function extend(a, b) { + a.minX = Math.min(a.minX, b.minX); + a.minY = Math.min(a.minY, b.minY); + a.maxX = Math.max(a.maxX, b.maxX); + a.maxY = Math.max(a.maxY, b.maxY); + return a; +} + +function compareNodeMinX(a, b) { return a.minX - b.minX; } +function compareNodeMinY(a, b) { return a.minY - b.minY; } + +function bboxArea(a) { return (a.maxX - a.minX) * (a.maxY - a.minY); } +function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); } + +function enlargedArea(a, b) { + return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) * + (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY)); +} + +function intersectionArea(a, b) { + var minX = Math.max(a.minX, b.minX), + minY = Math.max(a.minY, b.minY), + maxX = Math.min(a.maxX, b.maxX), + maxY = Math.min(a.maxY, b.maxY); + + return Math.max(0, maxX - minX) * + Math.max(0, maxY - minY); +} + +function contains(a, b) { + return a.minX <= b.minX && + a.minY <= b.minY && + b.maxX <= a.maxX && + b.maxY <= a.maxY; +} + +function intersects(a, b) { + return b.minX <= a.maxX && + b.minY <= a.maxY && + b.maxX >= a.minX && + b.maxY >= a.minY; +} + +function createNode(children) { + return { + children: children, + height: 1, + leaf: true, + minX: Infinity, + minY: Infinity, + maxX: -Infinity, + maxY: -Infinity + }; +} + +// sort an array so that items come in groups of n unsorted items, with groups sorted between each other; +// combines selection algorithm with binary divide & conquer approach + +function multiSelect(arr, left, right, n, compare) { + var stack = [left, right], + mid; + + while (stack.length) { + right = stack.pop(); + left = stack.pop(); + + if (right - left <= n) continue; + + mid = left + Math.ceil((right - left) / n / 2) * n; + quickselect(arr, mid, left, right, compare); + + stack.push(left, mid, mid, right); + } +} + +return rbush; +}); diff --git a/Specs/Core/CircleGeometrySpec.js b/Specs/Core/CircleGeometrySpec.js index a9a9c04ee138..1d247c4a7fd0 100644 --- a/Specs/Core/CircleGeometrySpec.js +++ b/Specs/Core/CircleGeometrySpec.js @@ -164,6 +164,25 @@ defineSuite([ expect(r.west).toEqual(-1.3196344953554853); }); + it('computing textureCoordinateRotationPoints property', function() { + var center = Cartesian3.fromDegrees(0, 0); + var ellipse = new CircleGeometry({ + center : center, + radius : 1000.0, + stRotation : CesiumMath.toRadians(90) + }); + + // 90 degree rotation means (0, 1) should be the new min and (1, 1) (0, 0) are extents + var textureCoordinateRotationPoints = ellipse.textureCoordinateRotationPoints; + expect(textureCoordinateRotationPoints.length).toEqual(6); + expect(textureCoordinateRotationPoints[0]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[1]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[2]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[3]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[4]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[5]).toEqualEpsilon(0, CesiumMath.EPSILON7); + }); + var center = Cartesian3.fromDegrees(0,0); var ellipsoid = Ellipsoid.WGS84; var packableInstance = new CircleGeometry({ diff --git a/Specs/Core/CorridorGeometrySpec.js b/Specs/Core/CorridorGeometrySpec.js index b338288504ad..dca5468bf5b2 100644 --- a/Specs/Core/CorridorGeometrySpec.js +++ b/Specs/Core/CorridorGeometrySpec.js @@ -281,6 +281,30 @@ defineSuite([ expect(CesiumMath.toDegrees(r.west)).toEqual(-67.6550047734171); }); + it('computing textureCoordinateRotationPoints property', function() { + var c = new CorridorGeometry({ + vertexFormat : VertexFormat.POSITION_ONLY, + positions : Cartesian3.fromDegreesArray([ + -67.655, 0.0, + -67.655, 15.0, + -67.655, 20.0 + ]), + cornerType: CornerType.MITERED, + width : 1, + granularity : Math.PI / 6.0 + }); + + // Corridors don't support geometry orientation or stRotation, so expect this to equal the original coordinate system. + var textureCoordinateRotationPoints = c.textureCoordinateRotationPoints; + expect(textureCoordinateRotationPoints.length).toEqual(6); + expect(textureCoordinateRotationPoints[0]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[1]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[2]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[3]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[4]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[5]).toEqualEpsilon(0, CesiumMath.EPSILON7); + }); + var positions = Cartesian3.fromDegreesArray([ 90.0, -30.0, 90.0, -31.0 diff --git a/Specs/Core/EllipseGeometrySpec.js b/Specs/Core/EllipseGeometrySpec.js index 57ef668ed30d..9f331f0d4841 100644 --- a/Specs/Core/EllipseGeometrySpec.js +++ b/Specs/Core/EllipseGeometrySpec.js @@ -264,6 +264,42 @@ defineSuite([ expect(r.west).toEqualEpsilon(-CesiumMath.PI, CesiumMath.EPSILON7); }); + it('computing textureCoordinateRotationPoints property', function() { + var center = Cartesian3.fromDegrees(0, 0); + var ellipse = new EllipseGeometry({ + center : center, + semiMajorAxis : 2000.0, + semiMinorAxis : 1000.0, + stRotation : CesiumMath.toRadians(90) + }); + + // 90 degree rotation means (0, 1) should be the new min and (1, 1) (0, 0) are extents + var textureCoordinateRotationPoints = ellipse.textureCoordinateRotationPoints; + expect(textureCoordinateRotationPoints.length).toEqual(6); + expect(textureCoordinateRotationPoints[0]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[1]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[2]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[3]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[4]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[5]).toEqualEpsilon(0, CesiumMath.EPSILON7); + + ellipse = new EllipseGeometry({ + center : center, + semiMajorAxis : 2000.0, + semiMinorAxis : 1000.0, + stRotation : CesiumMath.toRadians(0) + }); + + textureCoordinateRotationPoints = ellipse.textureCoordinateRotationPoints; + expect(textureCoordinateRotationPoints.length).toEqual(6); + expect(textureCoordinateRotationPoints[0]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[1]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[2]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[3]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[4]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[5]).toEqualEpsilon(0, CesiumMath.EPSILON7); + }); + var center = Cartesian3.fromDegrees(0,0); var ellipsoid = Ellipsoid.WGS84; var packableInstance = new EllipseGeometry({ diff --git a/Specs/Core/GeometrySpec.js b/Specs/Core/GeometrySpec.js index 692b85e036f8..08447225441e 100644 --- a/Specs/Core/GeometrySpec.js +++ b/Specs/Core/GeometrySpec.js @@ -2,18 +2,24 @@ defineSuite([ 'Core/Geometry', 'Core/BoundingSphere', 'Core/Cartesian3', + 'Core/Math', 'Core/ComponentDatatype', + 'Core/Ellipsoid', 'Core/GeometryAttribute', 'Core/GeometryType', - 'Core/PrimitiveType' + 'Core/PrimitiveType', + 'Core/Rectangle' ], function( Geometry, BoundingSphere, Cartesian3, + CesiumMath, ComponentDatatype, + Ellipsoid, GeometryAttribute, GeometryType, - PrimitiveType) { + PrimitiveType, + Rectangle) { 'use strict'; it('constructor', function() { @@ -129,4 +135,21 @@ defineSuite([ }).toThrowDeveloperError(); }); + it('computes textureCoordinateRotationPoints for collections of points', function() { + var positions = Cartesian3.fromDegreesArrayHeights([ + -10.0, -10.0, 0, + -10.0, 10.0, 0, + 10.0, -10.0, 0, + 10.0, 10.0, 0 + ]); + var boundingRectangle = Rectangle.fromCartesianArray(positions); + var textureCoordinateRotationPoints = Geometry._textureCoordinateRotationPoints(positions, 0.0, Ellipsoid.WGS84, boundingRectangle); + expect(textureCoordinateRotationPoints.length).toEqual(6); + expect(textureCoordinateRotationPoints[0]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[1]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[2]).toEqualEpsilon(0, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[3]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[4]).toEqualEpsilon(1, CesiumMath.EPSILON7); + expect(textureCoordinateRotationPoints[5]).toEqualEpsilon(0, CesiumMath.EPSILON7); + }); }); diff --git a/Specs/Core/MathSpec.js b/Specs/Core/MathSpec.js index 4026ea42c290..60876d81eef5 100644 --- a/Specs/Core/MathSpec.js +++ b/Specs/Core/MathSpec.js @@ -450,4 +450,22 @@ defineSuite([ expect(CesiumMath.cbrt(1.0)).toEqual(1.0); expect(CesiumMath.cbrt()).toEqual(NaN); }); + + it('fastApproximateAtan', function() { + expect(CesiumMath.fastApproximateAtan(0.0)).toEqualEpsilon(0.0, CesiumMath.EPSILON3); + expect(CesiumMath.fastApproximateAtan(1.0)).toEqualEpsilon(CesiumMath.PI_OVER_FOUR, CesiumMath.EPSILON3); + expect(CesiumMath.fastApproximateAtan(-1.0)).toEqualEpsilon(-CesiumMath.PI_OVER_FOUR, CesiumMath.EPSILON3); + }); + + it('fastApproximateAtan2', function() { + expect(CesiumMath.fastApproximateAtan2(1.0, 0.0)).toEqualEpsilon(0.0, CesiumMath.EPSILON3); + expect(CesiumMath.fastApproximateAtan2(1.0, 1.0)).toEqualEpsilon(CesiumMath.PI_OVER_FOUR, CesiumMath.EPSILON3); + expect(CesiumMath.fastApproximateAtan2(-1.0, 1.0)).toEqualEpsilon(CesiumMath.PI_OVER_FOUR + CesiumMath.PI_OVER_TWO, CesiumMath.EPSILON3); + }); + + it('fastApproximateAtan2 throws if both arguments are zero', function() { + expect(function() { + CesiumMath.fastApproximateAtan2(0, 0); + }).toThrowDeveloperError(); + }); }); diff --git a/Specs/Core/PolygonGeometrySpec.js b/Specs/Core/PolygonGeometrySpec.js index 645370d55fcf..90d519197bb6 100644 --- a/Specs/Core/PolygonGeometrySpec.js +++ b/Specs/Core/PolygonGeometrySpec.js @@ -307,7 +307,7 @@ defineSuite([ -110.0, 35.0 ]); for (i = 0; i < p.length; i++) { - expect(p[i]).toEqualEpsilon(pExpected[i], CesiumMath.EPSILON10); + expect(p[i]).toEqualEpsilon(pExpected[i], CesiumMath.EPSILON7); } var h1Expected = Cartesian3.fromDegreesArray([ @@ -317,7 +317,7 @@ defineSuite([ -122.0, 39.0 ]); for (i = 0; i < h1.length; i++) { - expect(h1[i]).toEqualEpsilon(h1Expected[i], CesiumMath.EPSILON10); + expect(h1[i]).toEqualEpsilon(h1Expected[i], CesiumMath.EPSILON7); } var h2Expected = Cartesian3.fromDegreesArray([ @@ -327,7 +327,7 @@ defineSuite([ -114.0, 36.5 ]); for (i = 0; i