diff --git a/Apps/Sandcastle/gallery/development/Ground Polyline Material.html b/Apps/Sandcastle/gallery/development/Ground Polyline Material.html
new file mode 100644
index 000000000000..ee5c8a489f78
--- /dev/null
+++ b/Apps/Sandcastle/gallery/development/Ground Polyline Material.html
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+ Cesium Demo
+
+
+
+
+
+
+
+Loading...
+
+
+
+
diff --git a/Apps/Sandcastle/gallery/development/Ground Polyline Material.jpg b/Apps/Sandcastle/gallery/development/Ground Polyline Material.jpg
new file mode 100644
index 000000000000..f83e97e704f5
Binary files /dev/null and b/Apps/Sandcastle/gallery/development/Ground Polyline Material.jpg differ
diff --git a/Apps/Sandcastle/gallery/development/Polylines On Terrain.html b/Apps/Sandcastle/gallery/development/Polylines On Terrain.html
new file mode 100644
index 000000000000..714121e15252
--- /dev/null
+++ b/Apps/Sandcastle/gallery/development/Polylines On Terrain.html
@@ -0,0 +1,300 @@
+
+
+
+
+
+
+
+
+ Cesium Demo
+
+
+
+
+
+
+
+Loading...
+
+
+
+
diff --git a/Apps/Sandcastle/gallery/development/Polylines On Terrain.jpg b/Apps/Sandcastle/gallery/development/Polylines On Terrain.jpg
new file mode 100644
index 000000000000..0b87ebcf318e
Binary files /dev/null and b/Apps/Sandcastle/gallery/development/Polylines On Terrain.jpg differ
diff --git a/CHANGES.md b/CHANGES.md
index bb1826a35bc4..bcb2c494b48c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,7 @@ Change Log
##### Additions :tada:
* `PostProcessStage` has a `selectedFeatures` property which is an array of primitives used for selectively applying a post-process stage. In the fragment shader, use the function `bool czm_selected(vec2 textureCoordinates` to determine whether or not the stage should be applied at that fragment.
* The black-and-white and silhouette stages have per-feature support.
+* Added `GroundPolylinePrimitive` and `GroundPolylineGeometry` for rendering polylines on terrain via the `Primitive` API. [#6615](https://github.com/AnalyticalGraphicsInc/cesium/pull/6615)
##### Fixes :wrench:
* Fixed a bug causing crashes with custom vertex attributes on `Geometry` crossing the IDL. Attributes will be barycentrically interpolated. [#6644](https://github.com/AnalyticalGraphicsInc/cesium/pull/6644)
diff --git a/Source/Core/ApproximateTerrainHeights.js b/Source/Core/ApproximateTerrainHeights.js
index e295abe57a39..fb489798b013 100644
--- a/Source/Core/ApproximateTerrainHeights.js
+++ b/Source/Core/ApproximateTerrainHeights.js
@@ -49,13 +49,14 @@ define([
* Initializes the minimum and maximum terrain heights
* @return {Promise}
*/
- ApproximateTerrainHeights.initialize = function() {
+ ApproximateTerrainHeights.initialize = function(url) {
var initPromise = ApproximateTerrainHeights._initPromise;
if (defined(initPromise)) {
return initPromise;
}
- ApproximateTerrainHeights._initPromise = Resource.fetchJson(buildModuleUrl('Assets/approximateTerrainHeights.json')).then(function(json) {
+ url = defaultValue(url, 'Assets/approximateTerrainHeights.json');
+ ApproximateTerrainHeights._initPromise = Resource.fetchJson(buildModuleUrl(url)).then(function(json) {
ApproximateTerrainHeights._terrainHeights = json;
});
diff --git a/Source/Core/GeometryInstanceAttribute.js b/Source/Core/GeometryInstanceAttribute.js
index 069d27cd0792..7b595f964bcb 100644
--- a/Source/Core/GeometryInstanceAttribute.js
+++ b/Source/Core/GeometryInstanceAttribute.js
@@ -15,10 +15,10 @@ define([
* @constructor
*
* @param {Object} options Object with the following properties:
- * @param {ComponentDatatype} [options.componentDatatype] The datatype of each component in the attribute, e.g., individual elements in values.
- * @param {Number} [options.componentsPerAttribute] A number between 1 and 4 that defines the number of components in an attributes.
+ * @param {ComponentDatatype} options.componentDatatype The datatype of each component in the attribute, e.g., individual elements in values.
+ * @param {Number} options.componentsPerAttribute A number between 1 and 4 that defines the number of components in an attributes.
* @param {Boolean} [options.normalize=false] When true
and componentDatatype
is an integer format, indicate that the components should be mapped to the range [0, 1] (unsigned) or [-1, 1] (signed) when they are accessed as floating-point for rendering.
- * @param {Number[]} [options.value] The value for the attribute.
+ * @param {Number[]} options.value The value for the attribute.
*
* @exception {DeveloperError} options.componentsPerAttribute must be between 1 and 4.
*
diff --git a/Source/Core/GroundPolylineGeometry.js b/Source/Core/GroundPolylineGeometry.js
new file mode 100644
index 000000000000..de9f2485b6e2
--- /dev/null
+++ b/Source/Core/GroundPolylineGeometry.js
@@ -0,0 +1,1128 @@
+define([
+ './ApproximateTerrainHeights',
+ './BoundingSphere',
+ './Cartesian3',
+ './Cartographic',
+ './Check',
+ './ComponentDatatype',
+ './DeveloperError',
+ './Math',
+ './defaultValue',
+ './defined',
+ './defineProperties',
+ './Ellipsoid',
+ './EllipsoidGeodesic',
+ './EncodedCartesian3',
+ './GeographicProjection',
+ './Geometry',
+ './GeometryAttribute',
+ './IntersectionTests',
+ './Matrix3',
+ './Plane',
+ './Quaternion',
+ './Rectangle',
+ './WebMercatorProjection'
+ ], function(
+ ApproximateTerrainHeights,
+ BoundingSphere,
+ Cartesian3,
+ Cartographic,
+ Check,
+ ComponentDatatype,
+ DeveloperError,
+ CesiumMath,
+ defaultValue,
+ defined,
+ defineProperties,
+ Ellipsoid,
+ EllipsoidGeodesic,
+ EncodedCartesian3,
+ GeographicProjection,
+ Geometry,
+ GeometryAttribute,
+ IntersectionTests,
+ Matrix3,
+ Plane,
+ Quaternion,
+ Rectangle,
+ WebMercatorProjection) {
+ 'use strict';
+
+ var PROJECTIONS = [GeographicProjection, WebMercatorProjection];
+ var PROJECTION_COUNT = PROJECTIONS.length;
+
+ var MITER_BREAK_SMALL = Math.cos(CesiumMath.toRadians(30.0));
+ var MITER_BREAK_LARGE = Math.cos(CesiumMath.toRadians(150.0));
+
+ // Initial heights for constructing the wall.
+ // Keeping WALL_INITIAL_MIN_HEIGHT near the ellipsoid surface helps
+ // prevent precision problems with planes in the shader.
+ // Putting the start point of a plane at ApproximateTerrainHeights._defaultMinTerrainHeight,
+ // which is a highly conservative bound, usually puts the plane origin several thousands
+ // of meters away from the actual terrain, causing floating point problems when checking
+ // fragments on terrain against the plane.
+ // Ellipsoid height is generally much closer.
+ // The initial max height is arbitrary.
+ // Both heights are corrected using ApproximateTerrainHeights for computing the actual volume geometry.
+ var WALL_INITIAL_MIN_HEIGHT = 0.0;
+ var WALL_INITIAL_MAX_HEIGHT = 1000.0;
+
+ /**
+ * A description of a polyline on terrain. Only to be used with {@link GroundPolylinePrimitive}.
+ *
+ * @alias GroundPolylineGeometry
+ * @constructor
+ *
+ * @param {Object} options Options with the following properties:
+ * @param {Cartesian3[]} options.positions An array of {@link Cartesian3} defining the polyline's points. Heights above the ellipsoid will be ignored.
+ * @param {Number} [options.width=1.0] The screen space width in pixels.
+ * @param {Number} [options.granularity=9999.0] The distance interval in meters used for interpolating options.points. Defaults to 9999.0 meters. Zero indicates no interpolation.
+ * @param {Boolean} [options.loop=false] Whether during geometry creation a line segment will be added between the last and first line positions to make this Polyline a loop.
+ *
+ * @exception {DeveloperError} At least two positions are required.
+ *
+ * @see GroundPolylinePrimitive
+ *
+ * @example
+ * var positions = Cesium.Cartesian3.fromDegreesArray([
+ * -112.1340164450331, 36.05494287836128,
+ * -112.08821010582645, 36.097804071380715,
+ * -112.13296079730024, 36.168769146801104
+ * ]);
+ *
+ * var geometry = new Cesium.GroundPolylineGeometry({
+ * positions : positions
+ * });
+ */
+ function GroundPolylineGeometry(options) {
+ options = defaultValue(options, defaultValue.EMPTY_OBJECT);
+ var positions = options.positions;
+
+ //>>includeStart('debug', pragmas.debug);
+ if ((!defined(positions)) || (positions.length < 2)) {
+ throw new DeveloperError('At least two positions are required.');
+ }
+ //>>includeEnd('debug');
+
+ /**
+ * The screen space width in pixels.
+ * @type {Number}
+ */
+ this.width = defaultValue(options.width, 1.0); // Doesn't get packed, not necessary for computing geometry.
+
+ this._positions = positions;
+
+ /**
+ * The distance interval used for interpolating options.points. Zero indicates no interpolation.
+ * Default of 9999.0 allows centimeter accuracy with 32 bit floating point.
+ * @type {Boolean}
+ * @default 9999.0
+ */
+ this.granularity = defaultValue(options.granularity, 9999.0);
+
+ /**
+ * Whether during geometry creation a line segment will be added between the last and first line positions to make this Polyline a loop.
+ * If the geometry has two positions this parameter will be ignored.
+ * @type {Boolean}
+ * @default false
+ */
+ this.loop = defaultValue(options.loop, false);
+
+ this._ellipsoid = Ellipsoid.WGS84;
+
+ // MapProjections can't be packed, so store the index to a known MapProjection.
+ this._projectionIndex = 0;
+ this._workerName = 'createGroundPolylineGeometry';
+
+ // Used by GroundPolylinePrimitive to signal worker that scenemode is 3D only.
+ this._scene3DOnly = false;
+ }
+
+ defineProperties(GroundPolylineGeometry.prototype, {
+ /**
+ * The number of elements used to pack the object into an array.
+ * @memberof GroundPolylineGeometry.prototype
+ * @type {Number}
+ * @readonly
+ * @private
+ */
+ packedLength: {
+ get: function() {
+ return 1.0 + this._positions.length * 3 + 1.0 + 1.0 + Ellipsoid.packedLength + 1.0 + 1.0;
+ }
+ }
+ });
+
+ /**
+ * Set the GroundPolylineGeometry's projection and ellipsoid.
+ * Used by GroundPolylinePrimitive to signal scene information to the geometry for generating 2D attributes.
+ *
+ * @param {GroundPolylineGeometry} groundPolylineGeometry GroundPolylinGeometry describing a polyline on terrain.
+ * @param {Projection} mapProjection A MapProjection used for projecting cartographic coordinates to 2D.
+ * @private
+ */
+ GroundPolylineGeometry.setProjectionAndEllipsoid = function(groundPolylineGeometry, mapProjection) {
+ var projectionIndex = 0;
+ for (var i = 0; i < PROJECTION_COUNT; i++) {
+ if (mapProjection instanceof PROJECTIONS[i]) {
+ projectionIndex = i;
+ break;
+ }
+ }
+
+ groundPolylineGeometry._projectionIndex = projectionIndex;
+ groundPolylineGeometry._ellipsoid = mapProjection.ellipsoid;
+ };
+
+ var cart3Scratch1 = new Cartesian3();
+ var cart3Scratch2 = new Cartesian3();
+ var cart3Scratch3 = new Cartesian3();
+ function computeRightNormal(start, end, maxHeight, ellipsoid, result) {
+ var startBottom = getPosition(ellipsoid, start, 0.0, cart3Scratch1);
+ var startTop = getPosition(ellipsoid, start, maxHeight, cart3Scratch2);
+ var endBottom = getPosition(ellipsoid, end, 0.0, cart3Scratch3);
+
+ var up = direction(startTop, startBottom, cart3Scratch2);
+ var forward = direction(endBottom, startBottom, cart3Scratch3);
+
+ Cartesian3.cross(forward, up, result);
+ return Cartesian3.normalize(result, result);
+ }
+
+ var interpolatedCartographicScratch = new Cartographic();
+ var interpolatedBottomScratch = new Cartesian3();
+ var interpolatedTopScratch = new Cartesian3();
+ var interpolatedNormalScratch = new Cartesian3();
+ function interpolateSegment(start, end, minHeight, maxHeight, granularity, ellipsoid, normalsArray, bottomPositionsArray, topPositionsArray, cartographicsArray) {
+ if (granularity === 0.0) {
+ return;
+ }
+ var ellipsoidGeodesic = new EllipsoidGeodesic(start, end, ellipsoid);
+ var surfaceDistance = ellipsoidGeodesic.surfaceDistance;
+ if (surfaceDistance < granularity) {
+ return;
+ }
+
+ // Compute rightwards normal applicable at all interpolated points
+ var interpolatedNormal = computeRightNormal(start, end, maxHeight, ellipsoid, interpolatedNormalScratch);
+
+ var segments = Math.ceil(surfaceDistance / granularity);
+ var interpointDistance = surfaceDistance / segments;
+ var distanceFromStart = interpointDistance;
+ var pointsToAdd = segments - 1;
+ var packIndex = normalsArray.length;
+ for (var i = 0; i < pointsToAdd; i++) {
+ var interpolatedCartographic = ellipsoidGeodesic.interpolateUsingSurfaceDistance(distanceFromStart, interpolatedCartographicScratch);
+ var interpolatedBottom = getPosition(ellipsoid, interpolatedCartographic, minHeight, interpolatedBottomScratch);
+ var interpolatedTop = getPosition(ellipsoid, interpolatedCartographic, maxHeight, interpolatedTopScratch);
+
+ Cartesian3.pack(interpolatedNormal, normalsArray, packIndex);
+ Cartesian3.pack(interpolatedBottom, bottomPositionsArray, packIndex);
+ Cartesian3.pack(interpolatedTop, topPositionsArray, packIndex);
+ cartographicsArray.push(interpolatedCartographic.latitude);
+ cartographicsArray.push(interpolatedCartographic.longitude);
+
+ packIndex += 3;
+ distanceFromStart += interpointDistance;
+ }
+ }
+
+ var heightlessCartographicScratch = new Cartographic();
+ function getPosition(ellipsoid, cartographic, height, result) {
+ Cartographic.clone(cartographic, heightlessCartographicScratch);
+ heightlessCartographicScratch.height = height;
+ return Cartographic.toCartesian(heightlessCartographicScratch, ellipsoid, result);
+ }
+
+ /**
+ * Stores the provided instance into the provided array.
+ *
+ * @param {PolygonGeometry} value The value to pack.
+ * @param {Number[]} array The array to pack into.
+ * @param {Number} [startingIndex=0] The index into the array at which to start packing the elements.
+ *
+ * @returns {Number[]} The array that was packed into
+ */
+ GroundPolylineGeometry.pack = function(value, array, startingIndex) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.typeOf.object('value', value);
+ Check.defined('array', array);
+ //>>includeEnd('debug');
+
+ var index = defaultValue(startingIndex, 0);
+
+ // Pack position length, then all positions
+ var positions = value._positions;
+ var positionsLength = positions.length;
+
+ array[index++] = positionsLength;
+
+ for (var i = 0; i < positionsLength; ++i) {
+ var cartesian = positions[i];
+ Cartesian3.pack(cartesian, array, index);
+ index += 3;
+ }
+
+ array[index++] = value.granularity;
+ array[index++] = value.loop ? 1.0 : 0.0;
+
+ Ellipsoid.pack(value._ellipsoid, array, index);
+ index += Ellipsoid.packedLength;
+
+ array[index++] = value._projectionIndex;
+ array[index++] = value._scene3DOnly ? 1.0 : 0.0;
+
+ return array;
+ };
+
+ /**
+ * Retrieves an instance from a packed array.
+ *
+ * @param {Number[]} array The packed array.
+ * @param {Number} [startingIndex=0] The starting index of the element to be unpacked.
+ * @param {PolygonGeometry} [result] The object into which to store the result.
+ */
+ GroundPolylineGeometry.unpack = function(array, startingIndex, result) {
+ //>>includeStart('debug', pragmas.debug);
+ Check.defined('array', array);
+ //>>includeEnd('debug');
+
+ var index = defaultValue(startingIndex, 0);
+ var positionsLength = array[index++];
+ var positions = new Array(positionsLength);
+
+ for (var i = 0; i < positionsLength; i++) {
+ positions[i] = Cartesian3.unpack(array, index);
+ index += 3;
+ }
+
+ var granularity = array[index++];
+ var loop = array[index++] === 1.0;
+
+ var ellipsoid = Ellipsoid.unpack(array, index);
+ index += Ellipsoid.packedLength;
+
+ var projectionIndex = array[index++];
+ var scene3DOnly = (array[index++] === 1.0);
+
+ if (!defined(result)) {
+ var geometry = new GroundPolylineGeometry({
+ positions : positions,
+ granularity : granularity,
+ loop : loop,
+ ellipsoid : ellipsoid,
+ projection : new PROJECTIONS[projectionIndex](ellipsoid)
+ });
+ geometry._scene3DOnly = scene3DOnly;
+ return geometry;
+ }
+
+ result._positions = positions;
+ result.granularity = granularity;
+ result.loop = loop;
+ result._ellipsoid = ellipsoid;
+ result._projectionIndex = projectionIndex;
+ result._scene3DOnly = scene3DOnly;
+
+ return result;
+ };
+
+ function direction(target, origin, result) {
+ Cartesian3.subtract(target, origin, result);
+ Cartesian3.normalize(result, result);
+ return result;
+ }
+
+ // Inputs are cartesians
+ var toPreviousScratch = new Cartesian3();
+ var toNextScratch = new Cartesian3();
+ var forwardScratch = new Cartesian3();
+ var coplanarNormalScratch = new Cartesian3();
+ var coplanarPlaneScratch = new Plane(Cartesian3.UNIT_X, 0.0);
+ var vertexUpScratch = new Cartesian3();
+ var cosine90 = 0.0;
+ function computeVertexMiterNormal(previousBottom, vertexBottom, vertexTop, nextBottom, result) {
+ var up = direction(vertexTop, vertexBottom, vertexUpScratch);
+ var toPrevious = direction(previousBottom, vertexBottom, toPreviousScratch);
+ var toNext = direction(nextBottom, vertexBottom, toNextScratch);
+
+ // Check if points are coplanar in a right-side-pointing plane that contains "up."
+ // This is roughly equivalent to the points being colinear in cartographic space.
+ var coplanarNormal = Cartesian3.cross(up, toPrevious, coplanarNormalScratch);
+ coplanarNormal = Cartesian3.normalize(coplanarNormal, coplanarNormal);
+ var coplanarPlane = Plane.fromPointNormal(vertexBottom, coplanarNormal, coplanarPlaneScratch);
+ var nextBottomDistance = Plane.getPointDistance(coplanarPlane, nextBottom);
+ if (CesiumMath.equalsEpsilon(nextBottomDistance, 0.0, CesiumMath.EPSILON7)) {
+ // If the points are coplanar, point the normal in the direction of the plane
+ Cartesian3.clone(coplanarNormal, result);
+ return result;
+ }
+
+ // Average directions to previous and to next
+ result = Cartesian3.add(toNext, toPrevious, result);
+ result = Cartesian3.normalize(result, result);
+
+ // Rotate this direction to be orthogonal to up
+ var forward = Cartesian3.cross(up, result, forwardScratch);
+ Cartesian3.normalize(forward, forward);
+ Cartesian3.cross(forward, up, result);
+ Cartesian3.normalize(result, result);
+
+ // Flip the normal if it isn't pointing roughly bound right (aka if forward is pointing more "backwards")
+ if (Cartesian3.dot(toNext, forward) < cosine90) {
+ result = Cartesian3.negate(result, result);
+ }
+
+ return result;
+ }
+
+ var XZ_PLANE = Plane.fromPointNormal(Cartesian3.ZERO, Cartesian3.UNIT_Y);
+
+ var previousBottomScratch = new Cartesian3();
+ var vertexBottomScratch = new Cartesian3();
+ var vertexTopScratch = new Cartesian3();
+ var nextBottomScratch = new Cartesian3();
+ var vertexNormalScratch = new Cartesian3();
+ var intersectionScratch = new Cartesian3();
+ /**
+ * Computes shadow volumes for the ground polyline, consisting of its vertices, indices, and a bounding sphere.
+ * Vertices are "fat," packing all the data needed in each volume to describe a line on terrain.
+ * Should not be called independent of {@link GroundPolylinePrimitive}.
+ *
+ * @param {GroundPolylineGeometry} groundPolylineGeometry
+ * @private
+ */
+ GroundPolylineGeometry.createGeometry = function(groundPolylineGeometry) {
+ var compute2dAttributes = !groundPolylineGeometry._scene3DOnly;
+ var loop = groundPolylineGeometry.loop;
+ var ellipsoid = groundPolylineGeometry._ellipsoid;
+ var granularity = groundPolylineGeometry.granularity;
+ var projection = new PROJECTIONS[groundPolylineGeometry._projectionIndex](ellipsoid);
+
+ var minHeight = WALL_INITIAL_MIN_HEIGHT;
+ var maxHeight = WALL_INITIAL_MAX_HEIGHT;
+
+ var index;
+ var i;
+
+ var positions = groundPolylineGeometry._positions;
+ var positionsLength = positions.length;
+
+ if (positionsLength === 2) {
+ loop = false;
+ }
+
+ // Split positions across the IDL and the Prime Meridian as well.
+ // Split across prime meridian because very large geometries crossing the Prime Meridian but not the IDL
+ // may get split by the plane of IDL + Prime Meridian.
+ var p0;
+ var p1;
+ var intersection;
+ var splitPositions = [positions[0]];
+ for (i = 0; i < positionsLength - 1; i++) {
+ p0 = positions[i];
+ p1 = positions[i + 1];
+ intersection = IntersectionTests.lineSegmentPlane(p0, p1, XZ_PLANE, intersectionScratch);
+ if (defined(intersection) &&
+ !Cartesian3.equalsEpsilon(intersection, p0, CesiumMath.EPSILON7) &&
+ !Cartesian3.equalsEpsilon(intersection, p1, CesiumMath.EPSILON7)) {
+ splitPositions.push(Cartesian3.clone(intersection));
+ }
+ splitPositions.push(p1);
+ }
+ // Check if loop also crosses IDL/Prime Meridian
+ if (loop) {
+ p0 = positions[positionsLength - 1];
+ p1 = positions[0];
+ intersection = IntersectionTests.lineSegmentPlane(p0, p1, XZ_PLANE, intersectionScratch);
+ if (defined(intersection) &&
+ !Cartesian3.equalsEpsilon(intersection, p0, CesiumMath.EPSILON7) &&
+ !Cartesian3.equalsEpsilon(intersection, p1, CesiumMath.EPSILON7)) {
+ splitPositions.push(Cartesian3.clone(intersection));
+ }
+ }
+ var cartographicsLength = splitPositions.length;
+
+ // Squash all cartesians to cartographic coordinates
+ var cartographics = new Array(cartographicsLength);
+ for (i = 0; i < cartographicsLength; i++) {
+ var cartographic = Cartographic.fromCartesian(splitPositions[i], ellipsoid);
+ cartographic.height = 0.0;
+ cartographics[i] = cartographic;
+ }
+
+ /**** Build heap-side arrays for positions, interpolated cartographics, and normals from which to compute vertices ****/
+ // We build a "wall" and then decompose it into separately connected component "volumes" because we need a lot
+ // of information about the wall. Also, this simplifies interpolation.
+ // Convention: "next" and "end" are locally forward to each segment of the wall,
+ // and we are computing normals pointing towards the local right side of the vertices in each segment.
+ var cartographicsArray = [];
+ var normalsArray = [];
+ var bottomPositionsArray = [];
+ var topPositionsArray = [];
+
+ var previousBottom = previousBottomScratch;
+ var vertexBottom = vertexBottomScratch;
+ var vertexTop = vertexTopScratch;
+ var nextBottom = nextBottomScratch;
+ var vertexNormal = vertexNormalScratch;
+
+ // First point - either loop or attach a "perpendicular" normal
+ var startCartographic = cartographics[0];
+ var nextCartographic = cartographics[1];
+
+ var prestartCartographic = cartographics[cartographicsLength - 1];
+ previousBottom = getPosition(ellipsoid, prestartCartographic, minHeight, previousBottom);
+ nextBottom = getPosition(ellipsoid, nextCartographic, minHeight, nextBottom);
+ vertexBottom = getPosition(ellipsoid, startCartographic, minHeight, vertexBottom);
+ vertexTop = getPosition(ellipsoid, startCartographic, maxHeight, vertexTop);
+
+ if (loop) {
+ vertexNormal = computeVertexMiterNormal(previousBottom, vertexBottom, vertexTop, nextBottom, vertexNormal);
+ } else {
+ vertexNormal = computeRightNormal(startCartographic, nextCartographic, maxHeight, ellipsoid, vertexNormal);
+ }
+
+ Cartesian3.pack(vertexNormal, normalsArray, 0);
+ Cartesian3.pack(vertexBottom, bottomPositionsArray, 0);
+ Cartesian3.pack(vertexTop, topPositionsArray, 0);
+ cartographicsArray.push(startCartographic.latitude);
+ cartographicsArray.push(startCartographic.longitude);
+
+ // Interpolate between start and start + 1
+ interpolateSegment(startCartographic, nextCartographic, minHeight, maxHeight, granularity, ellipsoid, normalsArray, bottomPositionsArray, topPositionsArray, cartographicsArray);
+
+ // All inbetween points
+ for (i = 1; i < cartographicsLength - 1; ++i) {
+ previousBottom = Cartesian3.clone(vertexBottom, previousBottom);
+ vertexBottom = Cartesian3.clone(nextBottom, vertexBottom);
+ var vertexCartographic = cartographics[i];
+ getPosition(ellipsoid, vertexCartographic, maxHeight, vertexTop);
+ getPosition(ellipsoid, cartographics[i + 1], minHeight, nextBottom);
+
+ computeVertexMiterNormal(previousBottom, vertexBottom, vertexTop, nextBottom, vertexNormal);
+
+ index = normalsArray.length;
+ Cartesian3.pack(vertexNormal, normalsArray, index);
+ Cartesian3.pack(vertexBottom, bottomPositionsArray, index);
+ Cartesian3.pack(vertexTop, topPositionsArray, index);
+ cartographicsArray.push(vertexCartographic.latitude);
+ cartographicsArray.push(vertexCartographic.longitude);
+
+ interpolateSegment(cartographics[i], cartographics[i + 1], minHeight, maxHeight, granularity, ellipsoid, normalsArray, bottomPositionsArray, topPositionsArray, cartographicsArray);
+ }
+
+ // Last point - either loop or attach a normal "perpendicular" to the wall.
+ var endCartographic = cartographics[cartographicsLength - 1];
+ var preEndCartographic = cartographics[cartographicsLength - 2];
+
+ vertexBottom = getPosition(ellipsoid, endCartographic, minHeight, vertexBottom);
+ vertexTop = getPosition(ellipsoid, endCartographic, maxHeight, vertexTop);
+
+ if (loop) {
+ var postEndCartographic = cartographics[0];
+ previousBottom = getPosition(ellipsoid, preEndCartographic, minHeight, previousBottom);
+ nextBottom = getPosition(ellipsoid, postEndCartographic, minHeight, nextBottom);
+
+ vertexNormal = computeVertexMiterNormal(previousBottom, vertexBottom, vertexTop, nextBottom, vertexNormal);
+ } else {
+ vertexNormal = computeRightNormal(preEndCartographic, endCartographic, maxHeight, ellipsoid, vertexNormal);
+ }
+
+ index = normalsArray.length;
+ Cartesian3.pack(vertexNormal, normalsArray, index);
+ Cartesian3.pack(vertexBottom, bottomPositionsArray, index);
+ Cartesian3.pack(vertexTop, topPositionsArray, index);
+ cartographicsArray.push(endCartographic.latitude);
+ cartographicsArray.push(endCartographic.longitude);
+
+ if (loop) {
+ interpolateSegment(endCartographic, startCartographic, minHeight, maxHeight, granularity, ellipsoid, normalsArray, bottomPositionsArray, topPositionsArray, cartographicsArray);
+ index = normalsArray.length;
+ // Copy the first vertex
+ for (i = 0; i < 3; ++i) {
+ normalsArray[index + i] = normalsArray[i];
+ bottomPositionsArray[index + i] = bottomPositionsArray[i];
+ topPositionsArray[index + i] = topPositionsArray[i];
+ }
+ cartographicsArray.push(startCartographic.latitude);
+ cartographicsArray.push(startCartographic.longitude);
+ }
+
+ return generateGeometryAttributes(loop, projection, bottomPositionsArray, topPositionsArray, normalsArray, cartographicsArray, compute2dAttributes);
+ };
+
+ // If the end normal angle is too steep compared to the direction of the line segment,
+ // "break" the miter by rotating the normal 90 degrees around the "up" direction at the point
+ // For ultra precision we would want to project into a plane, but in practice this is sufficient.
+ var lineDirectionScratch = new Cartesian3();
+ var matrix3Scratch = new Matrix3();
+ var quaternionScratch = new Quaternion();
+ function breakMiter(endGeometryNormal, startBottom, endBottom, endTop) {
+ var lineDirection = direction(endBottom, startBottom, lineDirectionScratch);
+
+ var dot = Cartesian3.dot(lineDirection, endGeometryNormal);
+ if (dot > MITER_BREAK_SMALL || dot < MITER_BREAK_LARGE) {
+ var vertexUp = direction(endTop, endBottom, vertexUpScratch);
+ var angle = dot < MITER_BREAK_LARGE ? CesiumMath.PI_OVER_TWO : -CesiumMath.PI_OVER_TWO;
+ var quaternion = Quaternion.fromAxisAngle(vertexUp, angle, quaternionScratch);
+ var rotationMatrix = Matrix3.fromQuaternion(quaternion, matrix3Scratch);
+ Matrix3.multiplyByVector(rotationMatrix, endGeometryNormal, endGeometryNormal);
+ return true;
+ }
+ return false;
+ }
+
+ var endPosCartographicScratch = new Cartographic();
+ var normalStartpointScratch = new Cartesian3();
+ var normalEndpointScratch = new Cartesian3();
+ function projectNormal(projection, cartographic, normal, projectedPosition, result) {
+ var position = Cartographic.toCartesian(cartographic, projection._ellipsoid, normalStartpointScratch);
+ var normalEndpoint = Cartesian3.add(position, normal, normalEndpointScratch);
+ var flipNormal = false;
+
+ var ellipsoid = projection._ellipsoid;
+ var normalEndpointCartographic = ellipsoid.cartesianToCartographic(normalEndpoint, endPosCartographicScratch);
+ // If normal crosses the IDL, go the other way and flip the result.
+ // In practice this almost never happens because the cartographic start
+ // and end points of each segment are "nudged" to be on the same side
+ // of the IDL and slightly away from the IDL.
+ if (Math.abs(cartographic.longitude - normalEndpointCartographic.longitude) > CesiumMath.PI_OVER_TWO) {
+ flipNormal = true;
+ normalEndpoint = Cartesian3.subtract(position, normal, normalEndpointScratch);
+ normalEndpointCartographic = ellipsoid.cartesianToCartographic(normalEndpoint, endPosCartographicScratch);
+ }
+
+ normalEndpointCartographic.height = 0.0;
+ var normalEndpointProjected = projection.project(normalEndpointCartographic, result);
+ result = Cartesian3.subtract(normalEndpointProjected, projectedPosition, result);
+ result.z = 0.0;
+ result = Cartesian3.normalize(result, result);
+ if (flipNormal) {
+ Cartesian3.negate(result, result);
+ }
+ return result;
+ }
+
+ var adjustHeightNormalScratch = new Cartesian3();
+ var adjustHeightOffsetScratch = new Cartesian3();
+ function adjustHeights(bottom, top, minHeight, maxHeight, adjustHeightBottom, adjustHeightTop) {
+ // bottom and top should be at WALL_INITIAL_MIN_HEIGHT and WALL_INITIAL_MAX_HEIGHT, respectively
+ var adjustHeightNormal = Cartesian3.subtract(top, bottom, adjustHeightNormalScratch);
+ Cartesian3.normalize(adjustHeightNormal, adjustHeightNormal);
+
+ var distanceForBottom = minHeight - WALL_INITIAL_MIN_HEIGHT;
+ var adjustHeightOffset = Cartesian3.multiplyByScalar(adjustHeightNormal, distanceForBottom, adjustHeightOffsetScratch);
+ Cartesian3.add(bottom, adjustHeightOffset, adjustHeightBottom);
+
+ var distanceForTop = maxHeight - WALL_INITIAL_MAX_HEIGHT;
+ adjustHeightOffset = Cartesian3.multiplyByScalar(adjustHeightNormal, distanceForTop, adjustHeightOffsetScratch);
+ Cartesian3.add(top, adjustHeightOffset, adjustHeightTop);
+ }
+
+ var nudgeDirectionScratch = new Cartesian3();
+ function nudgeXZ(start, end) {
+ var startToXZdistance = Plane.getPointDistance(XZ_PLANE, start);
+ var endToXZdistance = Plane.getPointDistance(XZ_PLANE, end);
+ var offset = nudgeDirectionScratch;
+ // Larger epsilon than what's used in GeometryPipeline, a centimeter in world space
+ if (CesiumMath.equalsEpsilon(startToXZdistance, 0.0, CesiumMath.EPSILON2)) {
+ offset = direction(end, start, offset);
+ Cartesian3.multiplyByScalar(offset, CesiumMath.EPSILON2, offset);
+ Cartesian3.add(start, offset, start);
+ } else if (CesiumMath.equalsEpsilon(endToXZdistance, 0.0, CesiumMath.EPSILON2)) {
+ offset = direction(start, end, offset);
+ Cartesian3.multiplyByScalar(offset, CesiumMath.EPSILON2, offset);
+ Cartesian3.add(end, offset, end);
+ }
+ }
+
+ // "Nudge" cartographic coordinates so start and end are on the same side of the IDL.
+ // Nudge amounts are tiny, basically just an IDL flip.
+ // Only used for 2D/CV.
+ function nudgeCartographic(start, end) {
+ var absStartLon = Math.abs(start.longitude);
+ var absEndLon = Math.abs(end.longitude);
+ if (CesiumMath.equalsEpsilon(absStartLon, CesiumMath.PI, CesiumMath.EPSILON11)) {
+ var endSign = Math.sign(end.longitude);
+ start.longitude = endSign * (absStartLon - CesiumMath.EPSILON11);
+ return 1;
+ } else if (CesiumMath.equalsEpsilon(absEndLon, CesiumMath.PI, CesiumMath.EPSILON11)) {
+ var startSign = Math.sign(start.longitude);
+ end.longitude = startSign * (absEndLon - CesiumMath.EPSILON11);
+ return 2;
+ }
+ return 0;
+ }
+
+ var startCartographicScratch = new Cartographic();
+ var endCartographicScratch = new Cartographic();
+
+ var segmentStartTopScratch = new Cartesian3();
+ var segmentEndTopScratch = new Cartesian3();
+ var segmentStartBottomScratch = new Cartesian3();
+ var segmentEndBottomScratch = new Cartesian3();
+ var segmentStartNormalScratch = new Cartesian3();
+ var segmentEndNormalScratch = new Cartesian3();
+
+ var getHeightCartographics = [startCartographicScratch, endCartographicScratch];
+ var getHeightRectangleScratch = new Rectangle();
+
+ var adjustHeightStartTopScratch = new Cartesian3();
+ var adjustHeightEndTopScratch = new Cartesian3();
+ var adjustHeightStartBottomScratch = new Cartesian3();
+ var adjustHeightEndBottomScratch = new Cartesian3();
+
+ var segmentStart2DScratch = new Cartesian3();
+ var segmentEnd2DScratch = new Cartesian3();
+ var segmentStartNormal2DScratch = new Cartesian3();
+ var segmentEndNormal2DScratch = new Cartesian3();
+
+ var offsetScratch = new Cartesian3();
+ var startUpScratch = new Cartesian3();
+ var endUpScratch = new Cartesian3();
+ var rightScratch = new Cartesian3();
+ var startPlaneNormalScratch = new Cartesian3();
+ var endPlaneNormalScratch = new Cartesian3();
+ var encodeScratch = new EncodedCartesian3();
+
+ var encodeScratch2D = new EncodedCartesian3();
+ var forwardOffset2DScratch = new Cartesian3();
+ var right2DScratch = new Cartesian3();
+
+ var normalNudgeScratch = new Cartesian3();
+
+ var scratchBoundingSpheres = [new BoundingSphere(), new BoundingSphere()];
+
+ // Winding order is reversed so each segment's volume is inside-out
+ var REFERENCE_INDICES = [
+ 0, 2, 1, 0, 3, 2, // right
+ 0, 7, 3, 0, 4, 7, // start
+ 0, 5, 4, 0, 1, 5, // bottom
+ 5, 7, 4, 5, 6, 7, // left
+ 5, 2, 6, 5, 1, 2, // end
+ 3, 6, 2, 3, 7, 6 // top
+ ];
+ var REFERENCE_INDICES_LENGTH = REFERENCE_INDICES.length;
+
+ // Decompose the "wall" into a series of shadow volumes.
+ // Each shadow volume's vertices encode a description of the line it contains,
+ // including mitering planes at the end points, a plane along the line itself,
+ // and attributes for computing length-wise texture coordinates.
+ function generateGeometryAttributes(loop, projection, bottomPositionsArray, topPositionsArray, normalsArray, cartographicsArray, compute2dAttributes) {
+ var i;
+ var index;
+ var ellipsoid = projection._ellipsoid;
+
+ // Each segment will have 8 vertices
+ var segmentCount = (bottomPositionsArray.length / 3) - 1;
+ var vertexCount = segmentCount * 8;
+ var arraySizeVec4 = vertexCount * 4;
+ var indexCount = segmentCount * 36;
+
+ var indices = vertexCount > 65535 ? new Uint32Array(indexCount) : new Uint16Array(indexCount);
+ var positionsArray = new Float64Array(vertexCount * 3);
+
+ var startHiAndForwardOffsetX = new Float32Array(arraySizeVec4);
+ var startLoAndForwardOffsetY = new Float32Array(arraySizeVec4);
+ var startNormalAndForwardOffsetZ = new Float32Array(arraySizeVec4);
+ var endNormalAndTextureCoordinateNormalizationX = new Float32Array(arraySizeVec4);
+ var rightNormalAndTextureCoordinateNormalizationY = new Float32Array(arraySizeVec4);
+
+ var startHiLo2D;
+ var offsetAndRight2D;
+ var startEndNormals2D;
+ var texcoordNormalization2D;
+
+ if (compute2dAttributes) {
+ startHiLo2D = new Float32Array(arraySizeVec4);
+ offsetAndRight2D = new Float32Array(arraySizeVec4);
+ startEndNormals2D = new Float32Array(arraySizeVec4);
+ texcoordNormalization2D = new Float32Array(vertexCount * 2);
+ }
+
+ /*** Compute total lengths for texture coordinate normalization ***/
+ // 2D
+ var cartographicsLength = cartographicsArray.length / 2;
+ var length2D = 0.0;
+
+ var startCartographic = startCartographicScratch;
+ startCartographic.height = 0.0;
+ var endCartographic = endCartographicScratch;
+ endCartographic.height = 0.0;
+
+ var segmentStartCartesian = segmentStartTopScratch;
+ var segmentEndCartesian = segmentEndTopScratch;
+
+ if (compute2dAttributes) {
+ index = 0;
+ for (i = 1; i < cartographicsLength; i++) {
+ // don't clone anything from previous segment b/c possible IDL touch
+ startCartographic.latitude = cartographicsArray[index];
+ startCartographic.longitude = cartographicsArray[index + 1];
+ endCartographic.latitude = cartographicsArray[index + 2];
+ endCartographic.longitude = cartographicsArray[index + 3];
+
+ segmentStartCartesian = projection.project(startCartographic, segmentStartCartesian);
+ segmentEndCartesian = projection.project(endCartographic, segmentEndCartesian);
+ length2D += Cartesian3.distance(segmentStartCartesian, segmentEndCartesian);
+ index += 2;
+ }
+ }
+
+ // 3D
+ var positionsLength = topPositionsArray.length / 3;
+ segmentEndCartesian = Cartesian3.unpack(topPositionsArray, 0, segmentEndCartesian);
+ var length3D = 0.0;
+
+ index = 3;
+ for (i = 1; i < positionsLength; i++) {
+ segmentStartCartesian = Cartesian3.clone(segmentEndCartesian, segmentStartCartesian);
+ segmentEndCartesian = Cartesian3.unpack(topPositionsArray, index, segmentEndCartesian);
+ length3D += Cartesian3.distance(segmentStartCartesian, segmentEndCartesian);
+ index += 3;
+ }
+
+ /*** Generate segments ***/
+ var j;
+ index = 3;
+ var cartographicsIndex = 0;
+ var vec2sWriteIndex = 0;
+ var vec3sWriteIndex = 0;
+ var vec4sWriteIndex = 0;
+ var miterBroken = false;
+
+ var endBottom = Cartesian3.unpack(bottomPositionsArray, 0, segmentEndBottomScratch);
+ var endTop = Cartesian3.unpack(topPositionsArray, 0, segmentEndTopScratch);
+ var endGeometryNormal = Cartesian3.unpack(normalsArray, 0, segmentEndNormalScratch);
+
+ if (loop) {
+ var preEndBottom = Cartesian3.unpack(bottomPositionsArray, bottomPositionsArray.length - 6, segmentStartBottomScratch);
+ if (breakMiter(endGeometryNormal, preEndBottom, endBottom, endTop)) {
+ // Miter broken as if for the last point in the loop, needs to be inverted for first point (clone of endBottom)
+ endGeometryNormal = Cartesian3.negate(endGeometryNormal, endGeometryNormal);
+ }
+ }
+
+ var lengthSoFar3D = 0.0;
+ var lengthSoFar2D = 0.0;
+
+ for (i = 0; i < segmentCount; i++) {
+ var startBottom = Cartesian3.clone(endBottom, segmentStartBottomScratch);
+ var startTop = Cartesian3.clone(endTop, segmentStartTopScratch);
+ var startGeometryNormal = Cartesian3.clone(endGeometryNormal, segmentStartNormalScratch);
+
+ if (miterBroken) {
+ startGeometryNormal = Cartesian3.negate(startGeometryNormal, startGeometryNormal);
+ }
+
+ endBottom = Cartesian3.unpack(bottomPositionsArray, index, segmentEndBottomScratch);
+ endTop = Cartesian3.unpack(topPositionsArray, index, segmentEndTopScratch);
+ endGeometryNormal = Cartesian3.unpack(normalsArray, index, segmentEndNormalScratch);
+
+ miterBroken = breakMiter(endGeometryNormal, startBottom, endBottom, endTop);
+
+ // 2D - don't clone anything from previous segment b/c possible IDL touch
+ startCartographic.latitude = cartographicsArray[cartographicsIndex];
+ startCartographic.longitude = cartographicsArray[cartographicsIndex + 1];
+ endCartographic.latitude = cartographicsArray[cartographicsIndex + 2];
+ endCartographic.longitude = cartographicsArray[cartographicsIndex + 3];
+ var start2D;
+ var end2D;
+ var startGeometryNormal2D;
+ var endGeometryNormal2D;
+
+ if (compute2dAttributes) {
+ var nudgeResult = nudgeCartographic(startCartographic, endCartographic);
+ start2D = projection.project(startCartographic, segmentStart2DScratch);
+ end2D = projection.project(endCartographic, segmentEnd2DScratch);
+ var direction2D = direction(end2D, start2D, forwardOffset2DScratch);
+ direction2D.y = Math.abs(direction2D.y);
+
+ startGeometryNormal2D = segmentStartNormal2DScratch;
+ endGeometryNormal2D = segmentEndNormal2DScratch;
+ if (nudgeResult === 0 || Cartesian3.dot(direction2D, Cartesian3.UNIT_Y) > MITER_BREAK_SMALL) {
+ // No nudge - project the original normal
+ // Or, if the line's angle relative to the IDL is very acute,
+ // in which case snapping will produce oddly shaped volumes.
+ startGeometryNormal2D = projectNormal(projection, startCartographic, startGeometryNormal, start2D, segmentStartNormal2DScratch);
+ endGeometryNormal2D = projectNormal(projection, endCartographic, endGeometryNormal, end2D, segmentEndNormal2DScratch);
+ } else if (nudgeResult === 1) {
+ // Start is close to IDL - snap start normal to align with IDL
+ endGeometryNormal2D = projectNormal(projection, endCartographic, endGeometryNormal, end2D, segmentEndNormal2DScratch);
+ startGeometryNormal2D.x = 0.0;
+ // If start longitude is negative and end longitude is less negative, relative right is unit -Y
+ // If start longitude is positive and end longitude is less positive, relative right is unit +Y
+ startGeometryNormal2D.y = Math.sign(startCartographic.longitude - Math.abs(endCartographic.longitude));
+ startGeometryNormal2D.z = 0.0;
+ } else {
+ // End is close to IDL - snap end normal to align with IDL
+ startGeometryNormal2D = projectNormal(projection, startCartographic, startGeometryNormal, start2D, segmentStartNormal2DScratch);
+ endGeometryNormal2D.x = 0.0;
+ // If end longitude is negative and start longitude is less negative, relative right is unit Y
+ // If end longitude is positive and start longitude is less positive, relative right is unit -Y
+ endGeometryNormal2D.y = Math.sign(startCartographic.longitude - endCartographic.longitude);
+ endGeometryNormal2D.z = 0.0;
+ }
+ }
+
+ /****************************************
+ * Geometry descriptors of a "line on terrain,"
+ * as opposed to the "shadow volume used to draw
+ * the line on terrain":
+ * - position of start + offset to end
+ * - start, end, and right-facing planes
+ * - encoded texture coordinate offsets
+ ****************************************/
+
+ /** 3D **/
+ var segmentLength3D = Cartesian3.distance(startTop, endTop);
+
+ // Encode start position and end position as high precision point + offset
+ var encodedStart = EncodedCartesian3.fromCartesian(startBottom, encodeScratch);
+ var forwardOffset = Cartesian3.subtract(endBottom, startBottom, offsetScratch);
+ var forward = Cartesian3.normalize(forwardOffset, rightScratch);
+
+ // Right plane
+ var startUp = Cartesian3.subtract(startTop, startBottom, startUpScratch);
+ startUp = Cartesian3.normalize(startUp, startUp);
+ var rightNormal = Cartesian3.cross(forward, startUp, rightScratch);
+ rightNormal = Cartesian3.normalize(rightNormal, rightNormal);
+
+ // Plane normals perpendicular to "geometry" normals, so cross (startTop - startBottom) with geometry normal at start
+ var startPlaneNormal = Cartesian3.cross(startUp, startGeometryNormal, startPlaneNormalScratch);
+ startPlaneNormal = Cartesian3.normalize(startPlaneNormal, startPlaneNormal);
+
+ // Similarly with (endTop - endBottom)
+ var endUp = Cartesian3.subtract(endTop, endBottom, endUpScratch);
+ endUp = Cartesian3.normalize(endUp, endUp);
+ var endPlaneNormal = Cartesian3.cross(endGeometryNormal, endUp, endPlaneNormalScratch);
+ endPlaneNormal = Cartesian3.normalize(endPlaneNormal, endPlaneNormal);
+
+ var texcoordNormalization3DX = segmentLength3D / length3D;
+ var texcoordNormalization3DY = lengthSoFar3D / length3D;
+
+ /** 2D **/
+ var segmentLength2D = 0.0;
+ var encodedStart2D;
+ var forwardOffset2D;
+ var right2D;
+ var texcoordNormalization2DX = 0.0;
+ var texcoordNormalization2DY = 0.0;
+ if (compute2dAttributes) {
+ // In 2D case, positions and normals can be done as 2 components
+ segmentLength2D = Cartesian3.distance(start2D, end2D);
+
+ encodedStart2D = EncodedCartesian3.fromCartesian(start2D, encodeScratch2D);
+ forwardOffset2D = Cartesian3.subtract(end2D, start2D, forwardOffset2DScratch);
+
+ // Right direction is just forward direction rotated by -90 degrees around Z
+ // Similarly with plane normals
+ right2D = Cartesian3.normalize(forwardOffset2D, right2DScratch);
+ var swap = right2D.x;
+ right2D.x = right2D.y;
+ right2D.y = -swap;
+
+ texcoordNormalization2DX = segmentLength2D / length2D;
+ texcoordNormalization2DY = lengthSoFar2D / length2D;
+ }
+ /** Pack **/
+ for (j = 0; j < 8; j++) {
+ var vec4Index = vec4sWriteIndex + j * 4;
+ var vec2Index = vec2sWriteIndex + j * 2;
+ var wIndex = vec4Index + 3;
+
+ // Encode sidedness of vertex relative to right plane in texture coordinate normalization X,
+ // whether vertex is top or bottom of volume in sign/magnitude of normalization Y.
+ var rightPlaneSide = j < 4 ? 1.0 : -1.0;
+ var topBottomSide = (j === 2 || j === 3 || j === 6 || j === 7) ? 1.0 : -1.0;
+
+ // 3D
+ Cartesian3.pack(encodedStart.high, startHiAndForwardOffsetX, vec4Index);
+ startHiAndForwardOffsetX[wIndex] = forwardOffset.x;
+
+ Cartesian3.pack(encodedStart.low, startLoAndForwardOffsetY, vec4Index);
+ startLoAndForwardOffsetY[wIndex] = forwardOffset.y;
+
+ Cartesian3.pack(startPlaneNormal, startNormalAndForwardOffsetZ, vec4Index);
+ startNormalAndForwardOffsetZ[wIndex] = forwardOffset.z;
+
+ Cartesian3.pack(endPlaneNormal, endNormalAndTextureCoordinateNormalizationX, vec4Index);
+ endNormalAndTextureCoordinateNormalizationX[wIndex] = texcoordNormalization3DX * rightPlaneSide;
+
+ Cartesian3.pack(rightNormal, rightNormalAndTextureCoordinateNormalizationY, vec4Index);
+
+ var texcoordNormalization = texcoordNormalization3DY * topBottomSide;
+ if (texcoordNormalization === 0.0 && topBottomSide < 0.0) {
+ texcoordNormalization = Number.POSITIVE_INFINITY;
+ }
+ rightNormalAndTextureCoordinateNormalizationY[wIndex] = texcoordNormalization;
+
+ // 2D
+ if (compute2dAttributes) {
+ startHiLo2D[vec4Index] = encodedStart2D.high.x;
+ startHiLo2D[vec4Index + 1] = encodedStart2D.high.y;
+ startHiLo2D[vec4Index + 2] = encodedStart2D.low.x;
+ startHiLo2D[vec4Index + 3] = encodedStart2D.low.y;
+
+ startEndNormals2D[vec4Index] = -startGeometryNormal2D.y;
+ startEndNormals2D[vec4Index + 1] = startGeometryNormal2D.x;
+ startEndNormals2D[vec4Index + 2] = endGeometryNormal2D.y;
+ startEndNormals2D[vec4Index + 3] = -endGeometryNormal2D.x;
+
+ offsetAndRight2D[vec4Index] = forwardOffset2D.x;
+ offsetAndRight2D[vec4Index + 1] = forwardOffset2D.y;
+ offsetAndRight2D[vec4Index + 2] = right2D.x;
+ offsetAndRight2D[vec4Index + 3] = right2D.y;
+
+ texcoordNormalization2D[vec2Index] = texcoordNormalization2DX * rightPlaneSide;
+
+ texcoordNormalization = texcoordNormalization2DY * topBottomSide;
+ if (texcoordNormalization === 0.0 && topBottomSide < 0.0) {
+ texcoordNormalization = Number.POSITIVE_INFINITY;
+ }
+ texcoordNormalization2D[vec2Index + 1] = texcoordNormalization;
+ }
+ }
+
+ /****************************************************************
+ * Vertex Positions
+ *
+ * Encode which side of the line segment each position is on by
+ * pushing it "away" by 1 meter along the geometry normal.
+ *
+ * Needed when pushing the vertices out by varying amounts to
+ * help simulate constant screen-space line width.
+ ****************************************************************/
+ // Adjust heights of positions in 3D
+ var adjustHeightStartBottom = adjustHeightStartBottomScratch;
+ var adjustHeightEndBottom = adjustHeightEndBottomScratch;
+ var adjustHeightStartTop = adjustHeightStartTopScratch;
+ var adjustHeightEndTop = adjustHeightEndTopScratch;
+
+ var getHeightsRectangle = Rectangle.fromCartographicArray(getHeightCartographics, getHeightRectangleScratch);
+ var minMaxHeights = ApproximateTerrainHeights.getApproximateTerrainHeights(getHeightsRectangle, ellipsoid);
+ var minHeight = minMaxHeights.minimumTerrainHeight;
+ var maxHeight = minMaxHeights.maximumTerrainHeight;
+
+ adjustHeights(startBottom, startTop, minHeight, maxHeight, adjustHeightStartBottom, adjustHeightStartTop);
+ adjustHeights(endBottom, endTop, minHeight, maxHeight, adjustHeightEndBottom, adjustHeightEndTop);
+
+ // Nudge the positions away from the "polyline" a little bit to prevent errors in GeometryPipeline
+ var normalNudge = Cartesian3.multiplyByScalar(rightNormal, CesiumMath.EPSILON5, normalNudgeScratch);
+ Cartesian3.add(adjustHeightStartBottom, normalNudge, adjustHeightStartBottom);
+ Cartesian3.add(adjustHeightEndBottom, normalNudge, adjustHeightEndBottom);
+ Cartesian3.add(adjustHeightStartTop, normalNudge, adjustHeightStartTop);
+ Cartesian3.add(adjustHeightEndTop, normalNudge, adjustHeightEndTop);
+
+ // If the segment is very close to the XZ plane, nudge the vertices slightly to avoid touching it.
+ nudgeXZ(adjustHeightStartBottom, adjustHeightEndBottom);
+ nudgeXZ(adjustHeightStartTop, adjustHeightEndTop);
+
+ Cartesian3.pack(adjustHeightStartBottom, positionsArray, vec3sWriteIndex);
+ Cartesian3.pack(adjustHeightEndBottom, positionsArray, vec3sWriteIndex + 3);
+ Cartesian3.pack(adjustHeightEndTop, positionsArray, vec3sWriteIndex + 6);
+ Cartesian3.pack(adjustHeightStartTop, positionsArray, vec3sWriteIndex + 9);
+
+ // Nudge in opposite direction
+ normalNudge = Cartesian3.multiplyByScalar(rightNormal, -2.0 * CesiumMath.EPSILON5, normalNudgeScratch);
+ Cartesian3.add(adjustHeightStartBottom, normalNudge, adjustHeightStartBottom);
+ Cartesian3.add(adjustHeightEndBottom, normalNudge, adjustHeightEndBottom);
+ Cartesian3.add(adjustHeightStartTop, normalNudge, adjustHeightStartTop);
+ Cartesian3.add(adjustHeightEndTop, normalNudge, adjustHeightEndTop);
+
+ // Check against XZ plane again
+ nudgeXZ(adjustHeightStartBottom, adjustHeightEndBottom);
+ nudgeXZ(adjustHeightStartTop, adjustHeightEndTop);
+
+ Cartesian3.pack(adjustHeightStartBottom, positionsArray, vec3sWriteIndex + 12);
+ Cartesian3.pack(adjustHeightEndBottom, positionsArray, vec3sWriteIndex + 15);
+ Cartesian3.pack(adjustHeightEndTop, positionsArray, vec3sWriteIndex + 18);
+ Cartesian3.pack(adjustHeightStartTop, positionsArray, vec3sWriteIndex + 21);
+
+ cartographicsIndex += 2;
+ index += 3;
+
+ vec2sWriteIndex += 16;
+ vec3sWriteIndex += 24;
+ vec4sWriteIndex += 32;
+
+ lengthSoFar3D += segmentLength3D;
+ lengthSoFar2D += segmentLength2D;
+ }
+
+ /*** Generate indices ***/
+ index = 0;
+ var indexOffset = 0;
+ for (i = 0; i < segmentCount; i++) {
+ for (j = 0; j < REFERENCE_INDICES_LENGTH; j++) {
+ indices[index + j] = REFERENCE_INDICES[j] + indexOffset;
+ }
+ indexOffset += 8;
+ index += REFERENCE_INDICES_LENGTH;
+ }
+
+ // Generate bounding sphere
+ var boundingSpheres = scratchBoundingSpheres;
+ BoundingSphere.fromVertices(bottomPositionsArray, Cartesian3.ZERO, 3, boundingSpheres[0]);
+ BoundingSphere.fromVertices(topPositionsArray, Cartesian3.ZERO, 3, boundingSpheres[1]);
+ var boundingSphere = BoundingSphere.fromBoundingSpheres(boundingSpheres);
+
+ var attributes = {
+ position : new GeometryAttribute({
+ componentDatatype : ComponentDatatype.DOUBLE,
+ componentsPerAttribute : 3,
+ normalize : false,
+ values : positionsArray
+ }),
+ startHiAndForwardOffsetX : getVec4GeometryAttribute(startHiAndForwardOffsetX),
+ startLoAndForwardOffsetY : getVec4GeometryAttribute(startLoAndForwardOffsetY),
+ startNormalAndForwardOffsetZ : getVec4GeometryAttribute(startNormalAndForwardOffsetZ),
+ endNormalAndTextureCoordinateNormalizationX : getVec4GeometryAttribute(endNormalAndTextureCoordinateNormalizationX),
+ rightNormalAndTextureCoordinateNormalizationY : getVec4GeometryAttribute(rightNormalAndTextureCoordinateNormalizationY)
+ };
+
+ if (compute2dAttributes) {
+ attributes.startHiLo2D = getVec4GeometryAttribute(startHiLo2D);
+ attributes.offsetAndRight2D = getVec4GeometryAttribute(offsetAndRight2D);
+ attributes.startEndNormals2D = getVec4GeometryAttribute(startEndNormals2D);
+ attributes.texcoordNormalization2D = new GeometryAttribute({
+ componentDatatype : ComponentDatatype.FLOAT,
+ componentsPerAttribute : 2,
+ normalize : false,
+ values : texcoordNormalization2D
+ });
+ }
+
+ return new Geometry({
+ attributes : attributes,
+ indices : indices,
+ boundingSphere : boundingSphere
+ });
+ }
+
+ function getVec4GeometryAttribute(typedArray) {
+ return new GeometryAttribute({
+ componentDatatype : ComponentDatatype.FLOAT,
+ componentsPerAttribute : 4,
+ normalize : false,
+ values : typedArray
+ });
+ }
+
+ /**
+ * Approximates an ellipsoid-tangent vector in 2D by projecting the end point into 2D.
+ * Exposed for testing.
+ *
+ * @param {MapProjection} projection Map Projection for projecting coordinates to 2D.
+ * @param {Cartographic} cartographic The cartographic origin point of the normal.
+ * Used to check if the normal crosses the IDL during projection.
+ * @param {Cartesian3} normal The normal in 3D.
+ * @param {Cartesian3} projectedPosition The projected origin point of the normal in 2D.
+ * @param {Cartesian3} result Result parameter on which to store the projected normal.
+ * @private
+ */
+ GroundPolylineGeometry._projectNormal = projectNormal;
+
+ return GroundPolylineGeometry;
+});
diff --git a/Source/Scene/DerivedCommand.js b/Source/Scene/DerivedCommand.js
index b3bbe671599c..21ffc6c53e7c 100644
--- a/Source/Scene/DerivedCommand.js
+++ b/Source/Scene/DerivedCommand.js
@@ -239,7 +239,8 @@ define([
}
newSources[length] = newMain;
fs = new ShaderSource({
- sources : newSources
+ sources : newSources,
+ defines : fs.defines
});
shader = context.shaderCache.createDerivedShaderProgram(shaderProgram, 'pick', {
vertexShaderSource : shaderProgram.vertexShaderSource,
diff --git a/Source/Scene/GroundPolylinePrimitive.js b/Source/Scene/GroundPolylinePrimitive.js
new file mode 100644
index 000000000000..72b2df985065
--- /dev/null
+++ b/Source/Scene/GroundPolylinePrimitive.js
@@ -0,0 +1,775 @@
+define([
+ '../Core/ApproximateTerrainHeights',
+ '../Core/ComponentDatatype',
+ '../Core/defaultValue',
+ '../Core/defined',
+ '../Core/defineProperties',
+ '../Core/destroyObject',
+ '../Core/DeveloperError',
+ '../Core/GeometryInstance',
+ '../Core/GeometryInstanceAttribute',
+ '../Core/GroundPolylineGeometry',
+ '../Core/isArray',
+ '../Shaders/PolylineShadowVolumeVS',
+ '../Shaders/PolylineShadowVolumeFS',
+ '../Shaders/PolylineShadowVolumeMorphVS',
+ '../Shaders/PolylineShadowVolumeMorphFS',
+ '../Renderer/DrawCommand',
+ '../Renderer/Pass',
+ '../Renderer/RenderState',
+ '../Renderer/ShaderProgram',
+ '../Renderer/ShaderSource',
+ '../ThirdParty/when',
+ './BlendingState',
+ './CullFace',
+ './PolylineColorAppearance',
+ './PolylineMaterialAppearance',
+ './Primitive',
+ './SceneMode'
+ ], function(
+ ApproximateTerrainHeights,
+ ComponentDatatype,
+ defaultValue,
+ defined,
+ defineProperties,
+ destroyObject,
+ DeveloperError,
+ GeometryInstance,
+ GeometryInstanceAttribute,
+ GroundPolylineGeometry,
+ isArray,
+ PolylineShadowVolumeVS,
+ PolylineShadowVolumeFS,
+ PolylineShadowVolumeMorphVS,
+ PolylineShadowVolumeMorphFS,
+ DrawCommand,
+ Pass,
+ RenderState,
+ ShaderProgram,
+ ShaderSource,
+ when,
+ BlendingState,
+ CullFace,
+ PolylineColorAppearance,
+ PolylineMaterialAppearance,
+ Primitive,
+ SceneMode) {
+ 'use strict';
+
+ /**
+ * A GroundPolylinePrimitive represents a polyline draped over the terrain in the {@link Scene}.
+ *
+ *
+ * Only to be used with GeometryInstances containing {@link GroundPolylineGeometry}.
+ *
+ * @param {Object} [options] Object with the following properties:
+ * @param {Array|GeometryInstance} [options.geometryInstances] GeometryInstances containing GroundPolylineGeometry
+ * @param {Appearance} [options.appearance] The Appearance used to render the polyline. Defaults to a white color {@link Material} on a {@link PolylineMaterialAppearance}.
+ * @param {Boolean} [options.show=true] Determines if this primitive will be shown.
+ * @param {Boolean} [options.interleave=false] When true
, geometry vertex attributes are interleaved, which can slightly improve rendering performance but increases load time.
+ * @param {Boolean} [options.releaseGeometryInstances=true] When true
, the primitive does not keep a reference to the input geometryInstances
to save memory.
+ * @param {Boolean} [options.allowPicking=true] When true
, each geometry instance will only be pickable with {@link Scene#pick}. When false
, GPU memory is saved.
+ * @param {Boolean} [options.asynchronous=true] Determines if the primitive will be created asynchronously or block until ready. If false initializeTerrainHeights() must be called first.
+ * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Determines if this primitive's commands' bounding spheres are shown.
+ * @param {Boolean} [options.debugShowShadowVolume=false] For debugging only. Determines if the shadow volume for each geometry in the primitive is drawn. Must be true
on creation to have effect.
+ *
+ * @example
+ * // 1. Draw a polyline on terrain with a basic color material
+ *
+ * var instance = new Cesium.GeometryInstance({
+ * geometry : new Cesium.GroundPolylineGeometry({
+ * positions : Cesium.Cartesian3.fromDegreesArray([
+ * -112.1340164450331, 36.05494287836128,
+ * -112.08821010582645, 36.097804071380715
+ * ]),
+ * width : 4.0
+ * }),
+ * id : 'object returned when this instance is picked and to get/set per-instance attributes'
+ * });
+ *
+ * scene.groundPrimitives.add(new Cesium.GroundPolylinePrimitive({
+ * geometryInstances : instance,
+ * appearance : new Cesium.PolylineMaterialAppearance({
+ * material : Cesium.Material.fromType('Color')
+ * })
+ * }));
+ *
+ * // 2. Draw a looped polyline on terrain with per-instance color and a distance display condition.
+ * // Distance display conditions for polylines on terrain are based on an approximate terrain height
+ * // instead of true terrain height.
+ *
+ * var instance = new Cesium.GeometryInstance({
+ * geometry : new Cesium.GroundPolylineGeometry({
+ * positions : Cesium.Cartesian3.fromDegreesArray([
+ * -112.1340164450331, 36.05494287836128,
+ * -112.08821010582645, 36.097804071380715,
+ * -112.13296079730024, 36.168769146801104
+ * ]),
+ * loop : true,
+ * width : 4.0
+ * }),
+ * attributes : {
+ * color : Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.fromCssColorString('green').withAlpha(0.7)),
+ distanceDisplayCondition : new Cesium.DistanceDisplayConditionGeometryInstanceAttribute(1000, 30000)
+ * },
+ * id : 'object returned when this instance is picked and to get/set per-instance attributes'
+ * });
+ *
+ * scene.groundPrimitives.add(new Cesium.GroundPolylinePrimitive({
+ * geometryInstances : instance,
+ * appearance : Cesium.PolylineColorAppearance()
+ * }));
+ */
+ function GroundPolylinePrimitive(options) {
+ options = defaultValue(options, defaultValue.EMPTY_OBJECT);
+
+ /**
+ * The geometry instances rendered with this primitive. This may
+ * be undefined
if options.releaseGeometryInstances
+ * is true
when the primitive is constructed.
+ *
+ * Changing this property after the primitive is rendered has no effect.
+ *
+ *
+ * @readonly
+ * @type {Array|GeometryInstance}
+ *
+ * @default undefined
+ */
+ this.geometryInstances = options.geometryInstances;
+ this._hasPerInstanceColors = true;
+
+ var appearance = options.appearance;
+ if (!defined(appearance)) {
+ appearance = new PolylineMaterialAppearance();
+ }
+ /**
+ * The {@link Appearance} used to shade this primitive. Each geometry
+ * instance is shaded with the same appearance. Some appearances, like
+ * {@link PolylineColorAppearance} allow giving each instance unique
+ * properties.
+ *
+ * @type Appearance
+ *
+ * @default undefined
+ */
+ this.appearance = appearance;
+
+ /**
+ * Determines if the primitive will be shown. This affects all geometry
+ * instances in the primitive.
+ *
+ * @type {Boolean}
+ *
+ * @default true
+ */
+ this.show = defaultValue(options.show, true);
+
+ /**
+ * This property is for debugging only; it is not for production use nor is it optimized.
+ *
+ * Draws the bounding sphere for each draw command in the primitive.
+ *
+ *
+ * @type {Boolean}
+ *
+ * @default false
+ */
+ this.debugShowBoundingVolume = defaultValue(options.debugShowBoundingVolume, false);
+
+ // Shadow volume is shown by removing a discard in the shader, so this isn't toggleable.
+ this._debugShowShadowVolume = defaultValue(options.debugShowShadowVolume, false);
+
+ this._primitiveOptions = {
+ geometryInstances : undefined,
+ appearance : undefined,
+ vertexCacheOptimize : false,
+ interleave : defaultValue(options.interleave, false),
+ releaseGeometryInstances : defaultValue(options.releaseGeometryInstances, true),
+ allowPicking : defaultValue(options.allowPicking, true),
+ asynchronous : defaultValue(options.asynchronous, true),
+ compressVertices : false,
+ _createShaderProgramFunction : undefined,
+ _createCommandsFunction : undefined,
+ _updateAndQueueCommandsFunction : undefined
+ };
+
+ // Used when inserting in an OrderedPrimitiveCollection
+ this._zIndex = undefined;
+
+ this._ready = false;
+ this._readyPromise = when.defer();
+
+ this._primitive = undefined;
+
+ this._sp = undefined;
+ this._sp2D = undefined;
+ this._spMorph = undefined;
+
+ this._renderState = RenderState.fromCache({
+ cull : {
+ enabled : true // prevent double-draw. Geometry is "inverted" (reversed winding order) so we're drawing backfaces.
+ },
+ blending : BlendingState.ALPHA_BLEND,
+ depthMask : false
+ });
+
+ this._renderStateMorph = RenderState.fromCache({
+ cull : {
+ enabled : true,
+ face : CullFace.FRONT // Geometry is "inverted," so cull front when materials on volume instead of on terrain (morph)
+ },
+ depthTest : {
+ enabled : true
+ },
+ blending : BlendingState.ALPHA_BLEND,
+ depthMask : false
+ });
+ }
+
+ defineProperties(GroundPolylinePrimitive.prototype, {
+ /**
+ * Determines if geometry vertex attributes are interleaved, which can slightly improve rendering performance.
+ *
+ * @memberof GroundPolylinePrimitive.prototype
+ *
+ * @type {Boolean}
+ * @readonly
+ *
+ * @default false
+ */
+ interleave : {
+ get : function() {
+ return this._primitiveOptions.interleave;
+ }
+ },
+
+ /**
+ * When true
, the primitive does not keep a reference to the input geometryInstances
to save memory.
+ *
+ * @memberof GroundPolylinePrimitive.prototype
+ *
+ * @type {Boolean}
+ * @readonly
+ *
+ * @default true
+ */
+ releaseGeometryInstances : {
+ get : function() {
+ return this._primitiveOptions.releaseGeometryInstances;
+ }
+ },
+
+ /**
+ * When true
, each geometry instance will only be pickable with {@link Scene#pick}. When false
, GPU memory is saved.
+ *
+ * @memberof GroundPolylinePrimitive.prototype
+ *
+ * @type {Boolean}
+ * @readonly
+ *
+ * @default true
+ */
+ allowPicking : {
+ get : function() {
+ return this._primitiveOptions.allowPicking;
+ }
+ },
+
+ /**
+ * Determines if the geometry instances will be created and batched on a web worker.
+ *
+ * @memberof GroundPolylinePrimitive.prototype
+ *
+ * @type {Boolean}
+ * @readonly
+ *
+ * @default true
+ */
+ asynchronous : {
+ get : function() {
+ return this._primitiveOptions.asynchronous;
+ }
+ },
+
+ /**
+ * Determines if the primitive is complete and ready to render. If this property is
+ * true, the primitive will be rendered the next time that {@link GroundPolylinePrimitive#update}
+ * is called.
+ *
+ * @memberof GroundPolylinePrimitive.prototype
+ *
+ * @type {Boolean}
+ * @readonly
+ */
+ ready : {
+ get : function() {
+ return this._ready;
+ }
+ },
+
+ /**
+ * Gets a promise that resolves when the primitive is ready to render.
+ * @memberof GroundPolylinePrimitive.prototype
+ * @type {Promise.}
+ * @readonly
+ */
+ readyPromise : {
+ get : function() {
+ return this._readyPromise.promise;
+ }
+ },
+
+ /**
+ * This property is for debugging only; it is not for production use nor is it optimized.
+ *
+ * If true, draws the shadow volume for each geometry in the primitive.
+ *
+ *
+ * @memberof GroundPolylinePrimitive.prototype
+ *
+ * @type {Boolean}
+ * @readonly
+ *
+ * @default false
+ */
+ debugShowShadowVolume : {
+ get : function() {
+ return this._debugShowShadowVolume;
+ }
+ }
+ });
+
+ GroundPolylinePrimitive._initialized = false;
+ GroundPolylinePrimitive._initPromise = undefined;
+
+ /**
+ * Initializes the minimum and maximum terrain heights. This only needs to be called if you are creating the
+ * GroundPolylinePrimitive synchronously.
+ *
+ * @returns {Promise} A promise that will resolve once the terrain heights have been loaded.
+ *
+ */
+ GroundPolylinePrimitive.initializeTerrainHeights = function() {
+ var initPromise = GroundPolylinePrimitive._initPromise;
+ if (defined(initPromise)) {
+ return initPromise;
+ }
+
+ GroundPolylinePrimitive._initPromise = ApproximateTerrainHeights.initialize()
+ .then(function() {
+ GroundPolylinePrimitive._initialized = true;
+ });
+
+ return GroundPolylinePrimitive._initPromise;
+ };
+
+ // For use with web workers.
+ GroundPolylinePrimitive._initializeTerrainHeightsWorker = function() {
+ var initPromise = GroundPolylinePrimitive._initPromise;
+ if (defined(initPromise)) {
+ return initPromise;
+ }
+
+ GroundPolylinePrimitive._initPromise = ApproximateTerrainHeights.initialize('../Assets/approximateTerrainHeights.json')
+ .then(function() {
+ GroundPolylinePrimitive._initialized = true;
+ });
+
+ return GroundPolylinePrimitive._initPromise;
+ };
+
+ function createShaderProgram(groundPolylinePrimitive, frameState, appearance) {
+ var context = frameState.context;
+ var primitive = groundPolylinePrimitive._primitive;
+ var attributeLocations = primitive._attributeLocations;
+
+ var vs = primitive._batchTable.getVertexShaderCallback()(PolylineShadowVolumeVS);
+ vs = Primitive._appendShowToShader(primitive, vs);
+ vs = Primitive._appendDistanceDisplayConditionToShader(primitive, vs);
+ vs = Primitive._modifyShaderPosition(groundPolylinePrimitive, vs, frameState.scene3DOnly);
+
+ var vsMorph = primitive._batchTable.getVertexShaderCallback()(PolylineShadowVolumeMorphVS);
+ vsMorph = Primitive._appendShowToShader(primitive, vsMorph);
+ vsMorph = Primitive._appendDistanceDisplayConditionToShader(primitive, vsMorph);
+ vsMorph = Primitive._modifyShaderPosition(groundPolylinePrimitive, vsMorph, frameState.scene3DOnly);
+
+ // Access pick color from fragment shader.
+ // Helps with varying budget.
+ var fs = primitive._batchTable.getVertexShaderCallback()(PolylineShadowVolumeFS);
+
+ // Tesselation on these volumes tends to be low,
+ // which causes problems when interpolating log depth from vertices.
+ // So force computing and writing log depth in the fragment shader.
+ // Re-enable at far distances to avoid z-fighting.
+ var vsDefines = ['ENABLE_GL_POSITION_LOG_DEPTH_AT_HEIGHT', 'GLOBE_MINIMUM_ALTITUDE ' + frameState.mapProjection.ellipsoid.minimumRadius.toFixed(1)];
+ var colorDefine = '';
+ var materialShaderSource = '';
+ if (defined(appearance.material)) {
+ materialShaderSource = defined(appearance.material) ? appearance.material.shaderSource : '';
+
+ // Check for use of v_width and v_polylineAngle in material shader
+ // to determine whether these varyings should be active in the vertex shader.
+ if (materialShaderSource.search(/varying\s+float\s+v_polylineAngle;/g) !== -1) {
+ vsDefines.push('ANGLE_VARYING');
+ }
+ if (materialShaderSource.search(/varying\s+float\s+v_width;/g) !== -1) {
+ vsDefines.push('WIDTH_VARYING');
+ }
+ } else {
+ colorDefine = 'PER_INSTANCE_COLOR';
+ }
+
+ vsDefines.push(colorDefine);
+ var fsDefines = groundPolylinePrimitive.debugShowShadowVolume ? ['DEBUG_SHOW_VOLUME', colorDefine] : [colorDefine];
+
+ var vsColor3D = new ShaderSource({
+ defines : vsDefines,
+ sources : [vs]
+ });
+ var fsColor3D = new ShaderSource({
+ defines : fsDefines,
+ sources : [materialShaderSource, fs]
+ });
+ groundPolylinePrimitive._sp = ShaderProgram.replaceCache({
+ context : context,
+ shaderProgram : primitive._sp,
+ vertexShaderSource : vsColor3D,
+ fragmentShaderSource : fsColor3D,
+ attributeLocations : attributeLocations
+ });
+
+ // Derive 2D/CV
+ var colorProgram2D = context.shaderCache.getDerivedShaderProgram(groundPolylinePrimitive._sp, '2dColor');
+ if (!defined(colorProgram2D)) {
+ var vsColor2D = new ShaderSource({
+ defines : vsDefines.concat(['COLUMBUS_VIEW_2D']),
+ sources : [vs]
+ });
+ colorProgram2D = context.shaderCache.createDerivedShaderProgram(groundPolylinePrimitive._sp, '2dColor', {
+ context : context,
+ shaderProgram : groundPolylinePrimitive._sp2D,
+ vertexShaderSource : vsColor2D,
+ fragmentShaderSource : fsColor3D,
+ attributeLocations : attributeLocations
+ });
+ }
+ groundPolylinePrimitive._sp2D = colorProgram2D;
+
+ // Derive Morph
+ var colorProgramMorph = context.shaderCache.getDerivedShaderProgram(groundPolylinePrimitive._sp, 'MorphColor');
+ if (!defined(colorProgramMorph)) {
+ var vsColorMorph = new ShaderSource({
+ defines : vsDefines.concat(['MAX_TERRAIN_HEIGHT ' + ApproximateTerrainHeights._defaultMaxTerrainHeight.toFixed(1)]),
+ sources : [vsMorph]
+ });
+
+ fs = primitive._batchTable.getVertexShaderCallback()(PolylineShadowVolumeMorphFS);
+ var fsColorMorph = new ShaderSource({
+ defines : fsDefines,
+ sources : [materialShaderSource, fs]
+ });
+ colorProgramMorph = context.shaderCache.createDerivedShaderProgram(groundPolylinePrimitive._sp, 'MorphColor', {
+ context : context,
+ shaderProgram : groundPolylinePrimitive._spMorph,
+ vertexShaderSource : vsColorMorph,
+ fragmentShaderSource : fsColorMorph,
+ attributeLocations : attributeLocations
+ });
+ }
+ groundPolylinePrimitive._spMorph = colorProgramMorph;
+ }
+
+ function createCommands(groundPolylinePrimitive, appearance, material, translucent, colorCommands, pickCommands) {
+ var primitive = groundPolylinePrimitive._primitive;
+ var length = primitive._va.length;
+ colorCommands.length = length;
+ pickCommands.length = length;
+
+ var isPolylineColorAppearance = appearance instanceof PolylineColorAppearance;
+
+ var i;
+ var command;
+ var materialUniforms = isPolylineColorAppearance ? {} : material._uniforms;
+ var uniformMap = primitive._batchTable.getUniformMapCallback()(materialUniforms);
+ var pass = Pass.TERRAIN_CLASSIFICATION;
+
+ for (i = 0; i < length; i++) {
+ var vertexArray = primitive._va[i];
+
+ command = colorCommands[i];
+ if (!defined(command)) {
+ command = colorCommands[i] = new DrawCommand({
+ owner : groundPolylinePrimitive,
+ primitiveType : primitive._primitiveType
+ });
+ }
+
+ command.vertexArray = vertexArray;
+ command.renderState = groundPolylinePrimitive._renderState;
+ command.shaderProgram = groundPolylinePrimitive._sp;
+ command.uniformMap = uniformMap;
+ command.pass = pass;
+ command.pickId = 'czm_batchTable_pickColor(v_endPlaneNormalEcAndBatchId.w)';
+
+ // derive for 2D
+ var derivedColorCommand = command.derivedCommands.color2D;
+ if (!defined(derivedColorCommand)) {
+ derivedColorCommand = DrawCommand.shallowClone(command);
+ command.derivedCommands.color2D = derivedColorCommand;
+ }
+ derivedColorCommand.vertexArray = vertexArray;
+ derivedColorCommand.renderState = groundPolylinePrimitive._renderState;
+ derivedColorCommand.shaderProgram = groundPolylinePrimitive._sp2D;
+ derivedColorCommand.uniformMap = uniformMap;
+ derivedColorCommand.pass = pass;
+ derivedColorCommand.pickId = 'czm_batchTable_pickColor(v_endPlaneNormalEcAndBatchId.w)';
+
+ // derive for Morph
+ derivedColorCommand = command.derivedCommands.colorMorph;
+ if (!defined(derivedColorCommand)) {
+ derivedColorCommand = DrawCommand.shallowClone(command);
+ command.derivedCommands.colorMorph = derivedColorCommand;
+ }
+ derivedColorCommand.vertexArray = vertexArray;
+ derivedColorCommand.renderState = groundPolylinePrimitive._renderStateMorph;
+ derivedColorCommand.shaderProgram = groundPolylinePrimitive._spMorph;
+ derivedColorCommand.uniformMap = uniformMap;
+ derivedColorCommand.pass = pass;
+ derivedColorCommand.pickId = 'czm_batchTable_pickColor(v_batchId)';
+ }
+ }
+
+ function updateAndQueueCommands(groundPolylinePrimitive, frameState, colorCommands, pickCommands, modelMatrix, cull, debugShowBoundingVolume) {
+ var primitive = groundPolylinePrimitive._primitive;
+
+ Primitive._updateBoundingVolumes(primitive, frameState, modelMatrix); // Expected to be identity - GroundPrimitives don't support other model matrices
+
+ var boundingSpheres;
+ if (frameState.mode === SceneMode.SCENE3D) {
+ boundingSpheres = primitive._boundingSphereWC;
+ } else if (frameState.mode === SceneMode.COLUMBUS_VIEW) {
+ boundingSpheres = primitive._boundingSphereCV;
+ } else if (frameState.mode === SceneMode.SCENE2D && defined(primitive._boundingSphere2D)) {
+ boundingSpheres = primitive._boundingSphere2D;
+ } else if (defined(primitive._boundingSphereMorph)) {
+ boundingSpheres = primitive._boundingSphereMorph;
+ }
+
+ var commandList = frameState.commandList;
+ var passes = frameState.passes;
+ if (passes.render || (passes.pick && primitive.allowPicking)) {
+ var colorLength = colorCommands.length;
+
+ for (var j = 0; j < colorLength; ++j) {
+ var colorCommand = colorCommands[j];
+ // Use derived appearance command for morph and 2D
+ if (frameState.mode === SceneMode.MORPHING && colorCommand.shaderProgram !== groundPolylinePrimitive._spMorph) {
+ colorCommand = colorCommand.derivedCommands.colorMorph;
+ } else if (frameState.mode !== SceneMode.SCENE3D && colorCommand.shaderProgram !== groundPolylinePrimitive._sp2D) {
+ colorCommand = colorCommand.derivedCommands.color2D;
+ }
+ colorCommand.modelMatrix = modelMatrix;
+ colorCommand.boundingVolume = boundingSpheres[j];
+ colorCommand.cull = cull;
+ colorCommand.debugShowBoundingVolume = debugShowBoundingVolume;
+
+ commandList.push(colorCommand);
+ }
+ }
+ }
+
+ /**
+ * Called when {@link Viewer} or {@link CesiumWidget} render the scene to
+ * get the draw commands needed to render this primitive.
+ *
+ * Do not call this function directly. This is documented just to
+ * list the exceptions that may be propagated when the scene is rendered:
+ *
+ *
+ * @exception {DeveloperError} For synchronous GroundPolylinePrimitives, you must call GroundPolylinePrimitives.initializeTerrainHeights() and wait for the returned promise to resolve.
+ * @exception {DeveloperError} All GeometryInstances must have color attributes to use PolylineColorAppearance with GroundPolylinePrimitive.
+ */
+ GroundPolylinePrimitive.prototype.update = function(frameState) {
+ if (!defined(this._primitive) && !defined(this.geometryInstances)) {
+ return;
+ }
+
+ if (!GroundPolylinePrimitive._initialized) {
+ //>>includeStart('debug', pragmas.debug);
+ if (!this.asynchronous) {
+ throw new DeveloperError('For synchronous GroundPolylinePrimitives, you must call GroundPolylinePrimitives.initializeTerrainHeights() and wait for the returned promise to resolve.');
+ }
+ //>>includeEnd('debug');
+
+ GroundPolylinePrimitive.initializeTerrainHeights();
+ return;
+ }
+
+ var i;
+
+ var that = this;
+ var primitiveOptions = this._primitiveOptions;
+ if (!defined(this._primitive)) {
+ var geometryInstances = isArray(this.geometryInstances) ? this.geometryInstances : [this.geometryInstances];
+ var geometryInstancesLength = geometryInstances.length;
+ var groundInstances = new Array(geometryInstancesLength);
+
+ var attributes;
+
+ // Check if each instance has a color attribute.
+ for (i = 0; i < geometryInstancesLength; ++i) {
+ attributes = geometryInstances[i].attributes;
+ if (!defined(attributes) || !defined(attributes.color)) {
+ this._hasPerInstanceColors = false;
+ break;
+ }
+ }
+
+ for (i = 0; i < geometryInstancesLength; ++i) {
+ var geometryInstance = geometryInstances[i];
+ attributes = {};
+ var instanceAttributes = geometryInstance.attributes;
+ for (var attributeKey in instanceAttributes) {
+ if (instanceAttributes.hasOwnProperty(attributeKey)) {
+ attributes[attributeKey] = instanceAttributes[attributeKey];
+ }
+ }
+
+ // Automatically create line width attribute if not already given
+ if (!defined(attributes.width)) {
+ attributes.width = new GeometryInstanceAttribute({
+ componentDatatype : ComponentDatatype.UNSIGNED_BYTE,
+ componentsPerAttribute : 1.0,
+ value : [geometryInstance.geometry.width]
+ });
+ }
+
+ // Update each geometry for framestate.scene3DOnly = true and projection
+ geometryInstance.geometry._scene3DOnly = frameState.scene3DOnly;
+ GroundPolylineGeometry.setProjectionAndEllipsoid(geometryInstance.geometry, frameState.mapProjection);
+
+ groundInstances[i] = new GeometryInstance({
+ geometry : geometryInstance.geometry,
+ attributes : attributes,
+ id : geometryInstance.id
+ });
+ }
+
+ primitiveOptions.geometryInstances = groundInstances;
+ primitiveOptions.appearance = this.appearance;
+
+ primitiveOptions._createShaderProgramFunction = function(primitive, frameState, appearance) {
+ createShaderProgram(that, frameState, appearance);
+ };
+ primitiveOptions._createCommandsFunction = function(primitive, appearance, material, translucent, twoPasses, colorCommands, pickCommands) {
+ createCommands(that, appearance, material, translucent, colorCommands, pickCommands);
+ };
+ primitiveOptions._updateAndQueueCommandsFunction = function(primitive, frameState, colorCommands, pickCommands, modelMatrix, cull, debugShowBoundingVolume, twoPasses) {
+ updateAndQueueCommands(that, frameState, colorCommands, pickCommands, modelMatrix, cull, debugShowBoundingVolume);
+ };
+
+ this._primitive = new Primitive(primitiveOptions);
+ this._primitive.readyPromise.then(function(primitive) {
+ that._ready = true;
+
+ if (that.releaseGeometryInstances) {
+ that.geometryInstances = undefined;
+ }
+
+ var error = primitive._error;
+ if (!defined(error)) {
+ that._readyPromise.resolve(that);
+ } else {
+ that._readyPromise.reject(error);
+ }
+ });
+ }
+
+ if (this.appearance instanceof PolylineColorAppearance && !this._hasPerInstanceColors) {
+ throw new DeveloperError('All GeometryInstances must have color attributes to use PolylineColorAppearance with GroundPolylinePrimitive.');
+ }
+
+ this._primitive.appearance = this.appearance;
+ this._primitive.show = this.show;
+ this._primitive.debugShowBoundingVolume = this.debugShowBoundingVolume;
+ this._primitive.update(frameState);
+ };
+
+ /**
+ * Returns the modifiable per-instance attributes for a {@link GeometryInstance}.
+ *
+ * @param {*} id The id of the {@link GeometryInstance}.
+ * @returns {Object} The typed array in the attribute's format or undefined if the is no instance with id.
+ *
+ * @exception {DeveloperError} must call update before calling getGeometryInstanceAttributes.
+ *
+ * @example
+ * var attributes = primitive.getGeometryInstanceAttributes('an id');
+ * attributes.color = Cesium.ColorGeometryInstanceAttribute.toValue(Cesium.Color.AQUA);
+ * attributes.show = Cesium.ShowGeometryInstanceAttribute.toValue(true);
+ */
+ GroundPolylinePrimitive.prototype.getGeometryInstanceAttributes = function(id) {
+ //>>includeStart('debug', pragmas.debug);
+ if (!defined(this._primitive)) {
+ throw new DeveloperError('must call update before calling getGeometryInstanceAttributes');
+ }
+ //>>includeEnd('debug');
+ return this._primitive.getGeometryInstanceAttributes(id);
+ };
+
+ /**
+ * Checks if the given Scene supports GroundPolylinePrimitives.
+ * GroundPolylinePrimitives require support for the WEBGL_depth_texture extension.
+ *
+ * @param {Scene} scene The current scene.
+ * @returns {Boolean} Whether or not the current scene supports GroundPolylinePrimitives.
+ */
+ GroundPolylinePrimitive.isSupported = function(scene) {
+ return scene.frameState.context.depthTexture;
+ };
+
+ /**
+ * Returns true if this object was destroyed; otherwise, false.
+ *
+ * If this object was destroyed, it should not be used; calling any function other than
+ * isDestroyed
will result in a {@link DeveloperError} exception.
+ *
+ *
+ * @returns {Boolean} true
if this object was destroyed; otherwise, false
.
+ *
+ * @see GroundPolylinePrimitive#destroy
+ */
+ GroundPolylinePrimitive.prototype.isDestroyed = function() {
+ return false;
+ };
+
+ /**
+ * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
+ * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
+ *
+ * Once an object is destroyed, it should not be used; calling any function other than
+ * isDestroyed
will result in a {@link DeveloperError} exception. Therefore,
+ * assign the return value (undefined
) to the object as done in the example.
+ *
+ *
+ * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
+ *
+ * @example
+ * e = e && e.destroy();
+ *
+ * @see GroundPolylinePrimitive#isDestroyed
+ */
+ GroundPolylinePrimitive.prototype.destroy = function() {
+ this._primitive = this._primitive && this._primitive.destroy();
+ this._sp = this._sp && this._sp.destroy();
+
+ // Derived programs, destroyed above if they existed.
+ this._sp2D = undefined;
+ this._spMorph = undefined;
+
+ return destroyObject(this);
+ };
+
+ return GroundPolylinePrimitive;
+});
diff --git a/Source/Scene/GroundPrimitive.js b/Source/Scene/GroundPrimitive.js
index 46cb2ce7befc..de6c371b6f71 100644
--- a/Source/Scene/GroundPrimitive.js
+++ b/Source/Scene/GroundPrimitive.js
@@ -174,21 +174,25 @@ define([
}
}
}
-
+ /**
+ * The {@link Appearance} used to shade this primitive. Each geometry
+ * instance is shaded with the same appearance. Some appearances, like
+ * {@link PerInstanceColorAppearance} allow giving each instance unique
+ * properties.
+ *
+ * @type Appearance
+ *
+ * @default undefined
+ */
this.appearance = appearance;
/**
- * The geometry instance rendered with this primitive. This may
+ * The geometry instances rendered with this primitive. This may
* be undefined
if options.releaseGeometryInstances
* is true
when the primitive is constructed.
*
* Changing this property after the primitive is rendered has no effect.
*
- *
- * Because of the rendering technique used, all geometry instances must be the same color.
- * If there is an instance with a differing color, a DeveloperError
will be thrown
- * on the first attempt to render.
- *
*
* @readonly
* @type {Array|GeometryInstance}
@@ -541,7 +545,7 @@ define([
for (i = 0; i < colorLength; ++i) {
colorCommand = colorCommands[i];
- // derive a separate appearance command for 2D if needed
+ // Use derived appearance command for 2D if needed
if (frameState.mode !== SceneMode.SCENE3D &&
colorCommand.shaderProgram === classificationPrimitive._spColor &&
classificationPrimitive._needs2DShader) {
@@ -587,7 +591,7 @@ define([
for (var j = 0; j < pickLength; ++j) {
var pickCommand = pickCommands[j];
- // derive a separate appearance command for 2D if needed
+ // Use derived pick command for 2D if needed
if (frameState.mode !== SceneMode.SCENE3D &&
pickCommand.shaderProgram === classificationPrimitive._spPick &&
classificationPrimitive._needs2DShader) {
@@ -642,9 +646,9 @@ define([
* list the exceptions that may be propagated when the scene is rendered:
*
*
+ * @exception {DeveloperError} For synchronous GroundPrimitive, you must call GroundPrimitive.initializeTerrainHeights() and wait for the returned promise to resolve.
* @exception {DeveloperError} All instance geometries must have the same primitiveType.
* @exception {DeveloperError} Appearance and material have a uniform with the same name.
- * @exception {DeveloperError} Not all of the geometry instances have the same color attribute.
*/
GroundPrimitive.prototype.update = function(frameState) {
if (!defined(this._primitive) && !defined(this.geometryInstances)) {
diff --git a/Source/Scene/PolylineColorAppearance.js b/Source/Scene/PolylineColorAppearance.js
index ceafe597913f..1d774cb031c6 100644
--- a/Source/Scene/PolylineColorAppearance.js
+++ b/Source/Scene/PolylineColorAppearance.js
@@ -20,7 +20,8 @@ define([
var defaultFragmentShaderSource = PerInstanceFlatColorAppearanceFS;
/**
- * An appearance for {@link GeometryInstance} instances with color attributes and {@link PolylineGeometry}.
+ * An appearance for {@link GeometryInstance} instances with color attributes and
+ * {@link PolylineGeometry} or {@link GroundPolylineGeometry}.
* This allows several geometry instances, each with a different color, to
* be drawn with the same {@link Primitive}.
*
diff --git a/Source/Shaders/Builtin/Functions/approximateSphericalCoordinates.glsl b/Source/Shaders/Builtin/Functions/approximateSphericalCoordinates.glsl
index 90fe79393a3d..78f52d54ff52 100644
--- a/Source/Shaders/Builtin/Functions/approximateSphericalCoordinates.glsl
+++ b/Source/Shaders/Builtin/Functions/approximateSphericalCoordinates.glsl
@@ -1,36 +1,3 @@
-// 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,
@@ -45,7 +12,7 @@ float fastApproximateAtan2(float x, float y) {
*/
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);
+ float latitudeApproximation = czm_fastApproximateAtan(sqrt(normal.x * normal.x + normal.y * normal.y), normal.z);
+ float longitudeApproximation = czm_fastApproximateAtan(normal.x, normal.y);
return vec2(latitudeApproximation, longitudeApproximation);
}
diff --git a/Source/Shaders/Builtin/Functions/branchFreeTernary.glsl b/Source/Shaders/Builtin/Functions/branchFreeTernary.glsl
new file mode 100644
index 000000000000..5959d1387713
--- /dev/null
+++ b/Source/Shaders/Builtin/Functions/branchFreeTernary.glsl
@@ -0,0 +1,71 @@
+/**
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
+ * evaluate both possibilities for a float expression.
+ *
+ * @name czm_branchFreeTernary
+ * @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_branchFreeTernary(bool comparison, float a, float b) {
+ float useA = float(comparison);
+ return a * useA + b * (1.0 - useA);
+}
+
+/**
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
+ * evaluate both possibilities for a vec2 expression.
+ *
+ * @name czm_branchFreeTernary
+ * @glslFunction
+ *
+ * @param {bool} comparison A comparison statement
+ * @param {vec2} a Value to return if the comparison is true.
+ * @param {vec2} b Value to return if the comparison is false.
+ *
+ * @returns {vec2} equivalent of comparison ? a : b
+ */
+vec2 czm_branchFreeTernary(bool comparison, vec2 a, vec2 b) {
+ float useA = float(comparison);
+ return a * useA + b * (1.0 - useA);
+}
+
+/**
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
+ * evaluate both possibilities for a vec3 expression.
+ *
+ * @name czm_branchFreeTernary
+ * @glslFunction
+ *
+ * @param {bool} comparison A comparison statement
+ * @param {vec3} a Value to return if the comparison is true.
+ * @param {vec3} b Value to return if the comparison is false.
+ *
+ * @returns {vec3} equivalent of comparison ? a : b
+ */
+vec3 czm_branchFreeTernary(bool comparison, vec3 a, vec3 b) {
+ float useA = float(comparison);
+ return a * useA + b * (1.0 - useA);
+}
+
+/**
+ * Branchless ternary operator to be used when it's inexpensive to explicitly
+ * evaluate both possibilities for a vec4 expression.
+ *
+ * @name czm_branchFreeTernary
+ * @glslFunction
+ *
+ * @param {bool} comparison A comparison statement
+ * @param {vec3} a Value to return if the comparison is true.
+ * @param {vec3} b Value to return if the comparison is false.
+ *
+ * @returns {vec3} equivalent of comparison ? a : b
+ */
+vec4 czm_branchFreeTernary(bool comparison, vec4 a, vec4 b) {
+ float useA = float(comparison);
+ return a * useA + b * (1.0 - useA);
+}
diff --git a/Source/Shaders/Builtin/Functions/branchFreeTernaryFloat.glsl b/Source/Shaders/Builtin/Functions/branchFreeTernaryFloat.glsl
deleted file mode 100644
index 951f2b155cee..000000000000
--- a/Source/Shaders/Builtin/Functions/branchFreeTernaryFloat.glsl
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * 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/fastApproximateAtan.glsl b/Source/Shaders/Builtin/Functions/fastApproximateAtan.glsl
new file mode 100644
index 000000000000..09d639a99a43
--- /dev/null
+++ b/Source/Shaders/Builtin/Functions/fastApproximateAtan.glsl
@@ -0,0 +1,55 @@
+/**
+ * Approxiamtes atan over the range [0, 1]. Safe to flip output for negative input.
+ *
+ * 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.
+ *
+ * @name czm_fastApproximateAtan
+ * @glslFunction
+ *
+ * @param {float} x Value between 0 and 1 inclusive.
+ *
+ * @returns {float} Approximation of atan(x)
+ */
+float czm_fastApproximateAtan(float x) {
+ return x * (-0.1784 * x - 0.0663 * x * x + 1.0301);
+}
+
+/**
+ * Approximation of atan2.
+ *
+ * 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 (see above).
+ *
+ * @name czm_fastApproximateAtan
+ * @glslFunction
+ *
+ * @param {float} x Value between -1 and 1 inclusive.
+ * @param {float} y Value between -1 and 1 inclusive.
+ *
+ * @returns {float} Approximation of atan2(x, y)
+ */
+float czm_fastApproximateAtan(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 = czm_fastApproximateAtan(opposite / adjacent);
+
+ // Undo range reduction
+ t = czm_branchFreeTernary(abs(y) > abs(x), czm_piOverTwo - t, t);
+ t = czm_branchFreeTernary(x < 0.0, czm_pi - t, t);
+ t = czm_branchFreeTernary(y < 0.0, -t, t);
+ return t;
+}
diff --git a/Source/Shaders/Builtin/Functions/planeDistance.glsl b/Source/Shaders/Builtin/Functions/planeDistance.glsl
index 38db05a173c6..7ebb5f46f2f5 100644
--- a/Source/Shaders/Builtin/Functions/planeDistance.glsl
+++ b/Source/Shaders/Builtin/Functions/planeDistance.glsl
@@ -1,5 +1,5 @@
/**
- * Computes distance from an point to a plane, typically in eye space.
+ * Computes distance from a point to a plane.
*
* @name czm_planeDistance
* @glslFunction
@@ -11,3 +11,18 @@
float czm_planeDistance(vec4 plane, vec3 point) {
return (dot(plane.xyz, point) + plane.w);
}
+
+/**
+ * Computes distance from a point to a plane.
+ *
+ * @name czm_planeDistance
+ * @glslFunction
+ *
+ * param {vec3} planeNormal Normal for a plane in Hessian Normal Form. See Plane.js
+ * param {float} planeDistance Distance for 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(vec3 planeNormal, float planeDistance, vec3 point) {
+ return (dot(planeNormal, point) + planeDistance);
+}
diff --git a/Source/Shaders/PolylineShadowVolumeFS.glsl b/Source/Shaders/PolylineShadowVolumeFS.glsl
new file mode 100644
index 000000000000..1042fe7f2c2f
--- /dev/null
+++ b/Source/Shaders/PolylineShadowVolumeFS.glsl
@@ -0,0 +1,81 @@
+varying vec4 v_startPlaneNormalEcAndHalfWidth;
+varying vec4 v_endPlaneNormalEcAndBatchId;
+varying vec4 v_rightPlaneEC; // Technically can compute distance for this here
+varying vec4 v_endEcAndStartEcX;
+varying vec4 v_texcoordNormalizationAndStartEcYZ;
+
+#ifdef PER_INSTANCE_COLOR
+varying vec4 v_color;
+#endif
+
+void main(void)
+{
+ float logDepthOrDepth = czm_branchFreeTernary(czm_sceneMode == czm_sceneMode2D, gl_FragCoord.z, czm_unpackDepth(texture2D(czm_globeDepthTexture, gl_FragCoord.xy / czm_viewport.zw)));
+ vec3 ecStart = vec3(v_endEcAndStartEcX.w, v_texcoordNormalizationAndStartEcYZ.zw);
+
+ // Discard for sky
+ if (logDepthOrDepth == 0.0) {
+#ifdef DEBUG_SHOW_VOLUME
+ gl_FragColor = vec4(1.0, 0.0, 0.0, 0.5);
+ return;
+#else // DEBUG_SHOW_VOLUME
+ discard;
+#endif // DEBUG_SHOW_VOLUME
+ }
+
+ vec4 eyeCoordinate = czm_windowToEyeCoordinates(gl_FragCoord.xy, logDepthOrDepth);
+ eyeCoordinate /= eyeCoordinate.w;
+
+ float halfMaxWidth = v_startPlaneNormalEcAndHalfWidth.w * czm_metersPerPixel(eyeCoordinate);
+ // Check distance of the eye coordinate against the right-facing plane
+ float widthwiseDistance = czm_planeDistance(v_rightPlaneEC, eyeCoordinate.xyz);
+
+ // Check eye coordinate against the mitering planes
+ float distanceFromStart = czm_planeDistance(v_startPlaneNormalEcAndHalfWidth.xyz, -dot(ecStart, v_startPlaneNormalEcAndHalfWidth.xyz), eyeCoordinate.xyz);
+ float distanceFromEnd = czm_planeDistance(v_endPlaneNormalEcAndBatchId.xyz, -dot(v_endEcAndStartEcX.xyz, v_endPlaneNormalEcAndBatchId.xyz), eyeCoordinate.xyz);
+
+ if (abs(widthwiseDistance) > halfMaxWidth || distanceFromStart < 0.0 || distanceFromEnd < 0.0) {
+#ifdef DEBUG_SHOW_VOLUME
+ gl_FragColor = vec4(1.0, 0.0, 0.0, 0.5);
+ return;
+#else // DEBUG_SHOW_VOLUME
+ discard;
+#endif // DEBUG_SHOW_VOLUME
+ }
+
+ // Check distance of the eye coordinate against start and end planes with normals in the right plane.
+ // For computing unskewed lengthwise texture coordinate.
+ // Can also be used for clipping extremely pointy miters, but in practice unnecessary because of miter breaking.
+
+ // aligned plane: cross the right plane normal with miter plane normal, then cross the result with right again to point it more "forward"
+ vec3 alignedPlaneNormal;
+
+ // start aligned plane
+ alignedPlaneNormal = cross(v_rightPlaneEC.xyz, v_startPlaneNormalEcAndHalfWidth.xyz);
+ alignedPlaneNormal = normalize(cross(alignedPlaneNormal, v_rightPlaneEC.xyz));
+ distanceFromStart = czm_planeDistance(alignedPlaneNormal, -dot(alignedPlaneNormal, ecStart), eyeCoordinate.xyz);
+
+ // end aligned plane
+ alignedPlaneNormal = cross(v_rightPlaneEC.xyz, v_endPlaneNormalEcAndBatchId.xyz);
+ alignedPlaneNormal = normalize(cross(alignedPlaneNormal, v_rightPlaneEC.xyz));
+ distanceFromEnd = czm_planeDistance(alignedPlaneNormal, -dot(alignedPlaneNormal, v_endEcAndStartEcX.xyz), eyeCoordinate.xyz);
+
+#ifdef PER_INSTANCE_COLOR
+ gl_FragColor = v_color;
+#else // PER_INSTANCE_COLOR
+ // Clamp - distance to aligned planes may be negative due to mitering,
+ // so fragment texture coordinate might be out-of-bounds.
+ float s = clamp(distanceFromStart / (distanceFromStart + distanceFromEnd), 0.0, 1.0);
+ s = (s * v_texcoordNormalizationAndStartEcYZ.x) + v_texcoordNormalizationAndStartEcYZ.y;
+ float t = (widthwiseDistance + halfMaxWidth) / (2.0 * halfMaxWidth);
+
+ czm_materialInput materialInput;
+
+ materialInput.s = s;
+ materialInput.st = vec2(s, t);
+ materialInput.str = vec3(s, t, 0.0);
+
+ czm_material material = czm_getMaterial(materialInput);
+ gl_FragColor = vec4(material.diffuse + material.emission, material.alpha);
+#endif // PER_INSTANCE_COLOR
+}
diff --git a/Source/Shaders/PolylineShadowVolumeMorphFS.glsl b/Source/Shaders/PolylineShadowVolumeMorphFS.glsl
new file mode 100644
index 000000000000..a5995bea035b
--- /dev/null
+++ b/Source/Shaders/PolylineShadowVolumeMorphFS.glsl
@@ -0,0 +1,45 @@
+varying vec3 v_forwardDirectionEC;
+varying vec3 v_texcoordNormalizationAndHalfWidth;
+varying float v_batchId;
+
+#ifdef PER_INSTANCE_COLOR
+varying vec4 v_color;
+#else
+varying vec2 v_alignedPlaneDistances;
+varying float v_texcoordT;
+#endif
+
+float rayPlaneDistanceUnsafe(vec3 origin, vec3 direction, vec3 planeNormal, float planeDistance) {
+ // We don't expect the ray to ever be parallel to the plane
+ return (-planeDistance - dot(planeNormal, origin)) / dot(planeNormal, direction);
+}
+
+void main(void)
+{
+ vec4 eyeCoordinate = gl_FragCoord;
+ eyeCoordinate /= eyeCoordinate.w;
+
+#ifdef PER_INSTANCE_COLOR
+ gl_FragColor = v_color;
+#else // PER_INSTANCE_COLOR
+ // Use distances for planes aligned with segment to prevent skew in dashing
+ float distanceFromStart = rayPlaneDistanceUnsafe(eyeCoordinate.xyz, -v_forwardDirectionEC, v_forwardDirectionEC.xyz, v_alignedPlaneDistances.x);
+ float distanceFromEnd = rayPlaneDistanceUnsafe(eyeCoordinate.xyz, v_forwardDirectionEC, -v_forwardDirectionEC.xyz, v_alignedPlaneDistances.y);
+
+ // Clamp - distance to aligned planes may be negative due to mitering
+ distanceFromStart = max(0.0, distanceFromStart);
+ distanceFromEnd = max(0.0, distanceFromEnd);
+
+ float s = distanceFromStart / (distanceFromStart + distanceFromEnd);
+ s = (s * v_texcoordNormalizationAndHalfWidth.x) + v_texcoordNormalizationAndHalfWidth.y;
+
+ czm_materialInput materialInput;
+
+ materialInput.s = s;
+ materialInput.st = vec2(s, v_texcoordT);
+ materialInput.str = vec3(s, v_texcoordT, 0.0);
+
+ czm_material material = czm_getMaterial(materialInput);
+ gl_FragColor = vec4(material.diffuse + material.emission, material.alpha);
+#endif // PER_INSTANCE_COLOR
+}
diff --git a/Source/Shaders/PolylineShadowVolumeMorphVS.glsl b/Source/Shaders/PolylineShadowVolumeMorphVS.glsl
new file mode 100644
index 000000000000..a2a618919953
--- /dev/null
+++ b/Source/Shaders/PolylineShadowVolumeMorphVS.glsl
@@ -0,0 +1,177 @@
+attribute vec3 position3DHigh;
+attribute vec3 position3DLow;
+
+attribute vec4 startHiAndForwardOffsetX;
+attribute vec4 startLoAndForwardOffsetY;
+attribute vec4 startNormalAndForwardOffsetZ;
+attribute vec4 endNormalAndTextureCoordinateNormalizationX;
+attribute vec4 rightNormalAndTextureCoordinateNormalizationY;
+attribute vec4 startHiLo2D;
+attribute vec4 offsetAndRight2D;
+attribute vec4 startEndNormals2D;
+attribute vec2 texcoordNormalization2D;
+
+attribute float batchId;
+
+varying vec3 v_forwardDirectionEC;
+varying vec3 v_texcoordNormalizationAndHalfWidth;
+varying float v_batchId;
+
+// For materials
+#ifdef WIDTH_VARYING
+varying float v_width;
+#endif
+#ifdef ANGLE_VARYING
+varying float v_polylineAngle;
+#endif
+
+#ifdef PER_INSTANCE_COLOR
+varying vec4 v_color;
+#else
+varying vec2 v_alignedPlaneDistances;
+varying float v_texcoordT;
+#endif
+
+// Morphing planes using SLERP or NLERP doesn't seem to work, so instead draw the material directly on the shadow volume.
+// Morph views are from very far away and aren't meant to be used precisely, so this should be sufficient.
+void main()
+{
+ v_batchId = batchId;
+
+ // Start position
+ vec4 posRelativeToEye2D = czm_translateRelativeToEye(vec3(0.0, startHiLo2D.xy), vec3(0.0, startHiLo2D.zw));
+ vec4 posRelativeToEye3D = czm_translateRelativeToEye(startHiAndForwardOffsetX.xyz, startLoAndForwardOffsetY.xyz);
+ vec4 posRelativeToEye = czm_columbusViewMorph(posRelativeToEye2D, posRelativeToEye3D, czm_morphTime);
+ vec3 posEc2D = (czm_modelViewRelativeToEye * posRelativeToEye2D).xyz;
+ vec3 posEc3D = (czm_modelViewRelativeToEye * posRelativeToEye3D).xyz;
+ vec3 startEC = (czm_modelViewRelativeToEye * posRelativeToEye).xyz;
+
+ // Start plane
+ vec4 startPlane2D;
+ vec4 startPlane3D;
+ startPlane2D.xyz = czm_normal * vec3(0.0, startEndNormals2D.xy);
+ startPlane3D.xyz = czm_normal * startNormalAndForwardOffsetZ.xyz;
+ startPlane2D.w = -dot(startPlane2D.xyz, posEc2D);
+ startPlane3D.w = -dot(startPlane3D.xyz, posEc3D);
+
+ // Right plane
+ vec4 rightPlane2D;
+ vec4 rightPlane3D;
+ rightPlane2D.xyz = czm_normal * vec3(0.0, offsetAndRight2D.zw);
+ rightPlane3D.xyz = czm_normal * rightNormalAndTextureCoordinateNormalizationY.xyz;
+ rightPlane2D.w = -dot(rightPlane2D.xyz, posEc2D);
+ rightPlane3D.w = -dot(rightPlane3D.xyz, posEc3D);
+
+ // End position
+ posRelativeToEye2D = posRelativeToEye2D + vec4(0.0, offsetAndRight2D.xy, 0.0);
+ posRelativeToEye3D = posRelativeToEye3D + vec4(startHiAndForwardOffsetX.w, startLoAndForwardOffsetY.w, startNormalAndForwardOffsetZ.w, 0.0);
+ posRelativeToEye = czm_columbusViewMorph(posRelativeToEye2D, posRelativeToEye3D, czm_morphTime);
+ posEc2D = (czm_modelViewRelativeToEye * posRelativeToEye2D).xyz;
+ posEc3D = (czm_modelViewRelativeToEye * posRelativeToEye3D).xyz;
+ vec3 endEC = (czm_modelViewRelativeToEye * posRelativeToEye).xyz;
+ vec3 forwardEc3D = czm_normal * normalize(vec3(startHiAndForwardOffsetX.w, startLoAndForwardOffsetY.w, startNormalAndForwardOffsetZ.w));
+ vec3 forwardEc2D = czm_normal * normalize(vec3(0.0, offsetAndRight2D.xy));
+
+ // End plane
+ vec4 endPlane2D;
+ vec4 endPlane3D;
+ endPlane2D.xyz = czm_normal * vec3(0.0, startEndNormals2D.zw);
+ endPlane3D.xyz = czm_normal * endNormalAndTextureCoordinateNormalizationX.xyz;
+ endPlane2D.w = -dot(endPlane2D.xyz, posEc2D);
+ endPlane3D.w = -dot(endPlane3D.xyz, posEc3D);
+
+ // Forward direction
+ v_forwardDirectionEC = normalize(endEC - startEC);
+
+ vec2 cleanTexcoordNormalization2D;
+ cleanTexcoordNormalization2D.x = abs(texcoordNormalization2D.x);
+ cleanTexcoordNormalization2D.y = czm_branchFreeTernary(texcoordNormalization2D.y > 1.0, 0.0, abs(texcoordNormalization2D.y));
+ vec2 cleanTexcoordNormalization3D;
+ cleanTexcoordNormalization3D.x = abs(endNormalAndTextureCoordinateNormalizationX.w);
+ cleanTexcoordNormalization3D.y = rightNormalAndTextureCoordinateNormalizationY.w;
+ cleanTexcoordNormalization3D.y = czm_branchFreeTernary(cleanTexcoordNormalization3D.y > 1.0, 0.0, abs(cleanTexcoordNormalization3D.y));
+
+ v_texcoordNormalizationAndHalfWidth.xy = mix(cleanTexcoordNormalization2D, cleanTexcoordNormalization3D, czm_morphTime);
+
+#ifdef PER_INSTANCE_COLOR
+ v_color = czm_batchTable_color(batchId);
+#else // PER_INSTANCE_COLOR
+ // For computing texture coordinates
+
+ v_alignedPlaneDistances.x = -dot(v_forwardDirectionEC, startEC);
+ v_alignedPlaneDistances.y = -dot(-v_forwardDirectionEC, endEC);
+#endif // PER_INSTANCE_COLOR
+
+#ifdef WIDTH_VARYING
+ float width = czm_batchTable_width(batchId);
+ float halfWidth = width * 0.5;
+ v_width = width;
+ v_texcoordNormalizationAndHalfWidth.z = halfWidth;
+#else
+ float halfWidth = 0.5 * czm_batchTable_width(batchId);
+ v_texcoordNormalizationAndHalfWidth.z = halfWidth;
+#endif
+
+ // Compute a normal along which to "push" the position out, extending the miter depending on view distance.
+ // Position has already been "pushed" by unit length along miter normal, and miter normals are encoded in the planes.
+ // Decode the normal to use at this specific vertex, push the position back, and then push to where it needs to be.
+ // Since this is morphing, compute both 3D and 2D positions and then blend.
+
+ // ****** 3D ******
+ // Check distance to the end plane and start plane, pick the plane that is closer
+ vec4 positionEc3D = czm_modelViewRelativeToEye * czm_translateRelativeToEye(position3DHigh, position3DLow); // w = 1.0, see czm_computePosition
+ float absStartPlaneDistance = abs(czm_planeDistance(startPlane3D, positionEc3D.xyz));
+ float absEndPlaneDistance = abs(czm_planeDistance(endPlane3D, positionEc3D.xyz));
+ vec3 planeDirection = czm_branchFreeTernary(absStartPlaneDistance < absEndPlaneDistance, startPlane3D.xyz, endPlane3D.xyz);
+ vec3 upOrDown = normalize(cross(rightPlane3D.xyz, planeDirection)); // Points "up" for start plane, "down" at end plane.
+ vec3 normalEC = normalize(cross(planeDirection, upOrDown)); // In practice, the opposite seems to work too.
+
+ // Nudge the top vertex upwards to prevent flickering
+ vec3 geodeticSurfaceNormal = normalize(cross(normalEC, forwardEc3D));
+ geodeticSurfaceNormal *= float(0.0 <= rightNormalAndTextureCoordinateNormalizationY.w && rightNormalAndTextureCoordinateNormalizationY.w <= 1.0);
+ geodeticSurfaceNormal *= MAX_TERRAIN_HEIGHT;
+ positionEc3D.xyz += geodeticSurfaceNormal;
+
+ // Determine if this vertex is on the "left" or "right"
+ normalEC *= sign(endNormalAndTextureCoordinateNormalizationX.w);
+
+ // A "perfect" implementation would push along normals according to the angle against forward.
+ // In practice, just pushing the normal out by halfWidth is sufficient for morph views.
+ positionEc3D.xyz += halfWidth * max(0.0, czm_metersPerPixel(positionEc3D)) * normalEC; // prevent artifacts when czm_metersPerPixel is negative (behind camera)
+
+ // ****** 2D ******
+ // Check distance to the end plane and start plane, pick the plane that is closer
+ vec4 positionEc2D = czm_modelViewRelativeToEye * czm_translateRelativeToEye(position2DHigh.zxy, position2DLow.zxy); // w = 1.0, see czm_computePosition
+ absStartPlaneDistance = abs(czm_planeDistance(startPlane2D, positionEc2D.xyz));
+ absEndPlaneDistance = abs(czm_planeDistance(endPlane2D, positionEc2D.xyz));
+ planeDirection = czm_branchFreeTernary(absStartPlaneDistance < absEndPlaneDistance, startPlane2D.xyz, endPlane2D.xyz);
+ upOrDown = normalize(cross(rightPlane2D.xyz, planeDirection)); // Points "up" for start plane, "down" at end plane.
+ normalEC = normalize(cross(planeDirection, upOrDown)); // In practice, the opposite seems to work too.
+
+ // Nudge the top vertex upwards to prevent flickering
+ geodeticSurfaceNormal = normalize(cross(normalEC, forwardEc2D));
+ geodeticSurfaceNormal *= float(0.0 <= texcoordNormalization2D.y && texcoordNormalization2D.y <= 1.0);
+ geodeticSurfaceNormal *= MAX_TERRAIN_HEIGHT;
+ positionEc2D.xyz += geodeticSurfaceNormal;
+
+ // Determine if this vertex is on the "left" or "right"
+ normalEC *= sign(texcoordNormalization2D.x);
+#ifndef PER_INSTANCE_COLOR
+ // Use vertex's sidedness to compute its texture coordinate.
+ v_texcoordT = clamp(sign(texcoordNormalization2D.x), 0.0, 1.0);
+#endif
+
+ // A "perfect" implementation would push along normals according to the angle against forward.
+ // In practice, just pushing the normal out by halfWidth is sufficient for morph views.
+ positionEc2D.xyz += halfWidth * max(0.0, czm_metersPerPixel(positionEc2D)) * normalEC; // prevent artifacts when czm_metersPerPixel is negative (behind camera)
+
+ // Blend for actual position
+ gl_Position = czm_projection * mix(positionEc2D, positionEc3D, czm_morphTime);
+
+#ifdef ANGLE_VARYING
+ // Approximate relative screen space direction of the line.
+ vec2 approxLineDirection = normalize(vec2(v_forwardDirectionEC.x, -v_forwardDirectionEC.y));
+ approxLineDirection.y = czm_branchFreeTernary(approxLineDirection.x == 0.0 && approxLineDirection.y == 0.0, -1.0, approxLineDirection.y);
+ v_polylineAngle = czm_fastApproximateAtan(approxLineDirection.x, approxLineDirection.y);
+#endif
+}
diff --git a/Source/Shaders/PolylineShadowVolumeVS.glsl b/Source/Shaders/PolylineShadowVolumeVS.glsl
new file mode 100644
index 000000000000..c0ae4e2d1bee
--- /dev/null
+++ b/Source/Shaders/PolylineShadowVolumeVS.glsl
@@ -0,0 +1,168 @@
+attribute vec3 position3DHigh;
+attribute vec3 position3DLow;
+
+// In 2D and in 3D, texture coordinate normalization component signs encodes:
+// * X sign - sidedness relative to right plane
+// * Y sign - is negative OR magnitude is greater than 1.0 if vertex is on bottom of volume
+#ifndef COLUMBUS_VIEW_2D
+attribute vec4 startHiAndForwardOffsetX;
+attribute vec4 startLoAndForwardOffsetY;
+attribute vec4 startNormalAndForwardOffsetZ;
+attribute vec4 endNormalAndTextureCoordinateNormalizationX;
+attribute vec4 rightNormalAndTextureCoordinateNormalizationY;
+#else
+attribute vec4 startHiLo2D;
+attribute vec4 offsetAndRight2D;
+attribute vec4 startEndNormals2D;
+attribute vec2 texcoordNormalization2D;
+#endif
+
+attribute float batchId;
+
+varying vec4 v_startPlaneNormalEcAndHalfWidth;
+varying vec4 v_endPlaneNormalEcAndBatchId;
+varying vec4 v_rightPlaneEC;
+varying vec4 v_endEcAndStartEcX;
+varying vec4 v_texcoordNormalizationAndStartEcYZ;
+
+// For materials
+#ifdef WIDTH_VARYING
+varying float v_width;
+#endif
+#ifdef ANGLE_VARYING
+varying float v_polylineAngle;
+#endif
+
+#ifdef PER_INSTANCE_COLOR
+varying vec4 v_color;
+#endif
+
+void main()
+{
+#ifdef COLUMBUS_VIEW_2D
+ vec3 ecStart = (czm_modelViewRelativeToEye * czm_translateRelativeToEye(vec3(0.0, startHiLo2D.xy), vec3(0.0, startHiLo2D.zw))).xyz;
+
+ vec3 forwardDirectionEC = czm_normal * vec3(0.0, offsetAndRight2D.xy);
+ vec3 ecEnd = forwardDirectionEC + ecStart;
+ forwardDirectionEC = normalize(forwardDirectionEC);
+
+ // Right plane
+ v_rightPlaneEC.xyz = czm_normal * vec3(0.0, offsetAndRight2D.zw);
+ v_rightPlaneEC.w = -dot(v_rightPlaneEC.xyz, ecStart);
+
+ // start plane
+ vec4 startPlaneEC;
+ startPlaneEC.xyz = czm_normal * vec3(0.0, startEndNormals2D.xy);
+ startPlaneEC.w = -dot(startPlaneEC.xyz, ecStart);
+
+ // end plane
+ vec4 endPlaneEC;
+ endPlaneEC.xyz = czm_normal * vec3(0.0, startEndNormals2D.zw);
+ endPlaneEC.w = -dot(endPlaneEC.xyz, ecEnd);
+
+ v_texcoordNormalizationAndStartEcYZ.x = abs(texcoordNormalization2D.x);
+ v_texcoordNormalizationAndStartEcYZ.y = texcoordNormalization2D.y;
+
+#else // COLUMBUS_VIEW_2D
+ vec3 ecStart = (czm_modelViewRelativeToEye * czm_translateRelativeToEye(startHiAndForwardOffsetX.xyz, startLoAndForwardOffsetY.xyz)).xyz;
+ vec3 offset = czm_normal * vec3(startHiAndForwardOffsetX.w, startLoAndForwardOffsetY.w, startNormalAndForwardOffsetZ.w);
+ vec3 ecEnd = ecStart + offset;
+
+ vec3 forwardDirectionEC = normalize(offset);
+
+ // start plane
+ vec4 startPlaneEC;
+ startPlaneEC.xyz = czm_normal * startNormalAndForwardOffsetZ.xyz;
+ startPlaneEC.w = -dot(startPlaneEC.xyz, ecStart);
+
+ // end plane
+ vec4 endPlaneEC;
+ endPlaneEC.xyz = czm_normal * endNormalAndTextureCoordinateNormalizationX.xyz;
+ endPlaneEC.w = -dot(endPlaneEC.xyz, ecEnd);
+
+ // Right plane
+ v_rightPlaneEC.xyz = czm_normal * rightNormalAndTextureCoordinateNormalizationY.xyz;
+ v_rightPlaneEC.w = -dot(v_rightPlaneEC.xyz, ecStart);
+
+ v_texcoordNormalizationAndStartEcYZ.x = abs(endNormalAndTextureCoordinateNormalizationX.w);
+ v_texcoordNormalizationAndStartEcYZ.y = rightNormalAndTextureCoordinateNormalizationY.w;
+
+#endif // COLUMBUS_VIEW_2D
+
+ v_endEcAndStartEcX.xyz = ecEnd;
+ v_endEcAndStartEcX.w = ecStart.x;
+ v_texcoordNormalizationAndStartEcYZ.zw = ecStart.yz;
+
+#ifdef PER_INSTANCE_COLOR
+ v_color = czm_batchTable_color(batchId);
+#endif // PER_INSTANCE_COLOR
+
+ // Compute a normal along which to "push" the position out, extending the miter depending on view distance.
+ // Position has already been "pushed" by unit length along miter normal, and miter normals are encoded in the planes.
+ // Decode the normal to use at this specific vertex, push the position back, and then push to where it needs to be.
+ vec4 positionRelativeToEye = czm_computePosition();
+
+ // Check distance to the end plane and start plane, pick the plane that is closer
+ vec4 positionEC = czm_modelViewRelativeToEye * positionRelativeToEye; // w = 1.0, see czm_computePosition
+ float absStartPlaneDistance = abs(czm_planeDistance(startPlaneEC, positionEC.xyz));
+ float absEndPlaneDistance = abs(czm_planeDistance(endPlaneEC, positionEC.xyz));
+ vec3 planeDirection = czm_branchFreeTernary(absStartPlaneDistance < absEndPlaneDistance, startPlaneEC.xyz, endPlaneEC.xyz);
+ vec3 upOrDown = normalize(cross(v_rightPlaneEC.xyz, planeDirection)); // Points "up" for start plane, "down" at end plane.
+ vec3 normalEC = normalize(cross(planeDirection, upOrDown)); // In practice, the opposite seems to work too.
+
+ // Extrude bottom vertices downward for far view distances, like for GroundPrimitives
+ upOrDown = cross(forwardDirectionEC, normalEC);
+ upOrDown = float(czm_sceneMode == czm_sceneMode3D) * upOrDown;
+ upOrDown = float(v_texcoordNormalizationAndStartEcYZ.y > 1.0 || v_texcoordNormalizationAndStartEcYZ.y < 0.0) * upOrDown;
+ upOrDown = min(GLOBE_MINIMUM_ALTITUDE, czm_geometricToleranceOverMeter * length(positionRelativeToEye.xyz)) * upOrDown;
+ positionEC.xyz += upOrDown;
+
+ v_texcoordNormalizationAndStartEcYZ.y = czm_branchFreeTernary(v_texcoordNormalizationAndStartEcYZ.y > 1.0, 0.0, abs(v_texcoordNormalizationAndStartEcYZ.y));
+
+ // Determine distance along normalEC to push for a volume of appropriate width.
+ // Make volumes about double pixel width for a conservative fit - in practice the
+ // extra cost here is minimal compared to the loose volume heights.
+ //
+ // N = normalEC (guaranteed "right-facing")
+ // R = rightEC
+ // p = angle between N and R
+ // w = distance to push along R if R == N
+ // d = distance to push along N
+ //
+ // N R
+ // { \ p| } * cos(p) = dot(N, R) = w / d
+ // d\ \ | |w * d = w / dot(N, R)
+ // { \| }
+ // o---------- polyline segment ---->
+ //
+ float width = czm_batchTable_width(batchId);
+#ifdef WIDTH_VARYING
+ v_width = width;
+#endif
+
+ v_startPlaneNormalEcAndHalfWidth.xyz = startPlaneEC.xyz;
+ v_startPlaneNormalEcAndHalfWidth.w = width * 0.5;
+
+ v_endPlaneNormalEcAndBatchId.xyz = endPlaneEC.xyz;
+ v_endPlaneNormalEcAndBatchId.w = batchId;
+
+ width = width * max(0.0, czm_metersPerPixel(positionEC)); // width = distance to push along R
+ width = width / dot(normalEC, v_rightPlaneEC.xyz); // width = distance to push along N
+
+ // Determine if this vertex is on the "left" or "right"
+#ifdef COLUMBUS_VIEW_2D
+ normalEC *= sign(texcoordNormalization2D.x);
+#else
+ normalEC *= sign(endNormalAndTextureCoordinateNormalizationX.w);
+#endif
+
+ positionEC.xyz += width * normalEC;
+ gl_Position = czm_projection * positionEC;
+
+#ifdef ANGLE_VARYING
+ // Approximate relative screen space direction of the line.
+ vec2 approxLineDirection = normalize(vec2(forwardDirectionEC.x, -forwardDirectionEC.y));
+ approxLineDirection.y = czm_branchFreeTernary(approxLineDirection.x == 0.0 && approxLineDirection.y == 0.0, -1.0, approxLineDirection.y);
+ v_polylineAngle = czm_fastApproximateAtan(approxLineDirection.x, approxLineDirection.y);
+#endif
+}
diff --git a/Source/Workers/createGeometry.js b/Source/Workers/createGeometry.js
index f82872a34920..49fb0d2ca04c 100644
--- a/Source/Workers/createGeometry.js
+++ b/Source/Workers/createGeometry.js
@@ -1,11 +1,13 @@
define([
'../Core/defined',
'../Scene/PrimitivePipeline',
+ '../ThirdParty/when',
'./createTaskProcessorWorker',
'require'
], function(
defined,
PrimitivePipeline,
+ when,
createTaskProcessorWorker,
require) {
'use strict';
@@ -33,7 +35,7 @@ define([
function createGeometry(parameters, transferableObjects) {
var subTasks = parameters.subTasks;
var length = subTasks.length;
- var results = new Array(length);
+ var resultsOrPromises = new Array(length);
for (var i = 0; i < length; i++) {
var task = subTasks[i];
@@ -42,14 +44,16 @@ define([
if (defined(moduleName)) {
var createFunction = getModule(moduleName);
- results[i] = createFunction(geometry, task.offset);
+ resultsOrPromises[i] = createFunction(geometry, task.offset);
} else {
//Already created geometry
- results[i] = geometry;
+ resultsOrPromises[i] = geometry;
}
}
- return PrimitivePipeline.packCreateGeometryResults(results, transferableObjects);
+ return when.all(resultsOrPromises, function(results) {
+ return PrimitivePipeline.packCreateGeometryResults(results, transferableObjects);
+ });
}
return createTaskProcessorWorker(createGeometry);
diff --git a/Source/Workers/createGroundPolylineGeometry.js b/Source/Workers/createGroundPolylineGeometry.js
new file mode 100644
index 000000000000..d7f1a992a7f7
--- /dev/null
+++ b/Source/Workers/createGroundPolylineGeometry.js
@@ -0,0 +1,22 @@
+define([
+ '../Core/defined',
+ '../Core/GroundPolylineGeometry',
+ '../Scene/GroundPolylinePrimitive'
+ ], function(
+ defined,
+ GroundPolylineGeometry,
+ GroundPolylinePrimitive) {
+ 'use strict';
+
+ function createGroundPolylineGeometry(groundPolylineGeometry, offset) {
+ return GroundPolylinePrimitive._initializeTerrainHeightsWorker()
+ .then(function() {
+ if (defined(offset)) {
+ groundPolylineGeometry = GroundPolylineGeometry.unpack(groundPolylineGeometry, offset);
+ }
+ return GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+ });
+ }
+
+ return createGroundPolylineGeometry;
+});
diff --git a/Source/Workers/createTaskProcessorWorker.js b/Source/Workers/createTaskProcessorWorker.js
index ce0d12a5a249..fc4261f59c76 100644
--- a/Source/Workers/createTaskProcessorWorker.js
+++ b/Source/Workers/createTaskProcessorWorker.js
@@ -1,13 +1,29 @@
define([
+ '../ThirdParty/when',
'../Core/defaultValue',
'../Core/defined',
'../Core/formatError'
], function(
+ when,
defaultValue,
defined,
formatError) {
'use strict';
+ // createXXXGeometry functions may return Geometry or a Promise that resolves to Geometry
+ // if the function requires access to ApproximateTerrainHeights.
+ // For fully synchronous functions, just wrapping the function call in a `when` Promise doesn't
+ // handle errors correctly, hence try-catch
+ function callAndWrap(workerFunction, parameters, transferableObjects) {
+ var resultOrPromise;
+ try {
+ resultOrPromise = workerFunction(parameters, transferableObjects);
+ return resultOrPromise; // errors handled by Promise
+ } catch (e) {
+ return when.reject(e);
+ }
+ }
+
/**
* Creates an adapter function to allow a calculation function to operate as a Web Worker,
* paired with TaskProcessor, to receive tasks and return results.
@@ -35,54 +51,53 @@ define([
*/
function createTaskProcessorWorker(workerFunction) {
var postMessage;
- var transferableObjects = [];
- var responseMessage = {
- id : undefined,
- result : undefined,
- error : undefined
- };
return function(event) {
/*global self*/
var data = event.data;
- transferableObjects.length = 0;
- responseMessage.id = data.id;
- responseMessage.error = undefined;
- responseMessage.result = undefined;
-
- try {
- responseMessage.result = workerFunction(data.parameters, transferableObjects);
- } catch (e) {
- if (e instanceof Error) {
- // Errors can't be posted in a message, copy the properties
- responseMessage.error = {
- name : e.name,
- message : e.message,
- stack : e.stack
- };
- } else {
- responseMessage.error = e;
- }
- }
+ var transferableObjects = [];
+ var responseMessage = {
+ id : data.id,
+ result : undefined,
+ error : undefined
+ };
- if (!defined(postMessage)) {
- postMessage = defaultValue(self.webkitPostMessage, self.postMessage);
- }
+ return when(callAndWrap(workerFunction, data.parameters, transferableObjects))
+ .then(function(result) {
+ responseMessage.result = result;
+ })
+ .otherwise(function(e) {
+ if (e instanceof Error) {
+ // Errors can't be posted in a message, copy the properties
+ responseMessage.error = {
+ name : e.name,
+ message : e.message,
+ stack : e.stack
+ };
+ } else {
+ responseMessage.error = e;
+ }
+ })
+ .always(function() {
+ if (!defined(postMessage)) {
+ postMessage = defaultValue(self.webkitPostMessage, self.postMessage);
+ }
- if (!data.canTransferArrayBuffer) {
- transferableObjects.length = 0;
- }
+ if (!data.canTransferArrayBuffer) {
+ transferableObjects.length = 0;
+ }
- try {
- postMessage(responseMessage, transferableObjects);
- } catch (e) {
- // something went wrong trying to post the message, post a simpler
- // error that we can be sure will be cloneable
- responseMessage.result = undefined;
- responseMessage.error = 'postMessage failed with error: ' + formatError(e) + '\n with responseMessage: ' + JSON.stringify(responseMessage);
- postMessage(responseMessage);
- }
+ try {
+ postMessage(responseMessage, transferableObjects);
+ } catch (e) {
+ // something went wrong trying to post the message, post a simpler
+ // error that we can be sure will be cloneable
+ responseMessage.result = undefined;
+ responseMessage.error = 'postMessage failed with error: ' + formatError(e) + '\n with responseMessage: ' + JSON.stringify(responseMessage);
+ postMessage(responseMessage);
+ }
+ });
};
}
diff --git a/Specs/Core/GroundPolylineGeometrySpec.js b/Specs/Core/GroundPolylineGeometrySpec.js
new file mode 100644
index 000000000000..5a95fc1d3ff2
--- /dev/null
+++ b/Specs/Core/GroundPolylineGeometrySpec.js
@@ -0,0 +1,515 @@
+defineSuite([
+ 'Core/GroundPolylineGeometry',
+ 'Core/ApproximateTerrainHeights',
+ 'Core/Cartesian3',
+ 'Core/Cartographic',
+ 'Core/Math',
+ 'Core/Ellipsoid',
+ 'Core/GeographicProjection',
+ 'Core/WebMercatorProjection',
+ 'Specs/createPackableSpecs'
+ ], function(
+ GroundPolylineGeometry,
+ ApproximateTerrainHeights,
+ Cartesian3,
+ Cartographic,
+ CesiumMath,
+ Ellipsoid,
+ GeographicProjection,
+ WebMercatorProjection,
+ createPackableSpecs) {
+ 'use strict';
+
+ beforeAll(function() {
+ return ApproximateTerrainHeights.initialize();
+ });
+
+ afterAll(function() {
+ ApproximateTerrainHeights._initPromise = undefined;
+ ApproximateTerrainHeights._terrainHeights = undefined;
+ });
+
+ function verifyAttributeValuesIdentical(attribute) {
+ var values = attribute.values;
+ var componentsPerAttribute = attribute.componentsPerAttribute;
+ var vertexCount = values.length / componentsPerAttribute;
+ var firstVertex = values.slice(0, componentsPerAttribute);
+ var identical = true;
+ for (var i = 1; i < vertexCount; i++) {
+ var index = i * componentsPerAttribute;
+ var vertex = values.slice(index, index + componentsPerAttribute);
+ for (var j = 0; j < componentsPerAttribute; j++) {
+ if (vertex[j] !== firstVertex[j]) {
+ identical = false;
+ break;
+ }
+ }
+ }
+ expect(identical).toBe(true);
+ }
+
+ it('computes positions and additional attributes for polylines', function() {
+ var startCartographic = Cartographic.fromDegrees(0.01, 0.0);
+ var endCartographic = Cartographic.fromDegrees(0.02, 0.0);
+ var groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromRadiansArray([
+ startCartographic.longitude, startCartographic.latitude,
+ endCartographic.longitude, endCartographic.latitude
+ ]),
+ granularity : 0.0
+ });
+
+ var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ expect(geometry.indices.length).toEqual(36);
+ expect(geometry.attributes.position.values.length).toEqual(24);
+
+ var startHiAndForwardOffsetX = geometry.attributes.startHiAndForwardOffsetX;
+ var startLoAndForwardOffsetY = geometry.attributes.startLoAndForwardOffsetY;
+ var startNormalAndForwardOffsetZ = geometry.attributes.startNormalAndForwardOffsetZ;
+ var endNormalAndTextureCoordinateNormalizationX = geometry.attributes.endNormalAndTextureCoordinateNormalizationX;
+ var rightNormalAndTextureCoordinateNormalizationY = geometry.attributes.rightNormalAndTextureCoordinateNormalizationY;
+ var startHiLo2D = geometry.attributes.startHiLo2D;
+ var offsetAndRight2D = geometry.attributes.offsetAndRight2D;
+ var startEndNormals2D = geometry.attributes.startEndNormals2D;
+ var texcoordNormalization2D = geometry.attributes.texcoordNormalization2D;
+
+ // Expect each entry in the additional attributes to be identical across all vertices since this is a single segment,
+ // except endNormalAndTextureCoordinateNormalizationX and texcoordNormalization2D, which should be "sided"
+ verifyAttributeValuesIdentical(startHiAndForwardOffsetX);
+ verifyAttributeValuesIdentical(startLoAndForwardOffsetY);
+ verifyAttributeValuesIdentical(startNormalAndForwardOffsetZ);
+ verifyAttributeValuesIdentical(startHiLo2D);
+ verifyAttributeValuesIdentical(offsetAndRight2D);
+ verifyAttributeValuesIdentical(startEndNormals2D);
+
+ // Expect endNormalAndTextureCoordinateNormalizationX and texcoordNormalization2D.x to encode the "side" of the geometry
+ var i;
+ var index;
+ var values = endNormalAndTextureCoordinateNormalizationX.values;
+ for (i = 0; i < 4; i++) {
+ index = i * 4 + 3;
+ expect(Math.sign(values[index])).toEqual(1.0);
+ }
+ for (i = 4; i < 8; i++) {
+ index = i * 4 + 3;
+ expect(Math.sign(values[index])).toEqual(-1.0);
+ }
+
+ values = texcoordNormalization2D.values;
+ for (i = 0; i < 4; i++) {
+ index = i * 2;
+ expect(Math.sign(values[index])).toEqual(1.0);
+ }
+ for (i = 4; i < 8; i++) {
+ index = i * 2;
+ expect(Math.sign(values[index])).toEqual(-1.0);
+ }
+
+ // Expect rightNormalAndTextureCoordinateNormalizationY and texcoordNormalization2D.y to encode if the vertex is on the bottom
+ values = rightNormalAndTextureCoordinateNormalizationY.values;
+ expect(values[3] > 1.0).toBe(true);
+ expect(values[1 * 4 + 3] > 1.0).toBe(true);
+ expect(values[4 * 4 + 3] > 1.0).toBe(true);
+ expect(values[5 * 4 + 3] > 1.0).toBe(true);
+
+ values = texcoordNormalization2D.values;
+ expect(values[1] > 1.0).toBe(true);
+ expect(values[1 * 2 + 1] > 1.0).toBe(true);
+ expect(values[4 * 2 + 1] > 1.0).toBe(true);
+ expect(values[5 * 2 + 1] > 1.0).toBe(true);
+
+ // Line segment geometry is encoded as:
+ // - start position
+ // - offset to the end position
+ // - normal for a mitered plane at each end
+ // - a right-facing normal
+ // - parameters for localizing the position along the line to texture coordinates
+ var startPosition3D = new Cartesian3();
+ startPosition3D.x = startHiAndForwardOffsetX.values[0] + startLoAndForwardOffsetY.values[0];
+ startPosition3D.y = startHiAndForwardOffsetX.values[1] + startLoAndForwardOffsetY.values[1];
+ startPosition3D.z = startHiAndForwardOffsetX.values[2] + startLoAndForwardOffsetY.values[2];
+ var reconstructedCarto = Cartographic.fromCartesian(startPosition3D);
+ reconstructedCarto.height = 0.0;
+ expect(Cartographic.equalsEpsilon(reconstructedCarto, startCartographic, CesiumMath.EPSILON7)).toBe(true);
+
+ var endPosition3D = new Cartesian3();
+ endPosition3D.x = startPosition3D.x + startHiAndForwardOffsetX.values[3];
+ endPosition3D.y = startPosition3D.y + startLoAndForwardOffsetY.values[3];
+ endPosition3D.z = startPosition3D.z + startNormalAndForwardOffsetZ.values[3];
+ reconstructedCarto = Cartographic.fromCartesian(endPosition3D);
+ reconstructedCarto.height = 0.0;
+ expect(Cartographic.equalsEpsilon(reconstructedCarto, endCartographic, CesiumMath.EPSILON7)).toBe(true);
+
+ var startNormal3D = Cartesian3.unpack(startNormalAndForwardOffsetZ.values);
+ expect(Cartesian3.equalsEpsilon(startNormal3D, new Cartesian3(0.0, 1.0, 0.0), CesiumMath.EPSILON2)).toBe(true);
+
+ var endNormal3D = Cartesian3.unpack(endNormalAndTextureCoordinateNormalizationX.values);
+ expect(Cartesian3.equalsEpsilon(endNormal3D, new Cartesian3(0.0, -1.0, 0.0), CesiumMath.EPSILON2)).toBe(true);
+
+ var rightNormal3D = Cartesian3.unpack(rightNormalAndTextureCoordinateNormalizationY.values);
+ expect(Cartesian3.equalsEpsilon(rightNormal3D, new Cartesian3(0.0, 0.0, -1.0), CesiumMath.EPSILON2)).toBe(true);
+
+ var texcoordNormalizationX = endNormalAndTextureCoordinateNormalizationX.values[3];
+ expect(texcoordNormalizationX).toEqualEpsilon(1.0, CesiumMath.EPSILON3);
+
+ // 2D
+ var projection = new GeographicProjection();
+
+ var startPosition2D = new Cartesian3();
+ startPosition2D.x = startHiLo2D.values[0] + startHiLo2D.values[2];
+ startPosition2D.y = startHiLo2D.values[1] + startHiLo2D.values[3];
+ reconstructedCarto = projection.unproject(startPosition2D);
+ reconstructedCarto.height = 0.0;
+ expect(Cartographic.equalsEpsilon(reconstructedCarto, startCartographic, CesiumMath.EPSILON7)).toBe(true);
+
+ var endPosition2D = new Cartesian3();
+ endPosition2D.x = startPosition2D.x + offsetAndRight2D.values[0];
+ endPosition2D.y = startPosition2D.y + offsetAndRight2D.values[1];
+ reconstructedCarto = projection.unproject(endPosition2D);
+ reconstructedCarto.height = 0.0;
+ expect(Cartographic.equalsEpsilon(reconstructedCarto, endCartographic, CesiumMath.EPSILON7)).toBe(true);
+
+ var startNormal2D = new Cartesian3();
+ startNormal2D.x = startEndNormals2D.values[0];
+ startNormal2D.y = startEndNormals2D.values[1];
+ expect(Cartesian3.equalsEpsilon(startNormal2D, new Cartesian3(1.0, 0.0, 0.0), CesiumMath.EPSILON2)).toBe(true);
+
+ var endNormal2D = new Cartesian3();
+ endNormal2D.x = startEndNormals2D.values[2];
+ endNormal2D.y = startEndNormals2D.values[3];
+ expect(Cartesian3.equalsEpsilon(endNormal2D, new Cartesian3(-1.0, 0.0, 0.0), CesiumMath.EPSILON2)).toBe(true);
+
+ var rightNormal2D = new Cartesian3();
+ rightNormal2D.x = offsetAndRight2D.values[2];
+ rightNormal2D.y = offsetAndRight2D.values[3];
+ expect(Cartesian3.equalsEpsilon(rightNormal2D, new Cartesian3(0.0, -1.0, 0.0), CesiumMath.EPSILON2)).toBe(true);
+
+ texcoordNormalizationX = texcoordNormalization2D.values[0];
+ expect(texcoordNormalizationX).toEqualEpsilon(1.0, CesiumMath.EPSILON3);
+ });
+
+ it('does not generate 2D attributes when scene3DOnly is true', function() {
+ var startCartographic = Cartographic.fromDegrees(0.01, 0.0);
+ var endCartographic = Cartographic.fromDegrees(0.02, 0.0);
+ var groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromRadiansArray([
+ startCartographic.longitude, startCartographic.latitude,
+ endCartographic.longitude, endCartographic.latitude
+ ]),
+ granularity : 0.0
+ });
+
+ groundPolylineGeometry._scene3DOnly = true;
+
+ var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ expect(geometry.attributes.startHiAndForwardOffsetX).toBeDefined();
+ expect(geometry.attributes.startLoAndForwardOffsetY).toBeDefined();
+ expect(geometry.attributes.startNormalAndForwardOffsetZ).toBeDefined();
+ expect(geometry.attributes.endNormalAndTextureCoordinateNormalizationX).toBeDefined();
+ expect(geometry.attributes.rightNormalAndTextureCoordinateNormalizationY).toBeDefined();
+
+ expect(geometry.attributes.startHiLo2D).not.toBeDefined();
+ expect(geometry.attributes.offsetAndRight2D).not.toBeDefined();
+ expect(geometry.attributes.startEndNormals2D).not.toBeDefined();
+ expect(geometry.attributes.texcoordNormalization2D).not.toBeDefined();
+ });
+
+ it('miters turns', function() {
+ var groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 0.01, 0.0,
+ 0.02, 0.0,
+ 0.02, 0.01
+ ]),
+ granularity : 0.0
+ });
+
+ var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+ expect(geometry.indices.length).toEqual(72);
+ expect(geometry.attributes.position.values.length).toEqual(48);
+
+ var startNormalAndForwardOffsetZvalues = geometry.attributes.startNormalAndForwardOffsetZ.values;
+ var endNormalAndTextureCoordinateNormalizationXvalues = geometry.attributes.endNormalAndTextureCoordinateNormalizationX.values;
+
+ var miteredStartNormal = Cartesian3.unpack(startNormalAndForwardOffsetZvalues, 32);
+ var miteredEndNormal = Cartesian3.unpack(endNormalAndTextureCoordinateNormalizationXvalues, 0);
+ var reverseMiteredEndNormal = Cartesian3.multiplyByScalar(miteredEndNormal, -1.0, new Cartesian3());
+
+ expect(Cartesian3.equalsEpsilon(miteredStartNormal, reverseMiteredEndNormal, CesiumMath.EPSILON7)).toBe(true);
+
+ var approximateExpectedMiterNormal = new Cartesian3(0.0, 1.0, 1.0);
+ Cartesian3.normalize(approximateExpectedMiterNormal, approximateExpectedMiterNormal);
+ expect(Cartesian3.equalsEpsilon(approximateExpectedMiterNormal, miteredStartNormal, CesiumMath.EPSILON2)).toBe(true);
+ });
+
+ it('breaks miters for tight turns', function() {
+ var groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 0.01, 0.0,
+ 0.02, 0.0,
+ 0.01, 0.0
+ ]),
+ granularity : 0.0
+ });
+
+ var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ var startNormalAndForwardOffsetZvalues = geometry.attributes.startNormalAndForwardOffsetZ.values;
+ var endNormalAndTextureCoordinateNormalizationXvalues = geometry.attributes.endNormalAndTextureCoordinateNormalizationX.values;
+
+ var miteredStartNormal = Cartesian3.unpack(startNormalAndForwardOffsetZvalues, 32);
+ var miteredEndNormal = Cartesian3.unpack(endNormalAndTextureCoordinateNormalizationXvalues, 0);
+ var reverseMiteredEndNormal = Cartesian3.multiplyByScalar(miteredEndNormal, -1.0, new Cartesian3());
+
+ expect(Cartesian3.equalsEpsilon(miteredStartNormal, reverseMiteredEndNormal, CesiumMath.EPSILON7)).toBe(true);
+
+ var approximateExpectedMiterNormal = new Cartesian3(0.0, 1.0, 0.0);
+
+ Cartesian3.normalize(approximateExpectedMiterNormal, approximateExpectedMiterNormal);
+ expect(Cartesian3.equalsEpsilon(approximateExpectedMiterNormal, miteredStartNormal, CesiumMath.EPSILON2)).toBe(true);
+
+ // Break miter on loop end
+ groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 0.01, 0.0,
+ 0.02, 0.0,
+ 0.015, CesiumMath.EPSILON7
+ ]),
+ granularity : 0.0,
+ loop : true
+ });
+
+ geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ startNormalAndForwardOffsetZvalues = geometry.attributes.startNormalAndForwardOffsetZ.values;
+ endNormalAndTextureCoordinateNormalizationXvalues = geometry.attributes.endNormalAndTextureCoordinateNormalizationX.values;
+
+ // Check normals at loop end
+ miteredStartNormal = Cartesian3.unpack(startNormalAndForwardOffsetZvalues, 0);
+ miteredEndNormal = Cartesian3.unpack(endNormalAndTextureCoordinateNormalizationXvalues, 32 * 2);
+
+ expect(Cartesian3.equalsEpsilon(miteredStartNormal, miteredEndNormal, CesiumMath.EPSILON7)).toBe(true);
+
+ approximateExpectedMiterNormal = new Cartesian3(0.0, 1.0, 0.0);
+
+ Cartesian3.normalize(approximateExpectedMiterNormal, approximateExpectedMiterNormal);
+ expect(Cartesian3.equalsEpsilon(approximateExpectedMiterNormal, miteredStartNormal, CesiumMath.EPSILON2)).toBe(true);
+ });
+
+ it('interpolates long polyline segments', function() {
+ var groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 0.01, 0.0,
+ 0.02, 0.0
+ ]),
+ granularity : 600.0 // 0.01 to 0.02 is about 1113 meters with default ellipsoid, expect two segments
+ });
+
+ var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ expect(geometry.indices.length).toEqual(72);
+ expect(geometry.attributes.position.values.length).toEqual(48);
+
+ // Interpolate one segment but not the other
+ groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 0.01, 0.0,
+ 0.02, 0.0,
+ 0.0201, 0.0
+ ]),
+ granularity : 600.0
+ });
+
+ geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ expect(geometry.indices.length).toEqual(36 * 3);
+ expect(geometry.attributes.position.values.length).toEqual(24 * 3);
+ });
+
+ it('loops when there are enough positions and loop is specified', function() {
+ var groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 0.01, 0.0,
+ 0.02, 0.0
+ ]),
+ granularity : 0.0,
+ loop : true
+ });
+
+ // Not enough positions to loop, should still be a single segment
+ var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+ expect(geometry.indices.length).toEqual(36);
+
+ groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 0.01, 0.0,
+ 0.02, 0.0,
+ 0.02, 0.02
+ ]),
+ granularity : 0.0,
+ loop : true
+ });
+
+ // Loop should produce 3 segments
+ geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+ expect(geometry.indices.length).toEqual(108);
+ });
+
+ it('subdivides geometry across the IDL and Prime Meridian', function() {
+ // Cross PM
+ var groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ -1.0, 0.0,
+ 1.0, 0.0
+ ]),
+ granularity : 0.0 // no interpolative subdivision
+ });
+
+ var geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ expect(geometry.indices.length).toEqual(72);
+ expect(geometry.attributes.position.values.length).toEqual(48);
+
+ // Cross IDL
+ groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ -179.0, 0.0,
+ 179.0, 0.0
+ ]),
+ granularity : 0.0 // no interpolative subdivision
+ });
+
+ geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ expect(geometry.indices.length).toEqual(72);
+ expect(geometry.attributes.position.values.length).toEqual(48);
+
+ // Cross IDL going opposite direction and loop
+ groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 179.0, 0.0,
+ 179.0, 1.0,
+ -179.0, 1.0,
+ -179.0, 0.0
+ ]),
+ granularity : 0.0, // no interpolative subdivision
+ loop : true
+ });
+
+ geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ expect(geometry.indices.length).toEqual(6 * 36);
+ expect(geometry.attributes.position.values.length).toEqual(6 * 24);
+
+ // Near-IDL case
+ groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 179.999, 80.0,
+ -179.999, 80.0
+ ]),
+ granularity : 0.0 // no interpolative subdivision
+ });
+
+ geometry = GroundPolylineGeometry.createGeometry(groundPolylineGeometry);
+
+ expect(geometry.indices.length).toEqual(72);
+ expect(geometry.attributes.position.values.length).toEqual(48);
+ });
+
+ it('throws errors if not enough positions have been provided', function() {
+ expect(function() {
+ return new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ 0.01, 0.0
+ ]),
+ granularity : 0.0,
+ loop : true
+ });
+ }).toThrowDeveloperError();
+ });
+
+ it('can unpack onto an existing instance', function() {
+ var groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ -1.0, 0.0,
+ 1.0, 0.0
+ ]),
+ loop : true,
+ granularity : 10.0 // no interpolative subdivision
+ });
+ groundPolylineGeometry._scene3DOnly = true;
+
+ var packedArray = [0];
+ GroundPolylineGeometry.pack(groundPolylineGeometry, packedArray, 1);
+ var scratch = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ -1.0, 0.0,
+ 1.0, 0.0
+ ])
+ });
+ GroundPolylineGeometry.unpack(packedArray, 1, scratch);
+
+ var scratchPositions = scratch._positions;
+ expect(scratchPositions.length).toEqual(2);
+ expect(Cartesian3.equals(scratchPositions[0], groundPolylineGeometry._positions[0])).toBe(true);
+ expect(Cartesian3.equals(scratchPositions[1], groundPolylineGeometry._positions[1])).toBe(true);
+ expect(scratch.loop).toBe(true);
+ expect(scratch.granularity).toEqual(10.0);
+ expect(scratch._ellipsoid.equals(Ellipsoid.WGS84)).toBe(true);
+ expect(scratch._scene3DOnly).toBe(true);
+ });
+
+ it('provides a method for setting projection and ellipsoid', function() {
+ var groundPolylineGeometry = new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ -1.0, 0.0,
+ 1.0, 0.0
+ ]),
+ loop : true,
+ granularity : 10.0 // no interpolative subdivision
+ });
+
+ GroundPolylineGeometry.setProjectionAndEllipsoid(groundPolylineGeometry, new WebMercatorProjection(Ellipsoid.UNIT_SPHERE));
+
+ expect(groundPolylineGeometry._projectionIndex).toEqual(1);
+ expect(groundPolylineGeometry._ellipsoid.equals(Ellipsoid.UNIT_SPHERE)).toBe(true);
+ });
+
+ var positions = Cartesian3.fromDegreesArray([
+ 0.01, 0.0,
+ 0.02, 0.0,
+ 0.02, 0.1
+ ]);
+ var polyline = new GroundPolylineGeometry({
+ positions : positions,
+ granularity : 1000.0,
+ loop : true
+ });
+
+ it('projects normals that cross the IDL', function() {
+ var projection = new GeographicProjection();
+ var cartographic = new Cartographic(CesiumMath.PI - CesiumMath.EPSILON11, 0.0);
+ var normal = new Cartesian3(0.0, -1.0, 0.0);
+ var projectedPosition = projection.project(cartographic, new Cartesian3());
+ var result = new Cartesian3();
+
+ GroundPolylineGeometry._projectNormal(projection, cartographic, normal, projectedPosition, result);
+ expect(Cartesian3.equalsEpsilon(result, new Cartesian3(1.0, 0.0, 0.0), CesiumMath.EPSILON7)).toBe(true);
+ });
+
+ var packedInstance = [positions.length];
+ Cartesian3.pack(positions[0], packedInstance, packedInstance.length);
+ Cartesian3.pack(positions[1], packedInstance, packedInstance.length);
+ Cartesian3.pack(positions[2], packedInstance, packedInstance.length);
+ packedInstance.push(polyline.granularity);
+ packedInstance.push(polyline.loop ? 1.0 : 0.0);
+
+ Ellipsoid.pack(Ellipsoid.WGS84, packedInstance, packedInstance.length);
+
+ packedInstance.push(0.0); // projection index for Geographic (default)
+ packedInstance.push(0.0); // scene3DModeOnly = false
+
+ createPackableSpecs(GroundPolylineGeometry, polyline, packedInstance);
+});
diff --git a/Specs/Renderer/BuiltinFunctionsSpec.js b/Specs/Renderer/BuiltinFunctionsSpec.js
index b766adab0c8e..3ff6aae764ab 100644
--- a/Specs/Renderer/BuiltinFunctionsSpec.js
+++ b/Specs/Renderer/BuiltinFunctionsSpec.js
@@ -2,6 +2,7 @@ defineSuite([
'Core/BoundingRectangle',
'Core/Cartesian3',
'Core/Cartesian4',
+ 'Core/Math',
'Core/EncodedCartesian3',
'Specs/createCamera',
'Specs/createContext',
@@ -10,6 +11,7 @@ defineSuite([
BoundingRectangle,
Cartesian3,
Cartesian4,
+ CesiumMath,
EncodedCartesian3,
createCamera,
createContext,
@@ -155,6 +157,19 @@ defineSuite([
context : context,
fragmentShader : fs
}).contextToRender();
+
+ fs =
+ 'void main() { ' +
+ ' vec4 plane = vec4(1.0, 0.0, 0.0, 0.0); ' +
+ ' vec3 point = vec3(1.0, 0.0, 0.0); ' +
+ ' float expected = 1.0; ' +
+ ' float actual = czm_planeDistance(plane.xyz, plane.w, point); ' +
+ ' gl_FragColor = vec4(actual == expected); ' +
+ '}';
+ expect({
+ context : context,
+ fragmentShader : fs
+ }).contextToRender();
});
it('has czm_lineDistance', function() {
@@ -455,14 +470,62 @@ defineSuite([
}).contextToRender();
});
- it('has czm_branchFreeTernaryFloat', function() {
+ it('has czm_branchFreeTernary', function() {
var fs =
'void main() { ' +
- ' gl_FragColor = vec4(czm_branchFreeTernaryFloat(true, 1.0, 0.0));' +
+ ' gl_FragColor = vec4(czm_branchFreeTernary(true, 1.0, 0.0));' +
+ '}';
+ expect({
+ context : context,
+ fragmentShader : fs
+ }).contextToRender();
+
+ fs =
+ 'void main() { ' +
+ ' gl_FragColor = vec4(czm_branchFreeTernary(true, vec2(1.0), vec2(0.0)), 1.0, 1.0);' +
+ '}';
+ expect({
+ context : context,
+ fragmentShader : fs
+ }).contextToRender();
+
+ fs =
+ 'void main() { ' +
+ ' gl_FragColor = vec4(czm_branchFreeTernary(true, vec3(1.0), vec3(0.0)), 1.0);' +
+ '}';
+ expect({
+ context : context,
+ fragmentShader : fs
+ }).contextToRender();
+
+ fs =
+ 'void main() { ' +
+ ' gl_FragColor = czm_branchFreeTernary(true, vec4(1.0), vec4(0.0));' +
'}';
expect({
context : context,
fragmentShader : fs
}).contextToRender();
});
+
+ it('has czm_fastApproximateAtan', function() {
+ var fsAtan =
+ 'void main() { ' +
+ ' gl_FragColor = vec4(czm_fastApproximateAtan(0.0) == 0.0);' +
+ '}';
+ expect({
+ context : context,
+ fragmentShader : fsAtan
+ }).contextToRender();
+
+ var fsAtan2 =
+ 'void main() { ' +
+ ' gl_FragColor = vec4(czm_fastApproximateAtan(1.0, 0.0) == 0.0);' +
+ '}';
+ expect({
+ context : context,
+ fragmentShader : fsAtan2
+ }).contextToRender();
+ });
+
}, 'WebGL');
diff --git a/Specs/Scene/GroundPolylinePrimitiveSpec.js b/Specs/Scene/GroundPolylinePrimitiveSpec.js
new file mode 100644
index 000000000000..4386e892259d
--- /dev/null
+++ b/Specs/Scene/GroundPolylinePrimitiveSpec.js
@@ -0,0 +1,954 @@
+defineSuite([
+ 'Scene/GroundPolylinePrimitive',
+ 'Core/Color',
+ 'Core/ColorGeometryInstanceAttribute',
+ 'Core/Cartesian3',
+ 'Core/destroyObject',
+ 'Core/DistanceDisplayConditionGeometryInstanceAttribute',
+ 'Core/Ellipsoid',
+ 'Core/GeometryInstance',
+ 'Core/GroundPolylineGeometry',
+ 'Core/Rectangle',
+ 'Core/RectangleGeometry',
+ 'Core/ShowGeometryInstanceAttribute',
+ 'Renderer/Pass',
+ 'Scene/PerInstanceColorAppearance',
+ 'Scene/PolylineColorAppearance',
+ 'Scene/PolylineMaterialAppearance',
+ 'Scene/Primitive',
+ 'Specs/createScene',
+ 'Specs/pollToPromise'
+ ], function(
+ GroundPolylinePrimitive,
+ Color,
+ ColorGeometryInstanceAttribute,
+ Cartesian3,
+ destroyObject,
+ DistanceDisplayConditionGeometryInstanceAttribute,
+ Ellipsoid,
+ GeometryInstance,
+ GroundPolylineGeometry,
+ Rectangle,
+ RectangleGeometry,
+ ShowGeometryInstanceAttribute,
+ Pass,
+ PerInstanceColorAppearance,
+ PolylineColorAppearance,
+ PolylineMaterialAppearance,
+ Primitive,
+ createScene,
+ pollToPromise) {
+ 'use strict';
+
+ var scene;
+ var context;
+
+ var ellipsoid;
+
+ var depthColor;
+ var polylineColor;
+ var polylineColorAttribute;
+
+ var groundPolylineInstance;
+ var groundPolylinePrimitive;
+ var depthRectanglePrimitive;
+
+ var positions = Cartesian3.fromDegreesArray([
+ 0.01, 0.0,
+ 0.03, 0.0
+ ]);
+
+ var lookPosition = Cartesian3.fromDegrees(0.02, 0.0);
+
+ beforeAll(function() {
+ scene = createScene();
+ scene.postProcessStages.fxaa.enabled = false;
+
+ context = scene.context;
+
+ ellipsoid = Ellipsoid.WGS84;
+ return GroundPolylinePrimitive.initializeTerrainHeights();
+ });
+
+ afterAll(function() {
+ scene.destroyForSpecs();
+
+ // Leave ground primitive uninitialized
+ GroundPolylinePrimitive._initialized = false;
+ GroundPolylinePrimitive._initPromise = undefined;
+ });
+
+ function MockGlobePrimitive(primitive) {
+ this._primitive = primitive;
+ this.pass = Pass.GLOBE;
+ }
+
+ MockGlobePrimitive.prototype.update = function(frameState) {
+ var commandList = frameState.commandList;
+ var startLength = commandList.length;
+ this._primitive.update(frameState);
+
+ for (var i = startLength; i < commandList.length; ++i) {
+ var command = commandList[i];
+ command.pass = this.pass;
+ }
+ };
+
+ MockGlobePrimitive.prototype.isDestroyed = function() {
+ return false;
+ };
+
+ MockGlobePrimitive.prototype.destroy = function() {
+ this._primitive.destroy();
+ return destroyObject(this);
+ };
+
+ beforeEach(function() {
+ scene.morphTo3D(0);
+ scene.render(); // clear any afterRender commands
+
+ var depthpolylineColorAttribute = ColorGeometryInstanceAttribute.fromColor(new Color(0.0, 0.0, 1.0, 1.0));
+ depthColor = depthpolylineColorAttribute.value;
+ var primitive = new Primitive({
+ geometryInstances : new GeometryInstance({
+ geometry : new RectangleGeometry({
+ ellipsoid : ellipsoid,
+ rectangle : Rectangle.fromDegrees(-1.0, -1.0, 1.0, 1.0)
+ }),
+ id : 'depth rectangle',
+ attributes : {
+ color : depthpolylineColorAttribute
+ }
+ }),
+ appearance : new PerInstanceColorAppearance({
+ translucent : false,
+ flat : true
+ }),
+ asynchronous : false
+ });
+
+ // wrap rectangle primitive so it gets executed during the globe pass to lay down depth
+ depthRectanglePrimitive = new MockGlobePrimitive(primitive);
+
+ polylineColorAttribute = ColorGeometryInstanceAttribute.fromColor(new Color(0.0, 1.0, 1.0, 1.0));
+ polylineColor = polylineColorAttribute.value;
+ groundPolylineInstance = new GeometryInstance({
+ geometry : new GroundPolylineGeometry({
+ positions : positions,
+ granularity : 0.0,
+ width : 1.0,
+ loop : false,
+ ellipsoid : ellipsoid
+ }),
+ id : 'polyline on terrain',
+ attributes : {
+ color : polylineColorAttribute
+ }
+ });
+ });
+
+ afterEach(function() {
+ scene.groundPrimitives.removeAll();
+ groundPolylinePrimitive = groundPolylinePrimitive && !groundPolylinePrimitive.isDestroyed() && groundPolylinePrimitive.destroy();
+ depthRectanglePrimitive = depthRectanglePrimitive && !depthRectanglePrimitive.isDestroyed() && depthRectanglePrimitive.destroy();
+ });
+
+ it('default constructs', function() {
+ groundPolylinePrimitive = new GroundPolylinePrimitive();
+ expect(groundPolylinePrimitive.geometryInstances).not.toBeDefined();
+ expect(groundPolylinePrimitive.appearance instanceof PolylineMaterialAppearance).toBe(true);
+ expect(groundPolylinePrimitive.show).toEqual(true);
+ expect(groundPolylinePrimitive.interleave).toEqual(false);
+ expect(groundPolylinePrimitive.releaseGeometryInstances).toEqual(true);
+ expect(groundPolylinePrimitive.allowPicking).toEqual(true);
+ expect(groundPolylinePrimitive.asynchronous).toEqual(true);
+ expect(groundPolylinePrimitive.debugShowBoundingVolume).toEqual(false);
+ expect(groundPolylinePrimitive.debugShowShadowVolume).toEqual(false);
+ });
+
+ it('constructs with options', function() {
+ var geometryInstances = [];
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : geometryInstances,
+ show : false,
+ interleave : true,
+ releaseGeometryInstances : false,
+ allowPicking : false,
+ asynchronous : false,
+ debugShowBoundingVolume : true,
+ debugShowShadowVolume : true
+ });
+
+ expect(groundPolylinePrimitive.geometryInstances).toEqual(geometryInstances);
+ expect(groundPolylinePrimitive.show).toEqual(false);
+ expect(groundPolylinePrimitive.interleave).toEqual(true);
+ expect(groundPolylinePrimitive.releaseGeometryInstances).toEqual(false);
+ expect(groundPolylinePrimitive.allowPicking).toEqual(false);
+ expect(groundPolylinePrimitive.asynchronous).toEqual(false);
+ expect(groundPolylinePrimitive.debugShowBoundingVolume).toEqual(true);
+ expect(groundPolylinePrimitive.debugShowShadowVolume).toEqual(true);
+ });
+
+ it('releases geometry instances when releaseGeometryInstances is true', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ releaseGeometryInstances : true,
+ asynchronous : false
+ });
+
+ expect(groundPolylinePrimitive.geometryInstances).toBeDefined();
+ scene.groundPrimitives.add(groundPolylinePrimitive);
+ scene.renderForSpecs();
+ expect(groundPolylinePrimitive.geometryInstances).not.toBeDefined();
+ });
+
+ it('does not release geometry instances when releaseGeometryInstances is false', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ releaseGeometryInstances : false,
+ asynchronous : false
+ });
+
+ expect(groundPolylinePrimitive.geometryInstances).toBeDefined();
+ scene.groundPrimitives.add(groundPolylinePrimitive);
+ scene.renderForSpecs();
+ expect(groundPolylinePrimitive.geometryInstances).toBeDefined();
+ });
+
+ it('adds afterRender promise to frame state', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ releaseGeometryInstances : false,
+ asynchronous : false
+ });
+
+ scene.groundPrimitives.add(groundPolylinePrimitive);
+ scene.renderForSpecs();
+
+ return groundPolylinePrimitive.readyPromise.then(function(param) {
+ expect(param.ready).toBe(true);
+ });
+ });
+
+ it('does not render when geometryInstances is undefined', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : undefined,
+ asynchronous : false
+ });
+
+ var frameState = scene.frameState;
+ frameState.commandList.length = 0;
+
+ groundPolylinePrimitive.update(frameState);
+ expect(frameState.commandList.length).toEqual(0);
+ });
+
+ it('does not render when show is false', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false
+ });
+
+ var frameState = scene.frameState;
+
+ frameState.commandList.length = 0;
+ groundPolylinePrimitive.update(frameState);
+ expect(frameState.afterRender.length).toEqual(1);
+
+ frameState.afterRender[0]();
+ frameState.commandList.length = 0;
+ groundPolylinePrimitive.update(frameState);
+ expect(frameState.commandList.length).toBeGreaterThan(0);
+
+ frameState.commandList.length = 0;
+ groundPolylinePrimitive.show = false;
+ groundPolylinePrimitive.update(frameState);
+ expect(frameState.commandList.length).toEqual(0);
+ });
+
+ it('becomes ready when show is false', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = scene.groundPrimitives.add(new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance
+ }));
+ groundPolylinePrimitive.show = false;
+
+ var ready = false;
+ groundPolylinePrimitive.readyPromise.then(function() {
+ ready = true;
+ });
+
+ return pollToPromise(function() {
+ scene.render();
+ return ready;
+ }).then(function() {
+ expect(ready).toEqual(true);
+ });
+ });
+
+ it('does not render other than for the color or pick pass', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false
+ });
+
+ var frameState = scene.frameState;
+ frameState.passes.render = false;
+ frameState.passes.pick = false;
+
+ groundPolylinePrimitive.update(frameState);
+ expect(frameState.commandList.length).toEqual(0);
+ });
+
+ function verifyGroundPolylinePrimitiveRender(primitive, color) {
+ scene.camera.lookAt(lookPosition, Cartesian3.UNIT_Z);
+
+ scene.groundPrimitives.add(depthRectanglePrimitive);
+ expect(scene).toRenderAndCall(function(rgba) {
+ expect(rgba).not.toEqual([0, 0, 0, 255]);
+ expect(rgba[0]).toEqual(0);
+ });
+
+ scene.groundPrimitives.add(primitive);
+ expect(scene).toRender(color);
+ }
+
+ it('renders in 3D', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+ });
+
+ it('renders in Columbus view when scene3DOnly is false', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ scene.morphToColumbusView(0);
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+ });
+
+ it('renders in 2D when scene3DOnly is false', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ scene.morphTo2D(0);
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+ });
+
+ it('renders during morph when scene3DOnly is false', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : new GeometryInstance({
+ geometry : new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ -30, 0.0,
+ 30, 0.0
+ ]),
+ granularity : 0.0,
+ width : 1000.0,
+ loop : false,
+ ellipsoid : ellipsoid
+ }),
+ attributes : {
+ color : polylineColorAttribute
+ }
+ }),
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ // Morph to 2D first because 3D -> 2D/CV morph is difficult in single-pixel
+ scene.morphTo2D(0);
+ scene.render();
+
+ scene.morphToColumbusView(1);
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+ scene.completeMorph();
+ });
+
+ it('renders batched instances', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ var instance1 = new GeometryInstance({
+ geometry : new GroundPolylineGeometry({
+ positions : positions,
+ granularity : 0.0,
+ width : 1.0,
+ loop : false,
+ ellipsoid : ellipsoid
+ }),
+ id : 'polyline on terrain',
+ attributes : {
+ color : ColorGeometryInstanceAttribute.fromColor(new Color(1.0, 1.0, 1.0, 0.5))
+ }
+ });
+
+ var instance2 = new GeometryInstance({
+ geometry : new GroundPolylineGeometry({
+ positions : positions,
+ granularity : 0.0,
+ width : 1.0,
+ loop : false,
+ ellipsoid : ellipsoid
+ }),
+ id : 'polyline on terrain',
+ attributes : {
+ color : ColorGeometryInstanceAttribute.fromColor(new Color(1.0, 1.0, 1.0, 0.5))
+ }
+ });
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : [instance1, instance2],
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, [192, 192, 255, 255]);
+ });
+
+ it('renders bounding volume with debugShowBoundingVolume', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance(),
+ debugShowBoundingVolume : true
+ });
+
+ scene.groundPrimitives.add(groundPolylinePrimitive);
+
+ scene.camera.lookAt(lookPosition, Cartesian3.UNIT_Z);
+ expect(scene).toRenderAndCall(function(rgba) {
+ expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
+ expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
+ expect(rgba[2]).toBeGreaterThanOrEqualTo(0);
+ expect(rgba[3]).toEqual(255);
+ });
+ });
+
+ it('renders shadow volume with debugShowShadowVolume', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance(),
+ debugShowShadowVolume : true
+ });
+
+ scene.groundPrimitives.add(groundPolylinePrimitive);
+
+ scene.camera.lookAt(lookPosition, Cartesian3.UNIT_Z);
+ expect(scene).toRenderAndCall(function(rgba) {
+ expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
+ expect(rgba[1]).toBeGreaterThanOrEqualTo(0);
+ expect(rgba[2]).toBeGreaterThanOrEqualTo(0);
+ expect(rgba[3]).toEqual(255);
+ });
+ });
+
+ it('get per instance attributes', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ var attributes = groundPolylinePrimitive.getGeometryInstanceAttributes('polyline on terrain');
+ expect(attributes.color).toBeDefined();
+ });
+
+ it('modify color instance attribute', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ // Remove so it can be re-added, but don't destroy.
+ scene.groundPrimitives.destroyPrimitives = false;
+ scene.groundPrimitives.removeAll();
+ scene.groundPrimitives.destroyPrimitives = true;
+
+ var newColor = [255, 255, 255, 255];
+ var attributes = groundPolylinePrimitive.getGeometryInstanceAttributes('polyline on terrain');
+ expect(attributes.color).toBeDefined();
+ attributes.color = newColor;
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, newColor);
+ });
+
+ it('adds width instance attribute', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ scene.groundPrimitives.destroyPrimitives = false;
+ scene.groundPrimitives.removeAll();
+ scene.groundPrimitives.destroyPrimitives = true;
+
+ var attributes = groundPolylinePrimitive.getGeometryInstanceAttributes('polyline on terrain');
+ expect(attributes.width).toBeDefined();
+ attributes.width = [0];
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, depthColor);
+ });
+
+ it('modify show instance attribute', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylineInstance.attributes.show = new ShowGeometryInstanceAttribute(true);
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ scene.groundPrimitives.destroyPrimitives = false;
+ scene.groundPrimitives.removeAll();
+ scene.groundPrimitives.destroyPrimitives = true;
+
+ var attributes = groundPolylinePrimitive.getGeometryInstanceAttributes('polyline on terrain');
+ expect(attributes.show).toBeDefined();
+ attributes.show = [0];
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, depthColor);
+ });
+
+ it('renders with distance display condition per instance attribute', function() {
+ if (!context.floatingPointTexture) {
+ return;
+ }
+
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ var near = 10000.0;
+ var far = 1000000.0;
+
+ var geometryInstance = new GeometryInstance({
+ geometry : new GroundPolylineGeometry({
+ positions : positions,
+ granularity : 0.0,
+ width : 1.0,
+ loop : false,
+ ellipsoid : ellipsoid
+ }),
+ id : 'polyline on terrain',
+ attributes : {
+ distanceDisplayCondition : new DistanceDisplayConditionGeometryInstanceAttribute(near, far)
+ }
+ });
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : geometryInstance,
+ asynchronous : false
+ });
+
+ scene.groundPrimitives.add(depthRectanglePrimitive);
+ scene.groundPrimitives.add(groundPolylinePrimitive);
+ scene.camera.lookAt(lookPosition, Cartesian3.UNIT_Z);
+ scene.renderForSpecs();
+
+ var boundingSphere = groundPolylinePrimitive.getGeometryInstanceAttributes('polyline on terrain').boundingSphere;
+ var center = boundingSphere.center;
+ var radius = boundingSphere.radius;
+
+ scene.camera.lookAt(center, new Cartesian3(0.0, 0.0, radius));
+ expect(scene).toRender(depthColor);
+
+ scene.camera.lookAt(center, new Cartesian3(0.0, 0.0, radius + near + 1.0));
+ expect(scene).not.toRender(depthColor);
+
+ scene.camera.lookAt(center, new Cartesian3(0.0, 0.0, radius + far + 1.0));
+ expect(scene).toRender(depthColor);
+ });
+
+ it('getGeometryInstanceAttributes returns same object each time', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylineInstance.attributes.show = new ShowGeometryInstanceAttribute(true);
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ var attributes = groundPolylinePrimitive.getGeometryInstanceAttributes('polyline on terrain');
+ var attributes2 = groundPolylinePrimitive.getGeometryInstanceAttributes('polyline on terrain');
+ expect(attributes).toBe(attributes2);
+ });
+
+ it('picking in 3D', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ expect(scene).toPickAndCall(function(result) {
+ expect(result.id).toEqual('polyline on terrain');
+ });
+ });
+
+ it('picking in 2D', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ scene.morphTo2D(0);
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ expect(scene).toPickAndCall(function(result) {
+ expect(result.id).toEqual('polyline on terrain');
+ });
+ });
+
+ it('picking in Columbus View', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ scene.morphToColumbusView(0);
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ expect(scene).toPickAndCall(function(result) {
+ expect(result.id).toEqual('polyline on terrain');
+ });
+ });
+
+ it('picking in Morph', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : new GeometryInstance({
+ geometry : new GroundPolylineGeometry({
+ positions : Cartesian3.fromDegreesArray([
+ -30, 0.0,
+ 30, 0.0
+ ]),
+ granularity : 0.0,
+ width : 1000.0,
+ loop : false,
+ ellipsoid : ellipsoid
+ }),
+ attributes : {
+ color : polylineColorAttribute
+ },
+ id : 'big polyline on terrain'
+ }),
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ // Morph to 2D first because 3D -> 2D/CV morph is difficult in single-pixel
+ scene.morphTo2D(0);
+ scene.render();
+
+ scene.morphToColumbusView(1);
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ expect(scene).toPickAndCall(function(result) {
+ expect(result.id).toEqual('big polyline on terrain');
+ });
+ scene.completeMorph();
+ });
+
+ it('does not pick when allowPicking is false', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ allowPicking : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ expect(scene).toPickAndCall(function(result) {
+ expect(result.id).toEqual('depth rectangle');
+ });
+ });
+
+ it('update throws when batched instance colors are missing', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : new GeometryInstance({
+ geometry : new GroundPolylineGeometry({
+ positions : positions
+ })
+ }),
+ appearance : new PolylineColorAppearance(),
+ asynchronous : false
+ });
+
+ expect(function() {
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+ }).toThrowDeveloperError();
+ });
+
+ it('setting per instance attribute throws when value is undefined', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ var attributes = groundPolylinePrimitive.getGeometryInstanceAttributes('polyline on terrain');
+
+ expect(function() {
+ attributes.color = undefined;
+ }).toThrowDeveloperError();
+ });
+
+ it('can disable picking when asynchronous', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : true,
+ allowPicking : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ var frameState = scene.frameState;
+
+ return pollToPromise(function() {
+ groundPolylinePrimitive.update(frameState);
+ for (var i = 0; i < frameState.afterRender.length; ++i) {
+ frameState.afterRender[i]();
+ }
+ return groundPolylinePrimitive.ready;
+ }).then(function() {
+ var attributes = groundPolylinePrimitive.getGeometryInstanceAttributes('polyline on terrain');
+ expect(function() {
+ attributes.color = undefined;
+ }).toThrowDeveloperError();
+ });
+ });
+
+ it('getGeometryInstanceAttributes throws without id', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+
+ expect(function() {
+ groundPolylinePrimitive.getGeometryInstanceAttributes();
+ }).toThrowDeveloperError();
+ });
+
+ it('getGeometryInstanceAttributes returns undefined if id does not exist', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false,
+ appearance : new PolylineColorAppearance()
+ });
+
+ expect(function() {
+ groundPolylinePrimitive.getGeometryInstanceAttributes('unknown');
+ }).toThrowDeveloperError();
+ });
+
+ it('isDestroyed', function() {
+ groundPolylinePrimitive = new GroundPolylinePrimitive();
+ expect(groundPolylinePrimitive.isDestroyed()).toEqual(false);
+ groundPolylinePrimitive.destroy();
+ expect(groundPolylinePrimitive.isDestroyed()).toEqual(true);
+ });
+
+ it('renders when using asynchronous pipeline', function() {
+ if (!GroundPolylinePrimitive.isSupported(scene)) {
+ return;
+ }
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : true,
+ appearance : new PolylineColorAppearance()
+ });
+
+ var frameState = scene.frameState;
+
+ return pollToPromise(function() {
+ groundPolylinePrimitive.update(frameState);
+ for (var i = 0; i < frameState.afterRender.length; ++i) {
+ frameState.afterRender[i]();
+ }
+ return groundPolylinePrimitive.ready;
+ }).then(function() {
+ verifyGroundPolylinePrimitiveRender(groundPolylinePrimitive, polylineColor);
+ });
+ });
+
+ it('destroy before asynchronous pipeline is complete', function() {
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : true,
+ appearance : new PolylineColorAppearance()
+ });
+
+ var frameState = scene.frameState;
+ groundPolylinePrimitive.update(frameState);
+
+ groundPolylinePrimitive.destroy();
+ expect(groundPolylinePrimitive.isDestroyed()).toEqual(true);
+ });
+
+ it('creating a synchronous primitive throws if initializeTerrainHeights wasn\'t called', function() {
+ // Make it seem like initializeTerrainHeights was never called
+ var initPromise = GroundPolylinePrimitive._initPromise;
+ GroundPolylinePrimitive._initPromise = undefined;
+ GroundPolylinePrimitive._initialized = false;
+
+ groundPolylinePrimitive = new GroundPolylinePrimitive({
+ geometryInstances : groundPolylineInstance,
+ asynchronous : false
+ });
+
+ if (GroundPolylinePrimitive.isSupported(scene)) {
+ expect(function() {
+ groundPolylinePrimitive.update(scene.frameState);
+ }).toThrowDeveloperError();
+ }
+
+ // Set back to initialized state
+ GroundPolylinePrimitive._initPromise = initPromise;
+ GroundPolylinePrimitive._initialized = true;
+ });
+}, 'WebGL');
diff --git a/Specs/Scene/GroundPrimitiveSpec.js b/Specs/Scene/GroundPrimitiveSpec.js
index bc5effd5f25c..dff18f65ca86 100644
--- a/Specs/Scene/GroundPrimitiveSpec.js
+++ b/Specs/Scene/GroundPrimitiveSpec.js
@@ -79,8 +79,6 @@ defineSuite([
// Leave ground primitive uninitialized
GroundPrimitive._initialized = false;
GroundPrimitive._initPromise = undefined;
- ApproximateTerrainHeights._initPromise = undefined;
- ApproximateTerrainHeights._terrainHeights = undefined;
});
function MockGlobePrimitive(primitive) {