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...

+
+ + + + + +
Width + + +
+
+ + + 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) {