diff --git a/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.html b/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.html new file mode 100644 index 000000000000..659492fb649f --- /dev/null +++ b/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.html @@ -0,0 +1,279 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + Show bounding volume + Enable edge styling +
+ + + + diff --git a/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.jpg b/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.jpg new file mode 100644 index 000000000000..a5098bed32e4 Binary files /dev/null and b/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.jpg differ diff --git a/Apps/Sandcastle/gallery/Clipping Planes.html b/Apps/Sandcastle/gallery/Clipping Planes.html deleted file mode 100644 index 16ff8c79c49b..000000000000 --- a/Apps/Sandcastle/gallery/Clipping Planes.html +++ /dev/null @@ -1,212 +0,0 @@ - - - - - - - - - Cesium Demo - - - - - - -
-

Loading...

-
- - - - - - - - - - - - - - - - - -
Type
x - - -
y - - -
z - - -
- Enable clipping planes - Show debug boundingVolume -
- - - - diff --git a/Apps/Sandcastle/gallery/Clipping Planes.jpg b/Apps/Sandcastle/gallery/Clipping Planes.jpg deleted file mode 100644 index b8b63c0307b4..000000000000 Binary files a/Apps/Sandcastle/gallery/Clipping Planes.jpg and /dev/null differ diff --git a/Apps/Sandcastle/gallery/Plane.html b/Apps/Sandcastle/gallery/Plane.html new file mode 100644 index 000000000000..2e4c6e8cef46 --- /dev/null +++ b/Apps/Sandcastle/gallery/Plane.html @@ -0,0 +1,77 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + diff --git a/Apps/Sandcastle/gallery/Plane.jpg b/Apps/Sandcastle/gallery/Plane.jpg new file mode 100644 index 000000000000..4fcb0355e641 Binary files /dev/null and b/Apps/Sandcastle/gallery/Plane.jpg differ diff --git a/Apps/Sandcastle/gallery/Terrain Clipping Planes.html b/Apps/Sandcastle/gallery/Terrain Clipping Planes.html new file mode 100644 index 000000000000..0570579157cc --- /dev/null +++ b/Apps/Sandcastle/gallery/Terrain Clipping Planes.html @@ -0,0 +1,110 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ Globe clipping planes enabled +
+ + + diff --git a/Apps/Sandcastle/gallery/Terrain Clipping Planes.jpg b/Apps/Sandcastle/gallery/Terrain Clipping Planes.jpg new file mode 100644 index 000000000000..d5c3198863c4 Binary files /dev/null and b/Apps/Sandcastle/gallery/Terrain Clipping Planes.jpg differ diff --git a/CHANGES.md b/CHANGES.md index 8dbc228e74bc..6efaddfb0013 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,8 +3,11 @@ Change Log ## TODO release -* Added `clippingPlanes` property to `ModelGraphics` and `Cesium3DTileset`, which specifies an array of planes to clip the object. [TODO]() -* Added `Plane.transformPlane` function to apply a transformation to a plane. [TODO]() +* Added `clippingPlanes` property to `ModelGraphics` and `Cesium3DTileset`, which specifies a `ClippingPlanesCollection` to clip the object. [#5913](https://github.com/AnalyticalGraphicsInc/cesium/pull/5913) +* Added `terrainClippingPlanes` property to `Globe` which specifies a `ClippingPlanesCollection` to clip the terrain. [#5996](https://github.com/AnalyticalGraphicsInc/cesium/pull/5996) +* Added `Plane.transformPlane` function to apply a transformation to a plane. [#5966](https://github.com/AnalyticalGraphicsInc/cesium/pull/5966) +* Added `PlaneGeometry`, `PlaneOutlineGeometry`, and `PlaneOutlineGeometryUpdater` classes to render plane primitives. [#5996](https://github.com/AnalyticalGraphicsInc/cesium/pull/5996) +* Added `PlaneGraphics` class and `plane` property to `Entity`. [#5996](https://github.com/AnalyticalGraphicsInc/cesium/pull/5996) ### 1.38 - 2017-10-02 diff --git a/Source/Core/PlaneGeometry.js b/Source/Core/PlaneGeometry.js new file mode 100644 index 000000000000..c46c9ad8df6b --- /dev/null +++ b/Source/Core/PlaneGeometry.js @@ -0,0 +1,380 @@ +define([ + './BoundingSphere', + './Cartesian3', + './Check', + './ComponentDatatype', + './defaultValue', + './defined', + './Geometry', + './GeometryAttribute', + './GeometryAttributes', + './PrimitiveType', + './VertexFormat' + ], function( + BoundingSphere, + Cartesian3, + Check, + ComponentDatatype, + defaultValue, + defined, + Geometry, + GeometryAttribute, + GeometryAttributes, + PrimitiveType, + VertexFormat) { + 'use strict'; + + /** + * Describes geometry representing a plane centered at the origin, with a unit width and length. + * + * @alias PlaneGeometry + * @constructor + * + * @param {Object} options Object with the following properties: + * @param {VertexFormat} [options.vertexFormat=VertexFormat.DEFAULT] The vertex attributes to be computed. + * + * @example + * var planeGeometry = new Cesium.PlaneGeometry({ + * vertexFormat : Cesium.VertexFormat.POSITION_ONLY + * }); + */ + function PlaneGeometry(options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + + var vertexFormat = defaultValue(options.vertexFormat, VertexFormat.DEFAULT); + + this._vertexFormat = vertexFormat; + this._workerName = 'createPlaneGeometry'; + } + + /** + * The number of elements used to pack the object into an array. + * @type {Number} + */ + PlaneGeometry.packedLength = VertexFormat.packedLength; + + /** + * Stores the provided instance into the provided array. + * + * @param {PlaneGeometry} 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 + */ + PlaneGeometry.pack = function(value, array, startingIndex) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('value', value); + Check.defined('array', array); + //>>includeEnd('debug'); + + startingIndex = defaultValue(startingIndex, 0); + + VertexFormat.pack(value._vertexFormat, array, startingIndex); + + return array; + }; + + var scratchVertexFormat = new VertexFormat(); + var scratchOptions = { + vertexFormat: scratchVertexFormat + }; + + /** + * 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 {PlaneGeometry} [result] The object into which to store the result. + * @returns {PlaneGeometry} The modified result parameter or a new PlaneGeometry instance if one was not provided. + */ + PlaneGeometry.unpack = function(array, startingIndex, result) { + //>>includeStart('debug', pragmas.debug); + Check.defined('array', array); + //>>includeEnd('debug'); + + startingIndex = defaultValue(startingIndex, 0); + + var vertexFormat = VertexFormat.unpack(array, startingIndex, scratchVertexFormat); + + if (!defined(result)) { + return new PlaneGeometry(scratchOptions); + } + + result._vertexFormat = VertexFormat.clone(vertexFormat, result._vertexFormat); + + return result; + }; + + /** + * Computes the geometric representation of a plane, including its vertices, indices, and a bounding sphere. + * + * @param {PlaneGeometry} planeGeometry A description of the plane. + * @returns {Geometry|undefined} The computed vertices and indices. + */ + PlaneGeometry.createGeometry = function(planeGeometry) { + var vertexFormat = planeGeometry._vertexFormat; + + var min = new Cartesian3(-0.5, -0.5, 0.0); + var max = new Cartesian3( 0.5, 0.5, 0.0); + + var attributes = new GeometryAttributes(); + var indices; + var positions; + + if (vertexFormat.position && + (vertexFormat.st || vertexFormat.normal || vertexFormat.tangent || vertexFormat.bitangent)) { + if (vertexFormat.position) { + // 4 corner points. Duplicated 3 times each for each incident edge/face. + positions = new Float64Array(2 * 4 * 3); + + // +z face + positions[0] = min.x; + positions[1] = min.y; + positions[2] = 0.0; + positions[3] = max.x; + positions[4] = min.y; + positions[5] = 0.0; + positions[6] = max.x; + positions[7] = max.y; + positions[8] = 0.0; + positions[9] = min.x; + positions[10] = max.y; + positions[11] = 0.0; + + // -z face + positions[12] = min.x; + positions[13] = min.y; + positions[14] = 0.0; + positions[15] = max.x; + positions[16] = min.y; + positions[17] = 0.0; + positions[18] = max.x; + positions[19] = max.y; + positions[20] = 0.0; + positions[21] = min.x; + positions[22] = max.y; + positions[23] = 0.0; + + attributes.position = new GeometryAttribute({ + componentDatatype : ComponentDatatype.DOUBLE, + componentsPerAttribute : 3, + values : positions + }); + } + + if (vertexFormat.normal) { + var normals = new Float32Array(2 * 4 * 3); + + // +z face + normals[0] = 0.0; + normals[1] = 0.0; + normals[2] = 1.0; + normals[3] = 0.0; + normals[4] = 0.0; + normals[5] = 1.0; + normals[6] = 0.0; + normals[7] = 0.0; + normals[8] = 1.0; + normals[9] = 0.0; + normals[10] = 0.0; + normals[11] = 1.0; + + // -z face + normals[12] = 0.0; + normals[13] = 0.0; + normals[14] = -1.0; + normals[15] = 0.0; + normals[16] = 0.0; + normals[17] = -1.0; + normals[18] = 0.0; + normals[19] = 0.0; + normals[20] = -1.0; + normals[21] = 0.0; + normals[22] = 0.0; + normals[23] = -1.0; + + attributes.normal = new GeometryAttribute({ + componentDatatype : ComponentDatatype.FLOAT, + componentsPerAttribute : 3, + values : normals + }); + } + + if (vertexFormat.st) { + var texCoords = new Float32Array(2 * 4 * 2); + + // +z face + texCoords[0] = 0.0; + texCoords[1] = 0.0; + texCoords[2] = 1.0; + texCoords[3] = 0.0; + texCoords[4] = 1.0; + texCoords[5] = 1.0; + texCoords[6] = 0.0; + texCoords[7] = 1.0; + + // -z face + texCoords[8] = 1.0; + texCoords[9] = 0.0; + texCoords[10] = 0.0; + texCoords[11] = 0.0; + texCoords[12] = 0.0; + texCoords[13] = 1.0; + texCoords[14] = 1.0; + texCoords[15] = 1.0; + + attributes.st = new GeometryAttribute({ + componentDatatype : ComponentDatatype.FLOAT, + componentsPerAttribute : 2, + values : texCoords + }); + } + + if (vertexFormat.tangent) { + var tangents = new Float32Array(2 * 4 * 3); + + // +z face + tangents[0] = 1.0; + tangents[1] = 0.0; + tangents[2] = 0.0; + tangents[3] = 1.0; + tangents[4] = 0.0; + tangents[5] = 0.0; + tangents[6] = 1.0; + tangents[7] = 0.0; + tangents[8] = 0.0; + tangents[9] = 1.0; + tangents[10] = 0.0; + tangents[11] = 0.0; + + // -z face + tangents[12] = -1.0; + tangents[13] = 0.0; + tangents[14] = 0.0; + tangents[15] = -1.0; + tangents[16] = 0.0; + tangents[17] = 0.0; + tangents[18] = -1.0; + tangents[19] = 0.0; + tangents[20] = 0.0; + tangents[21] = -1.0; + tangents[22] = 0.0; + tangents[23] = 0.0; + + attributes.tangent = new GeometryAttribute({ + componentDatatype : ComponentDatatype.FLOAT, + componentsPerAttribute : 3, + values : tangents + }); + } + + if (vertexFormat.bitangent) { + var bitangents = new Float32Array(2 * 4 * 3); + + // +z face + bitangents[0] = 0.0; + bitangents[1] = 1.0; + bitangents[2] = 0.0; + bitangents[3] = 0.0; + bitangents[4] = 1.0; + bitangents[5] = 0.0; + bitangents[6] = 0.0; + bitangents[7] = 1.0; + bitangents[8] = 0.0; + bitangents[9] = 0.0; + bitangents[10] = 1.0; + bitangents[11] = 0.0; + + // -z face + bitangents[12] = 0.0; + bitangents[13] = 1.0; + bitangents[14] = 0.0; + bitangents[15] = 0.0; + bitangents[16] = 1.0; + bitangents[17] = 0.0; + bitangents[18] = 0.0; + bitangents[19] = 1.0; + bitangents[20] = 0.0; + bitangents[21] = 0.0; + bitangents[22] = 1.0; + bitangents[23] = 0.0; + + attributes.bitangent = new GeometryAttribute({ + componentDatatype : ComponentDatatype.FLOAT, + componentsPerAttribute : 3, + values : bitangents + }); + } + + // 4 triangles: 2 faces, 2 triangles each. + indices = new Uint16Array(2 * 2 * 3); + + // +z face + indices[0] = 0; + indices[1] = 1; + indices[2] = 2; + indices[3] = 0; + indices[4] = 2; + indices[5] = 3; + + // -z face + indices[6] = 4 + 2; + indices[7] = 4 + 1; + indices[8] = 4 + 0; + indices[9] = 4 + 3; + indices[10] = 4 + 2; + indices[11] = 4 + 0; + } else { + // Positions only - no need to duplicate corner points + positions = new Float64Array(4 * 3); + + positions[0] = min.x; + positions[1] = min.y; + positions[2] = min.z; + positions[3] = max.x; + positions[4] = min.y; + positions[5] = min.z; + positions[6] = max.x; + positions[7] = max.y; + positions[8] = min.z; + positions[9] = min.x; + positions[10] = max.y; + positions[11] = min.z; + + attributes.position = new GeometryAttribute({ + componentDatatype : ComponentDatatype.DOUBLE, + componentsPerAttribute : 3, + values : positions + }); + + // 12 triangles: 2 faces, 2 triangles each. + indices = new Uint16Array(2 * 2 * 3); + + // plane z = corner.Z + indices[0] = 4; + indices[1] = 5; + indices[2] = 6; + indices[3] = 4; + indices[4] = 6; + indices[5] = 7; + + // plane z = -corner.Z + indices[6] = 1; + indices[7] = 0; + indices[8] = 3; + indices[9] = 1; + indices[10] = 3; + indices[11] = 2; + } + + return new Geometry({ + attributes : attributes, + indices : indices, + primitiveType : PrimitiveType.TRIANGLES, + boundingSphere : new BoundingSphere(Cartesian3.ZERO, Math.sqrt(2.0)/2.0) + }); + }; + + return PlaneGeometry; +}); diff --git a/Source/Core/PlaneOutlineGeometry.js b/Source/Core/PlaneOutlineGeometry.js new file mode 100644 index 000000000000..29f6cf0d3654 --- /dev/null +++ b/Source/Core/PlaneOutlineGeometry.js @@ -0,0 +1,129 @@ +define([ + './BoundingSphere', + './Cartesian3', + './Check', + './ComponentDatatype', + './defaultValue', + './defined', + './Geometry', + './GeometryAttribute', + './GeometryAttributes', + './PrimitiveType' + ], function( + BoundingSphere, + Cartesian3, + Check, + ComponentDatatype, + defaultValue, + defined, + Geometry, + GeometryAttribute, + GeometryAttributes, + PrimitiveType) { + 'use strict'; + + /** + * Describes geometry representing the outline of a plane centered at the origin, with a unit width and length. + * + * @alias PlaneOutlineGeometry + * @constructor + * + */ + function PlaneOutlineGeometry() { + this._workerName = 'createPlaneOutlineGeometry'; + } + + /** + * The number of elements used to pack the object into an array. + * @type {Number} + */ + PlaneOutlineGeometry.packedLength = 0; + + /** + * Stores the provided instance into the provided array. + * + * @param {PlaneOutlineGeometry} value The value to pack. + * @param {Number[]} array The array to pack into. + * + * @returns {Number[]} The array that was packed into + */ + PlaneOutlineGeometry.pack = function(value, array) { + //>>includeStart('debug', pragmas.debug); + Check.defined('value', value); + Check.defined('array', array); + //>>includeEnd('debug'); + + 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 {PlaneOutlineGeometry} [result] The object into which to store the result. + * @returns {PlaneOutlineGeometry} The modified result parameter or a new PlaneOutlineGeometry instance if one was not provided. + */ + PlaneOutlineGeometry.unpack = function(array, startingIndex, result) { + //>>includeStart('debug', pragmas.debug); + Check.defined('array', array); + //>>includeEnd('debug'); + + if (!defined(result)) { + return new PlaneOutlineGeometry(); + } + + return result; + }; + + /** + * Computes the geometric representation of an outline of a plane, including its vertices, indices, and a bounding sphere. + * + * @returns {Geometry|undefined} The computed vertices and indices. + */ + PlaneOutlineGeometry.createGeometry = function() { + var min = new Cartesian3(-0.5, -0.5, 0.0); + var max = new Cartesian3( 0.5, 0.5, 0.0); + + var attributes = new GeometryAttributes(); + var indices = new Uint16Array(4 * 2); + var positions = new Float64Array(4 * 3); + + positions[0] = min.x; + positions[1] = min.y; + positions[2] = min.z; + positions[3] = max.x; + positions[4] = min.y; + positions[5] = min.z; + positions[6] = max.x; + positions[7] = max.y; + positions[8] = min.z; + positions[9] = min.x; + positions[10] = max.y; + positions[11] = min.z; + + attributes.position = new GeometryAttribute({ + componentDatatype : ComponentDatatype.DOUBLE, + componentsPerAttribute : 3, + values : positions + }); + + indices[0] = 0; + indices[1] = 1; + indices[2] = 1; + indices[3] = 2; + indices[4] = 2; + indices[5] = 3; + indices[6] = 3; + indices[7] = 0; + + return new Geometry({ + attributes : attributes, + indices : indices, + primitiveType : PrimitiveType.LINES, + boundingSphere : new BoundingSphere(Cartesian3.ZERO, Math.sqrt(2.0)) + }); + }; + + return PlaneOutlineGeometry; +}); diff --git a/Source/DataSources/DataSourceDisplay.js b/Source/DataSources/DataSourceDisplay.js index 98d243669d77..977845700c24 100644 --- a/Source/DataSources/DataSourceDisplay.js +++ b/Source/DataSources/DataSourceDisplay.js @@ -20,6 +20,7 @@ define([ './LabelVisualizer', './ModelVisualizer', './PathVisualizer', + './PlaneGeometryUpdater', './PointVisualizer', './PolygonGeometryUpdater', './PolylineGeometryUpdater', @@ -48,6 +49,7 @@ define([ LabelVisualizer, ModelVisualizer, PathVisualizer, + PlaneGeometryUpdater, PointVisualizer, PolygonGeometryUpdater, PolylineGeometryUpdater, @@ -114,6 +116,7 @@ define([ new GeometryVisualizer(CorridorGeometryUpdater, scene, entities), new GeometryVisualizer(EllipseGeometryUpdater, scene, entities), new GeometryVisualizer(EllipsoidGeometryUpdater, scene, entities), + new GeometryVisualizer(PlaneGeometryUpdater, scene, entities), new GeometryVisualizer(PolygonGeometryUpdater, scene, entities), new GeometryVisualizer(PolylineGeometryUpdater, scene, entities), new GeometryVisualizer(PolylineVolumeGeometryUpdater, scene, entities), diff --git a/Source/DataSources/Entity.js b/Source/DataSources/Entity.js index 42c634f3b1b5..b487bdece44c 100644 --- a/Source/DataSources/Entity.js +++ b/Source/DataSources/Entity.js @@ -23,6 +23,7 @@ define([ './LabelGraphics', './ModelGraphics', './PathGraphics', + './PlaneGraphics', './PointGraphics', './PolygonGraphics', './PolylineGraphics', @@ -56,6 +57,7 @@ define([ LabelGraphics, ModelGraphics, PathGraphics, + PlaneGraphics, PointGraphics, PolygonGraphics, PolylineGraphics, @@ -109,6 +111,7 @@ define([ * @param {LabelGraphics} [options.label] A options.label to associate with this entity. * @param {ModelGraphics} [options.model] A model to associate with this entity. * @param {PathGraphics} [options.path] A path to associate with this entity. + * @param {PlaneGraphics} [options.plane] A plane to associate with this entity. * @param {PointGraphics} [options.point] A point to associate with this entity. * @param {PolygonGraphics} [options.polygon] A polygon to associate with this entity. * @param {PolylineGraphics} [options.polyline] A polyline to associate with this entity. @@ -134,7 +137,7 @@ define([ this._show = defaultValue(options.show, true); this._parent = undefined; this._propertyNames = ['billboard', 'box', 'corridor', 'cylinder', 'description', 'ellipse', // - 'ellipsoid', 'label', 'model', 'orientation', 'path', 'point', 'polygon', // + 'ellipsoid', 'label', 'model', 'orientation', 'path', 'plane', 'point', 'polygon', // 'polyline', 'polylineVolume', 'position', 'properties', 'rectangle', 'viewFrom', 'wall']; this._billboard = undefined; @@ -159,6 +162,8 @@ define([ this._orientationSubscription = undefined; this._path = undefined; this._pathSubscription = undefined; + this._plane = undefined; + this._planeSubscription = undefined; this._point = undefined; this._pointSubscription = undefined; this._polygon = undefined; @@ -398,6 +403,12 @@ define([ * @type {PathGraphics} */ path : createPropertyTypeDescriptor('path', PathGraphics), + /** + * Gets or sets the plane. + * @memberof Entity.prototype + * @type {PlaneGraphics} + */ + plane : createPropertyTypeDescriptor('plane', PlaneGraphics), /** * Gets or sets the point graphic. * @memberof Entity.prototype diff --git a/Source/DataSources/ModelGraphics.js b/Source/DataSources/ModelGraphics.js index 566b1249dc2e..eddda212b0ec 100644 --- a/Source/DataSources/ModelGraphics.js +++ b/Source/DataSources/ModelGraphics.js @@ -54,7 +54,7 @@ define([ * @param {Property} [options.color=Color.WHITE] A Property specifying the {@link Color} that blends with the model's rendered color. * @param {Property} [options.colorBlendMode=ColorBlendMode.HIGHLIGHT] An enum Property specifying how the color blends with the model. * @param {Property} [options.colorBlendAmount=0.5] A numeric Property specifying the color strength when the colorBlendMode is MIX. A value of 0.0 results in the model's rendered color while a value of 1.0 results in a solid color, with any value in-between resulting in a mix of the two. - * @param {Property} [options.clippingPlanes=[]] A property specifying an array of up to 6 {@link Plane} used to selectively disable rendering on the outside of each plane. + * @param {Property} [options.clippingPlanes] A property specifying the {@link ClippingPlanesCollection} used to selectively disable rendering the model. * * @see {@link http://cesiumjs.org/2014/03/03/Cesium-3D-Models-Tutorial/|3D Models Tutorial} * @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=3D%20Models.html|Cesium Sandcastle 3D Models Demo} @@ -248,12 +248,11 @@ define([ colorBlendAmount : createPropertyDescriptor('colorBlendAmount'), /** - * A property specifying an array of up to 6 {@link Plane} used to selectively disable rendering on the outside of each plane. + * A property specifying the {@link ClippingPlanesCollection} used to selectively disable rendering the model. * @memberof ModelGraphics.prototype * @type {Property} - * @default [] */ - clippingPlanes: createPropertyDescriptor('clippingPlanes') + clippingPlanes : createPropertyDescriptor('clippingPlanes') }); /** diff --git a/Source/DataSources/ModelVisualizer.js b/Source/DataSources/ModelVisualizer.js index f878277f2983..e8055bf1af71 100644 --- a/Source/DataSources/ModelVisualizer.js +++ b/Source/DataSources/ModelVisualizer.js @@ -40,7 +40,6 @@ define([ var defaultColor = Color.WHITE; var defaultColorBlendMode = ColorBlendMode.HIGHLIGHT; var defaultColorBlendAmount = 0.5; - var defaultClippingPlanes = []; var modelMatrixScratch = new Matrix4(); var nodeMatrixScratch = new Matrix4(); @@ -153,7 +152,7 @@ define([ model.color = Property.getValueOrDefault(modelGraphics._color, time, defaultColor, model._color); model.colorBlendMode = Property.getValueOrDefault(modelGraphics._colorBlendMode, time, defaultColorBlendMode); model.colorBlendAmount = Property.getValueOrDefault(modelGraphics._colorBlendAmount, time, defaultColorBlendAmount); - model.clippingPlanes = Property.getValueOrDefault(modelGraphics._clippingPlanes, time, defaultClippingPlanes); + model.clippingPlanes = Property.getValueOrUndefined(modelGraphics._clippingPlanes, time); if (model.ready) { var runAnimations = Property.getValueOrDefault(modelGraphics._runAnimations, time, true); diff --git a/Source/DataSources/PlaneGeometryUpdater.js b/Source/DataSources/PlaneGeometryUpdater.js new file mode 100644 index 000000000000..bf2b4915665b --- /dev/null +++ b/Source/DataSources/PlaneGeometryUpdater.js @@ -0,0 +1,731 @@ +define([ + '../Core/PlaneGeometry', + '../Core/PlaneOutlineGeometry', + '../Core/Cartesian2', + '../Core/Cartesian3', + '../Core/Color', + '../Core/ColorGeometryInstanceAttribute', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/destroyObject', + '../Core/DeveloperError', + '../Core/DistanceDisplayCondition', + '../Core/DistanceDisplayConditionGeometryInstanceAttribute', + '../Core/Event', + '../Core/GeometryInstance', + '../Core/Iso8601', + '../Core/Matrix4', + '../Core/ShowGeometryInstanceAttribute', + '../Core/Quaternion', + '../Scene/MaterialAppearance', + '../Scene/PerInstanceColorAppearance', + '../Scene/Primitive', + '../Scene/ShadowMode', + './ColorMaterialProperty', + './ConstantProperty', + './dynamicGeometryGetBoundingSphere', + './MaterialProperty', + './Property' + ], function( + PlaneGeometry, + PlaneOutlineGeometry, + Cartesian2, + Cartesian3, + Color, + ColorGeometryInstanceAttribute, + defaultValue, + defined, + defineProperties, + destroyObject, + DeveloperError, + DistanceDisplayCondition, + DistanceDisplayConditionGeometryInstanceAttribute, + Event, + GeometryInstance, + Iso8601, + Matrix4, + ShowGeometryInstanceAttribute, + Quaternion, + MaterialAppearance, + PerInstanceColorAppearance, + Primitive, + ShadowMode, + ColorMaterialProperty, + ConstantProperty, + dynamicGeometryGetBoundingSphere, + MaterialProperty, + Property) { + 'use strict'; + + var defaultMaterial = new ColorMaterialProperty(Color.WHITE); + var defaultShow = new ConstantProperty(true); + var defaultFill = new ConstantProperty(true); + var defaultOutline = new ConstantProperty(false); + var defaultOutlineColor = new ConstantProperty(Color.BLACK); + var defaultShadows = new ConstantProperty(ShadowMode.DISABLED); + var defaultDistanceDisplayCondition = new ConstantProperty(new DistanceDisplayCondition()); + var scratchColor = new Color(); + + function GeometryOptions(entity) { + this.id = entity; + this.vertexFormat = undefined; + this.plane = undefined; + this.dimensions = undefined; + } + + /** + * A {@link GeometryUpdater} for planes. + * Clients do not normally create this class directly, but instead rely on {@link DataSourceDisplay}. + * @alias PlaneGeometryUpdater + * @constructor + * + * @param {Entity} entity The entity containing the geometry to be visualized. + * @param {Scene} scene The scene where visualization is taking place. + */ + function PlaneGeometryUpdater(entity, scene) { + //>>includeStart('debug', pragmas.debug); + if (!defined(entity)) { + throw new DeveloperError('entity is required'); + } + if (!defined(scene)) { + throw new DeveloperError('scene is required'); + } + //>>includeEnd('debug'); + + this._entity = entity; + this._scene = scene; + this._entitySubscription = entity.definitionChanged.addEventListener(PlaneGeometryUpdater.prototype._onEntityPropertyChanged, this); + this._fillEnabled = false; + this._dynamic = false; + this._outlineEnabled = false; + this._geometryChanged = new Event(); + this._showProperty = undefined; + this._materialProperty = undefined; + this._hasConstantOutline = true; + this._showOutlineProperty = undefined; + this._outlineColorProperty = undefined; + this._outlineWidth = 1.0; + this._shadowsProperty = undefined; + this._distanceDisplayConditionProperty = undefined; + this._options = new GeometryOptions(entity); + this._onEntityPropertyChanged(entity, 'plane', entity.plane, undefined); + } + + defineProperties(PlaneGeometryUpdater, { + /** + * Gets the type of Appearance to use for simple color-based geometry. + * @memberof PlaneGeometryUpdater + * @type {Appearance} + */ + perInstanceColorAppearanceType : { + value : PerInstanceColorAppearance + }, + /** + * Gets the type of Appearance to use for material-based geometry. + * @memberof PlaneGeometryUpdater + * @type {Appearance} + */ + materialAppearanceType : { + value : MaterialAppearance + } + }); + + defineProperties(PlaneGeometryUpdater.prototype, { + /** + * Gets the entity associated with this geometry. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Entity} + * @readonly + */ + entity : { + get : function() { + return this._entity; + } + }, + /** + * Gets a value indicating if the geometry has a fill component. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Boolean} + * @readonly + */ + fillEnabled : { + get : function() { + return this._fillEnabled; + } + }, + /** + * Gets a value indicating if fill visibility varies with simulation time. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Boolean} + * @readonly + */ + hasConstantFill : { + get : function() { + return !this._fillEnabled || + (!defined(this._entity.availability) && + Property.isConstant(this._showProperty) && + Property.isConstant(this._fillProperty)); + } + }, + /** + * Gets the material property used to fill the geometry. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {MaterialProperty} + * @readonly + */ + fillMaterialProperty : { + get : function() { + return this._materialProperty; + } + }, + /** + * Gets a value indicating if the geometry has an outline component. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Boolean} + * @readonly + */ + outlineEnabled : { + get : function() { + return this._outlineEnabled; + } + }, + /** + * Gets a value indicating if the geometry has an outline component. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Boolean} + * @readonly + */ + hasConstantOutline : { + get : function() { + return !this._outlineEnabled || + (!defined(this._entity.availability) && + Property.isConstant(this._showProperty) && + Property.isConstant(this._showOutlineProperty)); + } + }, + /** + * Gets the {@link Color} property for the geometry outline. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Property} + * @readonly + */ + outlineColorProperty : { + get : function() { + return this._outlineColorProperty; + } + }, + /** + * Gets the constant with of the geometry outline, in pixels. + * This value is only valid if isDynamic is false. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Number} + * @readonly + */ + outlineWidth : { + get : function() { + return this._outlineWidth; + } + }, + /** + * Gets the property specifying whether the geometry + * casts or receives shadows from each light source. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Property} + * @readonly + */ + shadowsProperty : { + get : function() { + return this._shadowsProperty; + } + }, + /** + * Gets or sets the {@link DistanceDisplayCondition} Property specifying at what distance from the camera that this geometry will be displayed. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Property} + * @readonly + */ + distanceDisplayConditionProperty : { + get : function() { + return this._distanceDisplayConditionProperty; + } + }, + /** + * Gets a value indicating if the geometry is time-varying. + * If true, all visualization is delegated to the {@link DynamicGeometryUpdater} + * returned by GeometryUpdater#createDynamicUpdater. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Boolean} + * @readonly + */ + isDynamic : { + get : function() { + return this._dynamic; + } + }, + /** + * Gets a value indicating if the geometry is closed. + * This property is only valid for static geometry. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Boolean} + * @readonly + */ + isClosed : { + value : true + }, + /** + * Gets an event that is raised whenever the public properties + * of this updater change. + * @memberof PlaneGeometryUpdater.prototype + * + * @type {Boolean} + * @readonly + */ + geometryChanged : { + get : function() { + return this._geometryChanged; + } + } + }); + + /** + * Checks if the geometry is outlined at the provided time. + * + * @param {JulianDate} time The time for which to retrieve visibility. + * @returns {Boolean} true if geometry is outlined at the provided time, false otherwise. + */ + PlaneGeometryUpdater.prototype.isOutlineVisible = function(time) { + var entity = this._entity; + return this._outlineEnabled && entity.isAvailable(time) && this._showProperty.getValue(time) && this._showOutlineProperty.getValue(time); + }; + + /** + * Checks if the geometry is filled at the provided time. + * + * @param {JulianDate} time The time for which to retrieve visibility. + * @returns {Boolean} true if geometry is filled at the provided time, false otherwise. + */ + PlaneGeometryUpdater.prototype.isFilled = function(time) { + var entity = this._entity; + return this._fillEnabled && entity.isAvailable(time) && this._showProperty.getValue(time) && this._fillProperty.getValue(time); + }; + + /** + * Creates the geometry instance which represents the fill of the geometry. + * + * @param {JulianDate} time The time to use when retrieving initial attribute values. + * @returns {GeometryInstance} The geometry instance representing the filled portion of the geometry. + * + * @exception {DeveloperError} This instance does not represent a filled geometry. + */ + PlaneGeometryUpdater.prototype.createFillGeometryInstance = function(time) { + //>>includeStart('debug', pragmas.debug); + if (!defined(time)) { + throw new DeveloperError('time is required.'); + } + + if (!this._fillEnabled) { + throw new DeveloperError('This instance does not represent a filled geometry.'); + } + //>>includeEnd('debug'); + + var entity = this._entity; + var isAvailable = entity.isAvailable(time); + + var attributes; + + var color; + var show = new ShowGeometryInstanceAttribute(isAvailable && entity.isShowing && this._showProperty.getValue(time) && this._fillProperty.getValue(time)); + var distanceDisplayCondition = this._distanceDisplayConditionProperty.getValue(time); + var distanceDisplayConditionAttribute = DistanceDisplayConditionGeometryInstanceAttribute.fromDistanceDisplayCondition(distanceDisplayCondition); + if (this._materialProperty instanceof ColorMaterialProperty) { + var currentColor = Color.WHITE; + if (defined(this._materialProperty.color) && (this._materialProperty.color.isConstant || isAvailable)) { + currentColor = this._materialProperty.color.getValue(time); + } + color = ColorGeometryInstanceAttribute.fromColor(currentColor); + attributes = { + show : show, + distanceDisplayCondition : distanceDisplayConditionAttribute, + color : color + }; + } else { + attributes = { + show : show, + distanceDisplayCondition : distanceDisplayConditionAttribute + }; + } + + var planeGraphics = entity.plane; + var options = this._options; + var modelMatrix = entity.computeModelMatrix(time); + var plane = Property.getValueOrDefault(planeGraphics.plane, time, options.plane); + var dimensions = Property.getValueOrUndefined(planeGraphics.dimensions, time, options.dimensions); + if (!defined(modelMatrix) || !defined(plane) || !defined(dimensions)) { + return; + } + + options.plane = plane; + options.dimensions = dimensions; + + modelMatrix = createPrimitiveMatrix(plane, dimensions, modelMatrix, modelMatrix); + + return new GeometryInstance({ + id : entity, + geometry : new PlaneGeometry(this._options), + modelMatrix : modelMatrix, + attributes : attributes + }); + }; + + /** + * Creates the geometry instance which represents the outline of the geometry. + * + * @param {JulianDate} time The time to use when retrieving initial attribute values. + * @returns {GeometryInstance} The geometry instance representing the outline portion of the geometry. + * + * @exception {DeveloperError} This instance does not represent an outlined geometry. + */ + PlaneGeometryUpdater.prototype.createOutlineGeometryInstance = function(time) { + //>>includeStart('debug', pragmas.debug); + if (!defined(time)) { + throw new DeveloperError('time is required.'); + } + + if (!this._outlineEnabled) { + throw new DeveloperError('This instance does not represent an outlined geometry.'); + } + //>>includeEnd('debug'); + + var entity = this._entity; + var isAvailable = entity.isAvailable(time); + var outlineColor = Property.getValueOrDefault(this._outlineColorProperty, time, Color.BLACK); + var distanceDisplayCondition = this._distanceDisplayConditionProperty.getValue(time); + + var planeGraphics = entity.plane; + var options = this._options; + var modelMatrix = entity.computeModelMatrix(time); + var plane = Property.getValueOrDefault(planeGraphics.plane, time, options.plane); + var dimensions = Property.getValueOrUndefined(planeGraphics.dimensions, time, options.dimensions); + if (!defined(modelMatrix) || !defined(plane) || !defined(dimensions)) { + return; + } + + options.plane = plane; + options.dimensions = dimensions; + + modelMatrix = createPrimitiveMatrix(plane, dimensions, modelMatrix, modelMatrix); + + return new GeometryInstance({ + id : entity, + geometry : new PlaneOutlineGeometry(), + modelMatrix : modelMatrix, + attributes : { + show : new ShowGeometryInstanceAttribute(isAvailable && entity.isShowing && this._showProperty.getValue(time) && this._showOutlineProperty.getValue(time)), + color : ColorGeometryInstanceAttribute.fromColor(outlineColor), + distanceDisplayCondition : DistanceDisplayConditionGeometryInstanceAttribute.fromDistanceDisplayCondition(distanceDisplayCondition) + } + }); + }; + + /** + * Returns true if this object was destroyed; otherwise, false. + * + * @returns {Boolean} True if this object was destroyed; otherwise, false. + */ + PlaneGeometryUpdater.prototype.isDestroyed = function() { + return false; + }; + + /** + * Destroys and resources used by the object. Once an object is destroyed, it should not be used. + * + * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. + */ + PlaneGeometryUpdater.prototype.destroy = function() { + this._entitySubscription(); + destroyObject(this); + }; + + PlaneGeometryUpdater.prototype._onEntityPropertyChanged = function(entity, propertyName, newValue, oldValue) { + if (!(propertyName === 'availability' || propertyName === 'position' || propertyName === 'orientation' || propertyName === 'plane')) { + return; + } + var planeGraphics = this._entity.plane; + + if (!defined(planeGraphics)) { + if (this._fillEnabled || this._outlineEnabled) { + this._fillEnabled = false; + this._outlineEnabled = false; + this._geometryChanged.raiseEvent(this); + } + return; + } + + var fillProperty = planeGraphics.fill; + var fillEnabled = defined(fillProperty) && fillProperty.isConstant ? fillProperty.getValue(Iso8601.MINIMUM_VALUE) : true; + + var outlineProperty = planeGraphics.outline; + var outlineEnabled = defined(outlineProperty); + if (outlineEnabled && outlineProperty.isConstant) { + outlineEnabled = outlineProperty.getValue(Iso8601.MINIMUM_VALUE); + } + + if (!fillEnabled && !outlineEnabled) { + if (this._fillEnabled || this._outlineEnabled) { + this._fillEnabled = false; + this._outlineEnabled = false; + this._geometryChanged.raiseEvent(this); + } + return; + } + + var plane = planeGraphics.plane; + var dimensions = planeGraphics.dimensions; + var position = entity.position; + + var show = planeGraphics.show; + if (!defined(plane) || !defined(dimensions) || !defined(position) || (defined(show) && show.isConstant && !show.getValue(Iso8601.MINIMUM_VALUE))) { + if (this._fillEnabled || this._outlineEnabled) { + this._fillEnabled = false; + this._outlineEnabled = false; + this._geometryChanged.raiseEvent(this); + } + return; + } + + var material = defaultValue(planeGraphics.material, defaultMaterial); + var isColorMaterial = material instanceof ColorMaterialProperty; + this._materialProperty = material; + this._fillProperty = defaultValue(fillProperty, defaultFill); + this._showProperty = defaultValue(show, defaultShow); + this._showOutlineProperty = defaultValue(planeGraphics.outline, defaultOutline); + this._outlineColorProperty = outlineEnabled ? defaultValue(planeGraphics.outlineColor, defaultOutlineColor) : undefined; + this._shadowsProperty = defaultValue(planeGraphics.shadows, defaultShadows); + this._distanceDisplayConditionProperty = defaultValue(planeGraphics.distanceDisplayCondition, defaultDistanceDisplayCondition); + + var outlineWidth = planeGraphics.outlineWidth; + + this._fillEnabled = fillEnabled; + this._outlineEnabled = outlineEnabled; + + if (!position.isConstant || // + !Property.isConstant(entity.orientation) || // + !plane.isConstant || // + !dimensions.isConstant || // + !Property.isConstant(outlineWidth)) { + if (!this._dynamic) { + this._dynamic = true; + this._geometryChanged.raiseEvent(this); + } + } else { + var options = this._options; + options.vertexFormat = isColorMaterial ? PerInstanceColorAppearance.VERTEX_FORMAT : MaterialAppearance.MaterialSupport.TEXTURED.vertexFormat; + options.plane = plane.getValue(Iso8601.MINIMUM_VALUE, options.plane); + options.dimensions = dimensions.getValue(Iso8601.MINIMUM_VALUE, options.dimensions); + this._outlineWidth = defined(outlineWidth) ? outlineWidth.getValue(Iso8601.MINIMUM_VALUE) : 1.0; + this._dynamic = false; + this._geometryChanged.raiseEvent(this); + } + }; + + /** + * Creates the dynamic updater to be used when GeometryUpdater#isDynamic is true. + * + * @param {PrimitiveCollection} primitives The primitive collection to use. + * @returns {DynamicGeometryUpdater} The dynamic updater used to update the geometry each frame. + * + * @exception {DeveloperError} This instance does not represent dynamic geometry. + */ + PlaneGeometryUpdater.prototype.createDynamicUpdater = function(primitives) { + //>>includeStart('debug', pragmas.debug); + if (!this._dynamic) { + throw new DeveloperError('This instance does not represent dynamic geometry.'); + } + + if (!defined(primitives)) { + throw new DeveloperError('primitives is required.'); + } + //>>includeEnd('debug'); + + return new DynamicGeometryUpdater(primitives, this); + }; + + /** + * @private + */ + function DynamicGeometryUpdater(primitives, geometryUpdater) { + this._primitives = primitives; + this._primitive = undefined; + this._outlinePrimitive = undefined; + this._geometryUpdater = geometryUpdater; + this._options = new GeometryOptions(geometryUpdater._entity); + } + DynamicGeometryUpdater.prototype.update = function(time) { + //>>includeStart('debug', pragmas.debug); + if (!defined(time)) { + throw new DeveloperError('time is required.'); + } + //>>includeEnd('debug'); + + var primitives = this._primitives; + primitives.removeAndDestroy(this._primitive); + primitives.removeAndDestroy(this._outlinePrimitive); + this._primitive = undefined; + this._outlinePrimitive = undefined; + + var geometryUpdater = this._geometryUpdater; + var entity = geometryUpdater._entity; + var planeGraphics = entity.plane; + if (!entity.isShowing || !entity.isAvailable(time) || !Property.getValueOrDefault(planeGraphics.show, time, true)) { + return; + } + + var options = this._options; + var modelMatrix = entity.computeModelMatrix(time); + var plane = Property.getValueOrDefault(planeGraphics.plane, time, options.plane); + var dimensions = Property.getValueOrUndefined(planeGraphics.dimensions, time, options.dimensions); + if (!defined(modelMatrix) || !defined(plane) || !defined(dimensions)) { + return; + } + + options.plane = plane; + options.dimensions = dimensions; + + modelMatrix = createPrimitiveMatrix(plane, dimensions, modelMatrix, modelMatrix); + + var shadows = this._geometryUpdater.shadowsProperty.getValue(time); + + var distanceDisplayConditionProperty = this._geometryUpdater.distanceDisplayConditionProperty; + var distanceDisplayCondition = distanceDisplayConditionProperty.getValue(time); + var distanceDisplayConditionAttribute = DistanceDisplayConditionGeometryInstanceAttribute.fromDistanceDisplayCondition(distanceDisplayCondition); + + if (Property.getValueOrDefault(planeGraphics.fill, time, true)) { + var material = MaterialProperty.getValue(time, geometryUpdater.fillMaterialProperty, this._material); + this._material = material; + + var appearance = new MaterialAppearance({ + material : material, + translucent : material.isTranslucent(), + closed : true + }); + options.vertexFormat = appearance.vertexFormat; + + this._primitive = primitives.add(new Primitive({ + geometryInstances : new GeometryInstance({ + id : entity, + geometry : new PlaneGeometry(), + modelMatrix : modelMatrix, + attributes : { + distanceDisplayCondition : distanceDisplayConditionAttribute + } + }), + appearance : appearance, + asynchronous : false, + shadows : shadows + })); + } + + if (Property.getValueOrDefault(planeGraphics.outline, time, false)) { + options.vertexFormat = PerInstanceColorAppearance.VERTEX_FORMAT; + + var outlineColor = Property.getValueOrClonedDefault(planeGraphics.outlineColor, time, Color.BLACK, scratchColor); + var outlineWidth = Property.getValueOrDefault(planeGraphics.outlineWidth, time, 1.0); + var translucent = outlineColor.alpha !== 1.0; + + this._outlinePrimitive = primitives.add(new Primitive({ + geometryInstances : new GeometryInstance({ + id : entity, + geometry : new PlaneOutlineGeometry(), + modelMatrix : modelMatrix, + attributes : { + color : ColorGeometryInstanceAttribute.fromColor(outlineColor), + distanceDisplayCondition : distanceDisplayConditionAttribute + } + }), + appearance : new PerInstanceColorAppearance({ + flat : true, + translucent : translucent, + renderState : { + lineWidth : geometryUpdater._scene.clampLineWidth(outlineWidth) + } + }), + asynchronous : false, + shadows : shadows + })); + } + }; + + var scratchTranslation = new Cartesian3(); + var scratchNormal = new Cartesian3(); + var scratchScale = new Cartesian3(); + function createPrimitiveMatrix (plane, dimensions, modelMatrix, result) { + var normal; + var distance; + if (defined(plane)) { + normal = plane.normal; + distance = plane.distance; + } else { + normal = Cartesian3.clone(Cartesian3.UNIT_X, scratchNormal); + distance = 0.0; + } + + if (!defined(dimensions)) { + dimensions = new Cartesian2(1.0, 1.0); + } + + var translation = Cartesian3.multiplyByScalar(normal, distance, scratchTranslation); + translation = Matrix4.multiplyByPoint(modelMatrix, translation, translation); + + var transformedNormal = Matrix4.multiplyByPointAsVector(modelMatrix, normal, scratchNormal); + Cartesian3.normalize(transformedNormal, transformedNormal); + var rotation = getRotationMatrix(transformedNormal, Cartesian3.UNIT_Z); + + var scale = Cartesian2.clone(dimensions, scratchScale); + scale.z = 1.0; + + return Matrix4.fromTranslationQuaternionRotationScale(translation, rotation, scale, result); + } + + // get a rotation according to a normal + var scratchAxis = new Cartesian3(); + var scratchQuaternion = new Quaternion(); + function getRotationMatrix(direction, up) { + var angle = Cartesian3.angleBetween(direction, up); + if (angle === 0.0) { + return Quaternion.clone(Quaternion.IDENTITY, scratchQuaternion); + } + + var axis = Cartesian3.cross(up, direction, scratchAxis); + return Quaternion.fromAxisAngle(axis, angle, scratchQuaternion); + } + + DynamicGeometryUpdater.prototype.getBoundingSphere = function(entity, result) { + return dynamicGeometryGetBoundingSphere(entity, this._primitive, this._outlinePrimitive, result); + }; + + DynamicGeometryUpdater.prototype.isDestroyed = function() { + return false; + }; + + DynamicGeometryUpdater.prototype.destroy = function() { + var primitives = this._primitives; + primitives.removeAndDestroy(this._primitive); + primitives.removeAndDestroy(this._outlinePrimitive); + destroyObject(this); + }; + + return PlaneGeometryUpdater; +}); diff --git a/Source/DataSources/PlaneGraphics.js b/Source/DataSources/PlaneGraphics.js new file mode 100644 index 000000000000..5e5cf3dab481 --- /dev/null +++ b/Source/DataSources/PlaneGraphics.js @@ -0,0 +1,208 @@ +define([ + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/DeveloperError', + '../Core/Event', + './createMaterialPropertyDescriptor', + './createPropertyDescriptor' +], function( + defaultValue, + defined, + defineProperties, + DeveloperError, + Event, + createMaterialPropertyDescriptor, + createPropertyDescriptor) { + 'use strict'; + + /** + * Describes a plane. The center position and orientation are determined by the containing {@link Entity}. + * + * @alias PlaneGraphics + * @constructor + * + * @param {Object} [options] Object with the following properties: + * @param {Property} [options.plane] A {@link Plane} Property specifying the normal and distance for the plane. + * @param {Property} [options.dimensions] A {@link Cartesian2} Property specifying the width and height of the plane. + * @param {Property} [options.show=true] A boolean Property specifying the visibility of the plane. + * @param {Property} [options.fill=true] A boolean Property specifying whether the plane is filled with the provided material. + * @param {MaterialProperty} [options.material=Color.WHITE] A Property specifying the material used to fill the plane. + * @param {Property} [options.outline=false] A boolean Property specifying whether the plane is outlined. + * @param {Property} [options.outlineColor=Color.BLACK] A Property specifying the {@link Color} of the outline. + * @param {Property} [options.outlineWidth=1.0] A numeric Property specifying the width of the outline. + * @param {Property} [options.shadows=ShadowMode.DISABLED] An enum Property specifying whether the plane casts or receives shadows from each light source. + * @param {Property} [options.distanceDisplayCondition] A Property specifying at what distance from the camera that this plane will be displayed. + * + * @demo {@link http://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=Plane.html|Cesium Sandcastle Plane Demo} + */ + function PlaneGraphics(options) { + this._plane = undefined; + this._planeSubscription = undefined; + this._dimensions = undefined; + this._dimensionsSubscription = undefined; + this._show = undefined; + this._showSubscription = undefined; + this._fill = undefined; + this._fillSubscription = undefined; + this._material = undefined; + this._materialSubscription = undefined; + this._outline = undefined; + this._outlineSubscription = undefined; + this._outlineColor = undefined; + this._outlineColorSubscription = undefined; + this._outlineWidth = undefined; + this._outlineWidthSubscription = undefined; + this._shadows = undefined; + this._shadowsSubscription = undefined; + this._distanceDisplayCondition = undefined; + this._distanceDisplayConditionSubscription = undefined; + this._definitionChanged = new Event(); + + this.merge(defaultValue(options, defaultValue.EMPTY_OBJECT)); + } + + defineProperties(PlaneGraphics.prototype, { + /** + * Gets the event that is raised whenever a property or sub-property is changed or modified. + * @memberof PlaneGraphics.prototype + * @type {Event} + * @readonly + */ + definitionChanged : { + get : function() { + return this._definitionChanged; + } + }, + + /** + * Gets or sets the boolean Property specifying the visibility of the plane. + * @memberof PlaneGraphics.prototype + * @type {Property} + * @default true + */ + show : createPropertyDescriptor('show'), + + /** + * Gets or sets the {@link Plane} Property specifying the normal and distance of the plane. + * + * @memberof PlaneGraphics.prototype + * @type {Property} + */ + plane : createPropertyDescriptor('plane'), + + /** + * Gets or sets the {@link Cartesian2} Property specifying the width and height of the plane. + * + * @memberof PlaneGraphics.prototype + * @type {Property} + */ + dimensions : createPropertyDescriptor('dimensions'), + + /** + * Gets or sets the material used to fill the plane. + * @memberof PlaneGraphics.prototype + * @type {MaterialProperty} + * @default Color.WHITE + */ + material : createMaterialPropertyDescriptor('material'), + + /** + * Gets or sets the boolean Property specifying whether the plane is filled with the provided material. + * @memberof PlaneGraphics.prototype + * @type {Property} + * @default true + */ + fill : createPropertyDescriptor('fill'), + + /** + * Gets or sets the Property specifying whether the plane is outlined. + * @memberof PlaneGraphics.prototype + * @type {Property} + * @default false + */ + outline : createPropertyDescriptor('outline'), + + /** + * Gets or sets the Property specifying the {@link Color} of the outline. + * @memberof PlaneGraphics.prototype + * @type {Property} + * @default Color.BLACK + */ + outlineColor : createPropertyDescriptor('outlineColor'), + + /** + * Gets or sets the numeric Property specifying the width of the outline. + * @memberof PlaneGraphics.prototype + * @type {Property} + * @default 1.0 + */ + outlineWidth : createPropertyDescriptor('outlineWidth'), + + /** + * Get or sets the enum Property specifying whether the plane + * casts or receives shadows from each light source. + * @memberof PlaneGraphics.prototype + * @type {Property} + * @default ShadowMode.DISABLED + */ + shadows : createPropertyDescriptor('shadows'), + + /** + * Gets or sets the {@link DistanceDisplayCondition} Property specifying at what distance from the camera that this plane will be displayed. + * @memberof PlaneGraphics.prototype + * @type {Property} + */ + distanceDisplayCondition : createPropertyDescriptor('distanceDisplayCondition') + }); + + /** + * Duplicates this instance. + * + * @param {PlaneGraphics} [result] The object onto which to store the result. + * @returns {PlaneGraphics} The modified result parameter or a new instance if one was not provided. + */ + PlaneGraphics.prototype.clone = function(result) { + if (!defined(result)) { + return new PlaneGraphics(this); + } + result.plane = this.plane; + result.dimensions = this.dimensions; + result.show = this.show; + result.material = this.material; + result.fill = this.fill; + result.outline = this.outline; + result.outlineColor = this.outlineColor; + result.outlineWidth = this.outlineWidth; + result.shadows = this.shadows; + result.distanceDisplayCondition = this.distanceDisplayCondition; + return result; + }; + + /** + * Assigns each unassigned property on this object to the value + * of the same property on the provided source object. + * + * @param {PlaneGraphics} source The object to be merged into this object. + */ + PlaneGraphics.prototype.merge = function(source) { + //>>includeStart('debug', pragmas.debug); + if (!defined(source)) { + throw new DeveloperError('source is required.'); + } + //>>includeEnd('debug'); + + this.plane = defaultValue(this.plane, source.plane); + this.dimensions = defaultValue(this.dimensions, source.dimensions); + this.show = defaultValue(this.show, source.show); + this.material = defaultValue(this.material, source.material); + this.fill = defaultValue(this.fill, source.fill); + this.outline = defaultValue(this.outline, source.outline); + this.outlineColor = defaultValue(this.outlineColor, source.outlineColor); + this.outlineWidth = defaultValue(this.outlineWidth, source.outlineWidth); + this.shadows = defaultValue(this.shadows, source.shadows); + this.distanceDisplayCondition = defaultValue(this.distanceDisplayCondition, source.distanceDisplayCondition); + }; + + return PlaneGraphics; +}); diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js index 95ce75a3ab86..ec9df91a15e1 100644 --- a/Source/Scene/Batched3DModel3DTileContent.js +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -17,6 +17,7 @@ define([ './Cesium3DTileBatchTable', './Cesium3DTileFeature', './Cesium3DTileFeatureTable', + './ClippingPlanesCollection', './getAttributeOrUniformBySemantic', './Model' ], function( @@ -38,6 +39,7 @@ define([ Cesium3DTileBatchTable, Cesium3DTileFeature, Cesium3DTileFeatureTable, + ClippingPlanesCollection, getAttributeOrUniformBySemantic, Model) { 'use strict'; @@ -378,9 +380,14 @@ define([ pickUniformMapLoaded : batchTable.getPickUniformMapCallback(), addBatchIdToGeneratedShaders : (batchLength > 0), // If the batch table has values in it, generated shaders will need a batchId attribute pickObject : pickObject, - clippingPlanes : tileset.clippingPlanes, - clippingPlanesEnabled : tileset.clippingPlanesEnabled && tile._isClipped + clippingPlanes : new ClippingPlanesCollection({ + enabled : false + }) }); + + if (defined(tileset.clippingPlanes)) { + content._model.clippingPlanes = tileset.clippingPlanes.clone(); + } } function createFeatures(content) { @@ -449,8 +456,15 @@ define([ this._model.modelMatrix = this._tile.computedTransform; this._model.shadows = this._tileset.shadows; this._model.debugWireframe = this._tileset.debugWireframe; - this._model.clippingPlanes = this._tileset.clippingPlanes; - this._model.clippingPlanesEnabled = this._tileset.clippingPlanesEnabled && this._tile._isClipped; + + // Update clipping planes + var tilesetClippingPlanes = this._tileset.clippingPlanes; + if (defined(tilesetClippingPlanes)) { + var modelClippingPlanes = this._model.clippingPlanes; + tilesetClippingPlanes.clone(modelClippingPlanes); + modelClippingPlanes.enabled = tilesetClippingPlanes.enabled && this._tile._isClipped; + } + this._model.update(frameState); // If any commands were pushed, add derived commands diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index a09d67affac0..d7af1391ed35 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -740,27 +740,6 @@ define([ return frameState.mode !== SceneMode.SCENE3D ? tile._contentBoundingVolume2D : tile._contentBoundingVolume; } - - var scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0); - function checkTileClipped(tile, boundingVolume) { - var planes = tile._tileset.clippingPlanes; - if (defined(planes)) { - var length = planes.length; - var rootTransform = tile._tileset._root.computedTransform; - for (var i = 0; i < length; ++i) { - var plane = planes[i]; - Plane.transform(plane, rootTransform, scratchPlane); - var value = boundingVolume.intersectPlane(scratchPlane); - if (value !== Intersect.INSIDE) { - return value; - } - } - } - - return Intersect.INSIDE; - } - - /** * Determines whether the tile's bounding volume intersects the culling volume. * @@ -774,10 +753,13 @@ define([ var cullingVolume = frameState.cullingVolume; var boundingVolume = getBoundingVolume(this, frameState); - if (this._tileset.clippingPlanesEnabled) { - var clipped = checkTileClipped(this, boundingVolume); - this._isClipped = (clipped !== Intersect.INSIDE); - if (clipped === Intersect.OUTSIDE) { + var tileset = this._tileset; + var clippingPlanes = tileset.clippingPlanes; + if (defined(clippingPlanes) && clippingPlanes.enabled) { + var tileTransform = tileset._root.computedTransform; + var intersection = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume, tileTransform); + this._isClipped = (intersection !== Intersect.INSIDE); + if (intersection === Intersect.OUTSIDE) { return CullingVolume.MASK_OUTSIDE; } } @@ -806,10 +788,13 @@ define([ var cullingVolume = frameState.cullingVolume; var boundingVolume = getContentBoundingVolume(this, frameState); - if (this._tileset.clippingPlanesEnabled) { - var clipped = checkTileClipped(this, boundingVolume); - this._isClipped = (clipped !== Intersect.INSIDE); - if (clipped === Intersect.OUTSIDE) { + var tileset = this._tileset; + var clippingPlanes = tileset.clippingPlanes; + if (defined(clippingPlanes) && clippingPlanes.enabled) { + var tileTransform = tileset._root.computedTransform; + var intersection = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume, tileTransform); + this._isClipped = (intersection !== Intersect.INSIDE); + if (intersection === Intersect.OUTSIDE) { return Intersect.OUTSIDE; } } diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 06310646cf57..adcadce80fc7 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -105,8 +105,7 @@ define([ * @param {Number} [options.skipLevels=1] When skipLevelOfDetail is true, a constant defining the minimum number of levels to skip when loading tiles. When it is 0, no levels are skipped. Used in conjunction with skipScreenSpaceErrorFactor to determine which tiles to load. * @param {Boolean} [options.immediatelyLoadDesiredLevelOfDetail=false] When skipLevelOfDetail is true, only tiles that meet the maximum screen space error will ever be downloaded. Skipping factors are ignored and just the desired tiles are loaded. * @param {Boolean} [options.loadSiblings=false] When skipLevelOfDetail is true, determines whether siblings of visible tiles are always downloaded during traversal. - * @param {Plane[]} [options.clippingPlanes] A property specifying an array of up to 6 {@link Plane} used to selectively disable rendering on the outside of each plane. - * @param [Boolean} [options.clippingPlanesEnabled=true] Optimization option. If set to false, the tileset will not perform clipping operations. + * @param {ClippingPlanesCollection} [options.clippingPlanes] The {@link ClippingPlanesCollection} used to selectively disable rendering the tileset. * @param {Boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering. * @param {Boolean} [options.debugColorizeTiles=false] For debugging only. When true, assigns a random color to each tile. * @param {Boolean} [options.debugWireframe=false] For debugging only. When true, render's each tile's content as a wireframe. @@ -525,21 +524,12 @@ define([ this.loadSiblings = defaultValue(options.loadSiblings, false); /** - * A property specifying an array of up to 6 {@link Plane} used to selectively disable rendering on the outside of each plane. + * The {@link ClippingPlanesCollection} used to selectively disable rendering the tileset. * - * @type {Plane[]} + * @type {ClippingPlanesCollection} */ this.clippingPlanes = options.clippingPlanes; - /** - * Optimization option. If set to false, the tileset will not perform clipping operations. - * - * @type {Boolean} - * @default {true} - * @see Cesium3DTileset.clippingPlanes - */ - this.clippingPlanesEnabled = defaultValue(options.clippingPlanesEnabled, true); - /** * This property is for debugging only; it is not optimized for production use. *

diff --git a/Source/Scene/ClippingPlanesCollection.js b/Source/Scene/ClippingPlanesCollection.js new file mode 100644 index 000000000000..33d21eec26d8 --- /dev/null +++ b/Source/Scene/ClippingPlanesCollection.js @@ -0,0 +1,245 @@ +define([ + '../Core/Cartesian3', + '../Core/Cartesian4', + '../Core/Check', + '../Core/Color', + '../Core/defaultValue', + '../Core/defined', + '../Core/defineProperties', + '../Core/Intersect', + '../Core/Matrix4', + '../Core/Plane' + ], function( + Cartesian3, + Cartesian4, + Check, + Color, + defaultValue, + defined, + defineProperties, + Intersect, + Matrix4, + Plane) { + 'use strict'; + + /** + * Specifies a set of clipping planes. Clipping planes selectively disable rendering in a region on the outside of the specified list of {@link Plane} objects. + * + * @alias ClippingPlanesCollection + * @constructor + * + * @param {Object} [options] Object with the following properties: + * @param {Plane[]} [options.planes=[]] An array of up to 6 {@link Plane} objects used to selectively disable rendering on the outside of each plane. + * @param {Boolean} [options.enabled=true] Determines whether the clipping planes are active. + * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix specifying an additional transform relative to the clipping planes original coordinate system. + * @param {Boolean} [options.combineClippingRegions=true] If true, the region to be clipped must be included in all planes in this collection. Otherwise, a region will be clipped if included in any plane in the collection. + * @param {Color} [options.edgeColor=Color.WHITE] The color applied to highlight the edge along which an object is clipped. + * @param {Number} [options.edgeWidth=0.0] The width, in pixels, of the highlight applied to the edge along which an object is clipped. + */ + function ClippingPlanesCollection(options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + + /** + * An array of up to 6 {@link Plane} objects used to selectively disable rendering on the outside of each plane. + * + * @type {Plane} + * @default [] + */ + this.planes = defaultValue(options.planes, []); + + /** + * Determines whether the clipping planes are active. + * + * @type {Boolean} + * @default true + */ + this.enabled = defaultValue(options.enabled, true); + + /** + * The 4x4 transformation matrix specifying an additional transform relative to the clipping planes original coordinate system. + * + * @type {Matrix4} + * @default Matrix4.IDENTITY + */ + this.modelMatrix = defaultValue(options.modelMatrix, Matrix4.clone(Matrix4.IDENTITY)); + + /** + * The color applied to highlight the edge along which an object is clipped. + * + * @type {Color} + * @default Color.WHITE + */ + this.edgeColor = defaultValue(options.edgeColor, Color.clone(Color.WHITE)); + + /** + * The width, in pixels, of the highlight applied to the edge along which an object is clipped. + * + * @type {Number} + * @default 0.0 + */ + this.edgeWidth = defaultValue(options.edgeWidth, 0.0); + + this._testIntersection = undefined; + this._combineClippingRegions = undefined; + this.combineClippingRegions = defaultValue(options.combineClippingRegions, true); + } + + defineProperties(ClippingPlanesCollection.prototype, { + /** + * If true, the region to be clipped must be included in all planes in this collection. + * Otherwise, a region will be clipped if included in any plane in the collection. + * + * @memberof ClippingPlanesCollection.prototype + * @type {Boolean} + * @default true + */ + combineClippingRegions : { + get : function() { + return this._combineClippingRegions; + }, + set : function(value) { + if (this._combineClippingRegions !== value) { + this._combineClippingRegions = value; + this._testIntersection = getTestIntersectionFunction(value); + } + } + } + }); + + function getTestIntersectionFunction(combineClippingRegions) { + if (combineClippingRegions) { + return function(value) { + return (value === Intersect.INSIDE); + }; + } + + return function(value) { + return (value === Intersect.OUTSIDE); + }; + } + + var scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0); + var scratchMatrix = new Matrix4(); + /** + * Applies the transformations to each plane and packs it into an array. + * @private + * + * @param viewMatrix The 4x4 matrix to transform the plane into eyespace. + * @param [array] The array into which the planes will be packed. + * @returns {Cartesian4[]} The array of packed planes. + */ + ClippingPlanesCollection.prototype.transformAndPackPlanes = function(viewMatrix, array) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('viewMatrix', viewMatrix); + //>>includeEnd('debug'); + + var planes = this.planes; + var length = planes.length; + + var i; + if (!defined(array) || array.length !== length) { + array = new Array(length); + + for (i = 0; i < length; ++i) { + array[i] = new Cartesian4(); + } + } + + var transform = Matrix4.multiply(viewMatrix, this.modelMatrix, scratchMatrix); + + for (i = 0; i < length; ++i) { + var plane = planes[i]; + var packedPlane = array[i]; + + Plane.transform(plane, transform, scratchPlane); + + Cartesian3.clone(scratchPlane.normal, packedPlane); + packedPlane.w = scratchPlane.distance; + } + + return array; + }; + + /** + * Duplicates this ClippingPlanesCollection instance. + * + * @param {ClippingPlanesCollection} [result] The object onto which to store the result. + * @returns {ClippingPlanesCollection} The modified result parameter or a new ClippingPlanesCollection instance if one was not provided. + */ + ClippingPlanesCollection.prototype.clone = function(result) { + if (!defined(result)) { + result = new ClippingPlanesCollection(); + } + + var length = this.planes.length; + var i; + if (result.planes.length !== length) { + result.planes = new Array(length); + + for (i = 0; i < length; ++i) { + result.planes[i] = new Plane(Cartesian3.UNIT_X, 0.0); + } + } + + for (i = 0; i < length; ++i) { + var plane = this.planes[i]; + var resultPlane = result.planes[i]; + resultPlane.normal = plane.normal; + resultPlane.distance = plane.distance; + } + + result.enabled = this.enabled; + Matrix4.clone(this.modelMatrix, result.modelMatrix); + result.combineClippingRegions = this.combineClippingRegions; + Color.clone(this.edgeColor, result.edgeColor); + result.edgeWidth = this.edgeWidth; + + return result; + }; + + /** + * Determines the type intersection with the planes of this ClippingPlanesCollection instance and the specified {@link BoundingVolume}. + * @private + * + * @param {Object} boundingVolume The volume to determine the intersection with the planes. + * @param {Matrix4} [transform] An optional, additional matrix to transform the plane to world coordinates. + * @returns {Intersect} {@link Intersect.INSIDE} if the entire volume is on the side of the planes + * the normal is pointing and should be entirely rendered, {@link Intersect.OUTSIDE} + * if the entire volume is on the opposite side and should be clipped, and + * {@link Intersect.INTERSECTING} if the volume intersects the planes. + */ + ClippingPlanesCollection.prototype.computeIntersectionWithBoundingVolume = function(boundingVolume, transform) { + var planes = this.planes; + var length = planes.length; + + var modelMatrix = this.modelMatrix; + if (defined(transform)) { + modelMatrix = Matrix4.multiply(modelMatrix, transform, scratchMatrix); + } + + // If the clipping planes are using combineClippingRegions, the volume must be outside of all planes to be considered + // completely clipped. Otherwise, if the volume can be outside any the planes, it is considered completely clipped. + // Lastly, if not completely clipped, if any plane is intersecting, more calculations must be performed. + var intersection = Intersect.INSIDE; + if (this.combineClippingRegions && length > 0) { + intersection = Intersect.OUTSIDE; + } + + for (var i = 0; i < length; ++i) { + var plane = planes[i]; + + Plane.transform(plane, modelMatrix, scratchPlane); + + var value = boundingVolume.intersectPlane(scratchPlane); + if (value === Intersect.INTERSECTING) { + intersection = value; + } else if (this._testIntersection(value)) { + return value; + } + } + + return intersection; + }; + + return ClippingPlanesCollection; +}); diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 8a351098c7b9..150e780c1d01 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -234,6 +234,20 @@ define([ this._surface.tileProvider.baseColor = value; } }, + /** + * A property specifying an array of up to 6 {@link Plane} used to selectively disable rendering on the outside of each plane. + * + * @memberof Globe.prototype + * @type {ClippingPlanesCollection} + */ + clippingPlanes : { + get : function() { + return this._surface.tileProvider.clippingPlanes; + }, + set : function(value) { + this._surface.tileProvider.clippingPlanes = value; + } + }, /** * The terrain provider providing surface geometry for this globe. * @type {TerrainProvider} diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index 2442df1a5efb..7de0b451d118 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -61,7 +61,7 @@ define([ return useWebMercatorProjection ? get2DYPositionFractionMercatorProjection : get2DYPositionFractionGeographicProjection; } - GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog) { + GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog, enableClippingPlanes, combineClippingRegions) { var quantization = 0; var quantizationDefine = ''; @@ -87,7 +87,8 @@ define([ (useWebMercatorProjection << 12) | (enableFog << 13) | (quantization << 14) | - (applySplit << 15); + (applySplit << 15) | + (enableClippingPlanes << 16); var surfaceShader = surfaceTile.surfaceShader; if (defined(surfaceShader) && @@ -160,6 +161,14 @@ define([ fs.defines.push('APPLY_SPLIT'); } + if (enableClippingPlanes) { + fs.defines.push('ENABLE_CLIPPING_PLANES'); + + if (combineClippingRegions) { + fs.defines.push('COMBINE_CLIPPING_REGIONS'); + } + } + var computeDayColor = '\ vec4 computeDayColor(vec4 initialColor, vec3 textureCoordinates)\n\ {\n\ diff --git a/Source/Scene/GlobeSurfaceTile.js b/Source/Scene/GlobeSurfaceTile.js index a976e41f53c2..22aad038b2b4 100644 --- a/Source/Scene/GlobeSurfaceTile.js +++ b/Source/Scene/GlobeSurfaceTile.js @@ -80,6 +80,7 @@ define([ this.pickTerrain = undefined; this.surfaceShader = undefined; + this.isClipped = true; } defineProperties(GlobeSurfaceTile.prototype, { diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index 00c047e2b83d..a7646e76bde8 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -20,6 +20,7 @@ define([ '../Core/Matrix4', '../Core/OrientedBoundingBox', '../Core/OrthographicFrustum', + '../Core/Plane', '../Core/PrimitiveType', '../Core/Rectangle', '../Core/SphereOutlineGeometry', @@ -64,6 +65,7 @@ define([ Matrix4, OrientedBoundingBox, OrthographicFrustum, + Plane, PrimitiveType, Rectangle, SphereOutlineGeometry, @@ -158,6 +160,13 @@ define([ this._baseColor = undefined; this._firstPassInitialColor = undefined; this.baseColor = new Color(0.0, 0.0, 0.5, 1.0); + + /** + * Gets or sets the array of clipping planes, defined in world space; + * Each clipping plane selectively disables rendering contents on the outside of the plane. + * @type {ClippingPlanesCollection} + */ + this.clippingPlanes = undefined; } defineProperties(GlobeSurfaceTileProvider.prototype, { @@ -498,6 +507,15 @@ define([ } } + var clippingPlanes = this.clippingPlanes; + if (defined(clippingPlanes) && clippingPlanes.enabled) { + var planeIntersection = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + tile.isClipped = (planeIntersection !== Intersect.INSIDE); + if (planeIntersection === Intersect.OUTSIDE) { + return Intersect.OUTSIDE; + } + } + var intersection = cullingVolume.computeVisibility(boundingVolume); if (intersection === Intersect.OUTSIDE) { return Visibility.NONE; @@ -849,6 +867,17 @@ define([ u_dayTextureSplit : function() { return this.properties.dayTextureSplit; }, + u_clippingPlanesLength : function() { + return this.properties.clippingPlanes.length; + }, + u_clippingPlanes : function() { + return this.properties.clippingPlanes; + }, + u_clippingPlanesEdgeStyle : function() { + var style = this.properties.clippingPlanesEdgeColor; + style.alpha = this.properties.clippingPlanesEdgeWidth; + return style; + }, // make a separate object so that changes to the properties are seen on // derived commands that combine another uniform map with this one. @@ -883,7 +912,10 @@ define([ waterMaskTranslationAndScale : new Cartesian4(), minMaxHeight : new Cartesian2(), - scaleAndBias : new Matrix4() + scaleAndBias : new Matrix4(), + clippingPlanes : [], + clippingPlanesEdgeColor : Color.clone(Color.WHITE), + clippingPlanesEdgeWidth : 0.0 } }; @@ -1259,7 +1291,33 @@ define([ uniformMapProperties.minMaxHeight.y = encoding.maximumHeight; Matrix4.clone(encoding.matrix, uniformMapProperties.scaleAndBias); - command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog); + // update clipping planes + var clippingPlanes = tileProvider.clippingPlanes; + var length = 0; + + if (defined(clippingPlanes) && tile.isClipped) { + length = clippingPlanes.planes.length; + } + + var clippingPlanesProperty = uniformMapProperties.clippingPlanes; + if (length !== clippingPlanesProperty.length) { + clippingPlanesProperty = uniformMapProperties.clippingPlanes = new Array(length); + + for (var i = 0; i < length; ++i) { + clippingPlanesProperty[i] = new Cartesian4(); + } + } + + if (defined(clippingPlanes) && clippingPlanes.enabled && tile.isClipped) { + clippingPlanes.transformAndPackPlanes(context.uniformState.view, clippingPlanesProperty); + uniformMapProperties.clippingPlanesEdgeColor = Color.clone(clippingPlanes.edgeColor, uniformMapProperties.clippingPlanesEdgeColor); + uniformMapProperties.clippingPlanesEdgeWidth = clippingPlanes.edgeWidth; + } + + var clippingPlanesEnabled = defined(clippingPlanes) && clippingPlanes.enabled && (uniformMapProperties.clippingPlanes.length > 0); + var combineClippingRegions = clippingPlanesEnabled ? clippingPlanes.combineClippingRegions : true; + + command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog, clippingPlanesEnabled, combineClippingRegions); command.castShadows = castShadows; command.receiveShadows = receiveShadows; command.renderState = renderState; diff --git a/Source/Scene/Instanced3DModel3DTileContent.js b/Source/Scene/Instanced3DModel3DTileContent.js index 2bd40a0ae6db..0e66cfc5989b 100644 --- a/Source/Scene/Instanced3DModel3DTileContent.js +++ b/Source/Scene/Instanced3DModel3DTileContent.js @@ -26,6 +26,7 @@ define([ './Cesium3DTileBatchTable', './Cesium3DTileFeature', './Cesium3DTileFeatureTable', + './ClippingPlanesCollection', './ModelInstanceCollection' ], function( AttributeCompression, @@ -55,6 +56,7 @@ define([ Cesium3DTileBatchTable, Cesium3DTileFeature, Cesium3DTileFeatureTable, + ClippingPlanesCollection, ModelInstanceCollection) { 'use strict'; @@ -443,7 +445,6 @@ define([ var modelMatrix = instanceTransform.clone(); instances[i] = { modelMatrix : modelMatrix, - batchId : batchId }; } @@ -514,6 +515,18 @@ define([ this._modelInstanceCollection.debugWireframe = this._tileset.debugWireframe; this._modelInstanceCollection.update(frameState); + // Update clipping planes + var tilesetClippingPlanes = this._tileset.clippingPlanes; + if (defined(tilesetClippingPlanes)) { + var model = this._modelInstanceCollection._model; + if (!defined(model.clippingPlanes)) { + model.clippingPlanes = new ClippingPlanesCollection(); + } + + tilesetClippingPlanes.clone(model.clippingPlanes); + model.clippingPlanes.enabled = tilesetClippingPlanes.enabled && this._tile._isClipped; + } + // If any commands were pushed, add derived commands var commandEnd = frameState.commandList.length; if ((commandStart < commandEnd) && frameState.passes.render) { diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index d52238b7759a..e5a570a58d63 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -341,8 +341,7 @@ define([ * @param {Number} [options.colorBlendAmount=0.5] Value used to determine the color strength when the colorBlendMode is MIX. A value of 0.0 results in the model's rendered color while a value of 1.0 results in a solid color, with any value in-between resulting in a mix of the two. * @param {Color} [options.silhouetteColor=Color.RED] The silhouette color. If more than 256 models have silhouettes enabled, there is a small chance that overlapping models will have minor artifacts. * @param {Number} [options.silhouetteSize=0.0] The size of the silhouette in pixels. - * @param {Plane[]} [options.clippingPlanes=[]] An array of {@link Plane} used to clip the model. - * @param {Boolean} [options.clippingPlanesEnabled=true] Optimization option. If set to false, the model will not perform clipping operations. + * @param {ClippingPlanesCollection} [options.clippingPlanes] The {@link ClippingPlanesCollection} used to selectively disable rendering the model. * * @exception {DeveloperError} bgltf is not a valid Binary glTF file. * @exception {DeveloperError} Only glTF Binary version 1 is supported. @@ -581,22 +580,15 @@ define([ this.colorBlendAmount = defaultValue(options.colorBlendAmount, 0.5); /** - * A property specifying an array of up to 6 {@link Plane} used to selectively disable rendering on the outside of each plane. + * The {@link ClippingPlanesCollection} used to selectively disable rendering the model. * - * @type {Plane[]} + * @type {ClippingPlanesCollection} */ this.clippingPlanes = options.clippingPlanes; - /** - * Optimization option. If set to false, the model will not perform clipping operations. - * - * @see Model.clippingPlanes - * - * @type {Boolean} - * - * @default true - */ - this.clippingPlanesEnabled = defaultValue(options.clippingPlanesEnabled, true); + // Used to determine if the shader needs to be recompiled + this._clippingPlanesEnabled = false; + this._clippingPlanesCombined = true; /** * This property is for debugging only; it is not for production use nor is it optimized. @@ -653,7 +645,7 @@ define([ this.opaquePass = defaultValue(options.opaquePass, Pass.OPAQUE); this._computedModelMatrix = new Matrix4(); // Derived from modelMatrix and scale - this._modelViewMatrix = new Matrix4(); // Derived from modelMatrix, scale, and the current view matrix + this._modelViewMatrix = Matrix4.clone(Matrix4.IDENTITY); // Derived from modelMatrix, scale, and the current view matrix this._initialRadius = undefined; // Radius without model's scale property, model-matrix scale, animations, or skins this._boundingSphere = undefined; this._scaledBoundingSphere = new BoundingSphere(); @@ -2120,11 +2112,11 @@ define([ } var premultipliedAlpha = hasPremultipliedAlpha(model); - var blendFS = modifyShaderForColor(fs, premultipliedAlpha); - var clippingFS = modifyShaderForClippingPlanes(blendFS); + var finalFS = modifyShaderForColor(fs, premultipliedAlpha); + finalFS = modifyShaderForClippingPlanes(finalFS); var drawVS = modifyShader(vs, id, model._vertexShaderLoaded); - var drawFS = modifyShader(clippingFS, id, model._fragmentShaderLoaded); + var drawFS = modifyShader(finalFS, id, model._fragmentShaderLoaded); model._rendererResources.programs[id] = ShaderProgram.fromCache({ context : context, @@ -3319,37 +3311,49 @@ define([ }; } - function createClippingPlanesEnabledFunction(model) { + function createClippingPlanesLengthFunction(model) { return function() { - return model.clippingPlanesEnabled; + return model._packedClippingPlanes.length; }; } - function createClippingPlanesLengthFunction(model) { + function createClippingPlanesCombineRegionsFunction(model) { return function() { - return model._packedClippingPlanes.length; + var clippingPlanes = model.clippingPlanes; + if (!defined(clippingPlanes)) { + return true; + } + + return clippingPlanes.combineClippingRegions; }; } - var scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0); function createClippingPlanesFunction(model) { return function() { - var planes = model.clippingPlanes; + var clippingPlanes = model.clippingPlanes; var packedPlanes = model._packedClippingPlanes; - var length = packedPlanes.length; - for (var i = 0; i < length; ++i) { - var plane = planes[i]; - var packedPlane = packedPlanes[i]; - Plane.transform(plane, model._modelViewMatrix, scratchPlane); - - Cartesian3.clone(scratchPlane.normal, packedPlane); - packedPlane.w = scratchPlane.distance; + if (defined(clippingPlanes) && clippingPlanes.enabled) { + clippingPlanes.transformAndPackPlanes(model._modelViewMatrix, packedPlanes); } + return packedPlanes; }; } + function createClippingPlanesEdgeStyleFunction(model) { + return function() { + var clippingPlanes = model.clippingPlanes; + if (!defined(clippingPlanes)) { + return Color.WHITE.withAlpha(0.0); + } + + var style = Color.clone(clippingPlanes.edgeColor); + style.alpha = clippingPlanes.edgeWidth; + return style; + }; + } + function createColorBlendFunction(model) { return function() { return ColorBlendMode.getColorBlend(model.colorBlendMode, model.colorBlendAmount); @@ -3445,9 +3449,10 @@ define([ uniformMap = combine(uniformMap, { gltf_color : createColorFunction(model), gltf_colorBlend : createColorBlendFunction(model), - gltf_clippingPlanesEnabled: createClippingPlanesEnabledFunction(model), gltf_clippingPlanesLength: createClippingPlanesLengthFunction(model), - gltf_clippingPlanes: createClippingPlanesFunction(model, context) + gltf_clippingPlanesCombineRegions: createClippingPlanesCombineRegionsFunction(model), + gltf_clippingPlanes: createClippingPlanesFunction(model, context), + gltf_clippingPlanesEdgeStyle: createClippingPlanesEdgeStyleFunction(model) }); // Allow callback to modify the uniformMap @@ -4244,14 +4249,32 @@ define([ function modifyShaderForClippingPlanes(shader) { shader = ShaderSource.replaceMain(shader, 'gltf_clip_main'); shader += - 'uniform bool gltf_clippingPlanesEnabled; \n' + 'uniform int gltf_clippingPlanesLength; \n' + + 'uniform bool gltf_clippingPlanesCombineRegions; \n' + 'uniform vec4 gltf_clippingPlanes[czm_maxClippingPlanes]; \n' + + 'uniform vec4 gltf_clippingPlanesEdgeStyle; \n' + 'void main() \n' + '{ \n' + ' gltf_clip_main(); \n' + - ' if (gltf_clippingPlanesEnabled) { \n' + - ' czm_discardIfClipped(gltf_clippingPlanes, gltf_clippingPlanesLength); \n' + + ' if (gltf_clippingPlanesLength > 0) \n' + + ' { \n' + + ' float clipDistance; \n' + + ' if (gltf_clippingPlanesCombineRegions) \n' + + ' { \n' + + ' clipDistance = czm_discardIfClippedCombineRegions(gltf_clippingPlanes, gltf_clippingPlanesLength); \n' + + ' } \n' + + ' else \n' + + ' { \n' + + ' clipDistance = czm_discardIfClipped(gltf_clippingPlanes, gltf_clippingPlanesLength); \n' + + ' } \n' + + ' \n' + + ' vec4 clippingPlanesEdgeColor = vec4(1.0); \n' + + ' clippingPlanesEdgeColor.rgb = gltf_clippingPlanesEdgeStyle.rgb; \n' + + ' float clippingPlanesEdgeWidth = gltf_clippingPlanesEdgeStyle.a; \n' + + ' if (clipDistance > 0.0 && clipDistance < clippingPlanesEdgeWidth) \n' + + ' { \n' + + ' gl_FragColor = clippingPlanesEdgeColor; \n' + + ' } \n' + ' } \n' + '} \n'; @@ -4281,10 +4304,10 @@ define([ } function updateClippingPlanes(model) { - var length = 0; var clippingPlanes = model.clippingPlanes; - if (defined(clippingPlanes)) { - length = clippingPlanes.length; + var length = 0; + if (defined(clippingPlanes) && clippingPlanes.enabled) { + length = clippingPlanes.planes.length; } if (model._packedClippingPlanes.length !== length) { @@ -4702,7 +4725,6 @@ define([ this._cesiumAnimationsDirty = false; this._dirty = false; var modelMatrix = this.modelMatrix; - var modelViewChanged = context.uniformState._modelViewDirty; var modeChanged = frameState.mode !== this._mode; this._mode = frameState.mode; @@ -4716,8 +4738,6 @@ define([ modeChanged; if (modelTransformChanged || justLoaded) { - modelViewChanged = true; - Matrix4.clone(modelMatrix, this._modelMatrix); updateClamping(this); @@ -4742,8 +4762,9 @@ define([ } } - if (this.clippingPlanesEnabled && modelViewChanged) { - Matrix4.multiply(context.uniformState.view3D, this._computedModelMatrix, this._modelViewMatrix); + var clippingPlanes = this.clippingPlanes; + if (defined(clippingPlanes) && clippingPlanes.enabled) { + Matrix4.multiply(context.uniformState.view3D, modelMatrix, this._modelViewMatrix); } // Update modelMatrix throughout the graph as needed @@ -4767,7 +4788,7 @@ define([ updateShadows(this); updateColor(this, frameState); updateSilhouette(this, frameState); - updateClippingPlanes(this); + updateClippingPlanes(this, frameState); } if (justLoaded) { diff --git a/Source/Scene/PointCloud3DTileContent.js b/Source/Scene/PointCloud3DTileContent.js index 72cbcff5e7f9..d22a7049085d 100644 --- a/Source/Scene/PointCloud3DTileContent.js +++ b/Source/Scene/PointCloud3DTileContent.js @@ -117,6 +117,9 @@ define([ this._hasNormals = false; this._hasBatchIds = false; + // Used to regenerate shader when clipping on this tile changes + this._isClipped = true; + // Use per-point normals to hide back-facing points. this.backFaceCulling = false; this._backFaceCulling = false; @@ -138,7 +141,8 @@ define([ this._features = undefined; - this._packedClippingPlanes = []; + this._packedClippingPlanes = []; + this._modelViewMatrix = Matrix4.clone(Matrix4.IDENTITY); /** * @inheritdoc Cesium3DTileContent#featurePropertiesDirty @@ -516,8 +520,7 @@ define([ } } - var scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0); - var scratchMatrix = new Matrix4(); + var clippingPlanes = content._tileset.clippingPlanes; var uniformMap = { u_pointSizeAndTilesetTime : function() { scratchPointSizeAndTilesetTime.x = content._pointSize; @@ -530,28 +533,20 @@ define([ u_constantColor : function() { return content._constantColor; }, - u_clippingPlanesEnabled : function() { - return content._tileset.clippingPlanesEnabled && content._tile._isClipped; - }, u_clippingPlanesLength : function() { return content._packedClippingPlanes.length; }, u_clippingPlanes : function() { - Matrix4.multiply(context.uniformState.view3D, content._modelMatrix, scratchMatrix); - - var planes = content._tileset.clippingPlanes; - var packedPlanes = content._packedClippingPlanes; - var length = packedPlanes.length; - for (var i = 0; i < length; ++i) { - var plane = planes[i]; - var packedPlane = packedPlanes[i]; - - Plane.transform(plane, scratchMatrix, scratchPlane); - - Cartesian3.clone(scratchPlane.normal, packedPlane); - packedPlane.w = scratchPlane.distance; + return content._packedClippingPlanes; + }, + u_clippingPlanesEdgeStyle : function() { + if (!defined(clippingPlanes)) { + return Color.WHITE.withAlpha(0.0); } - return packedPlanes; + + var style = Color.clone(clippingPlanes.edgeColor); + style.alpha = clippingPlanes.edgeWidth; + return style; } }; @@ -818,6 +813,7 @@ define([ var hasBatchIds = content._hasBatchIds; var backFaceCulling = content._backFaceCulling; var vertexArray = content._drawCommand.vertexArray; + var clippingPlanes = content._tileset.clippingPlanes; var colorStyleFunction; var showStyleFunction; @@ -846,6 +842,7 @@ define([ var hasColorStyle = defined(colorStyleFunction); var hasShowStyle = defined(showStyleFunction); var hasPointSizeStyle = defined(pointSizeStyleFunction); + var hasClippedContent = defined(clippingPlanes) && clippingPlanes.enabled && content._tile._isClipped; // Get the properties in use by the style var styleableProperties = []; @@ -1060,17 +1057,31 @@ define([ vs += '} \n'; - var fs = 'varying vec4 v_color; \n' + - 'uniform bool u_clippingPlanesEnabled; \n' + - 'uniform int u_clippingPlanesLength;' + - 'uniform vec4 u_clippingPlanes[czm_maxClippingPlanes]; \n' + - 'void main() \n' + - '{ \n' + - ' if (u_clippingPlanesEnabled) { \n' + - ' czm_discardIfClipped(u_clippingPlanes, u_clippingPlanesLength); \n' + - ' } \n' + - ' gl_FragColor = v_color; \n' + - '} \n'; + var fs = 'varying vec4 v_color; \n'; + + if (hasClippedContent) { + fs += 'uniform int u_clippingPlanesLength;' + + 'uniform vec4 u_clippingPlanes[czm_maxClippingPlanes]; \n' + + 'uniform vec4 u_clippingPlanesEdgeStyle; \n'; + } + + fs += 'void main() \n' + + '{ \n' + + ' gl_FragColor = v_color; \n'; + + if (hasClippedContent) { + var clippingFunction = clippingPlanes.combineClippingRegions ? 'czm_discardIfClippedCombineRegions' : 'czm_discardIfClipped'; + fs += ' float clipDistance = ' + clippingFunction + '(u_clippingPlanes, u_clippingPlanesLength); \n' + + ' vec4 clippingPlanesEdgeColor = vec4(1.0); \n' + + ' clippingPlanesEdgeColor.rgb = u_clippingPlanesEdgeStyle.rgb; \n' + + ' float clippingPlanesEdgeWidth = u_clippingPlanesEdgeStyle.a; \n' + + ' if (clipDistance > 0.0 && clipDistance < clippingPlanesEdgeWidth) \n' + + ' { \n' + + ' gl_FragColor = clippingPlanesEdgeColor; \n' + + ' } \n'; + } + + fs += '} \n'; var drawVS = vs; var drawFS = fs; @@ -1203,10 +1214,15 @@ define([ this._mode = frameState.mode; + var context = frameState.context; + // update clipping planes + var clippingPlanes = this._tileset.clippingPlanes; + var clippingEnabled = defined(clippingPlanes) && clippingPlanes.enabled && this._tile._isClipped; var length = 0; - if (defined(this._tileset.clippingPlanes)) { - length = this._tileset.clippingPlanes.length; + if (clippingEnabled) { + length = clippingPlanes.planes.length; + Matrix4.multiply(context.uniformState.view3D, modelMatrix, this._modelViewMatrix); } if (this._packedClippingPlanes.length !== length) { @@ -1226,6 +1242,16 @@ define([ this._parsedContent = undefined; // Unload } + if (clippingEnabled) { + clippingPlanes.transformAndPackPlanes(this._modelViewMatrix, this._packedClippingPlanes); + } + + var isClipped = this._tile._isClipped; + if (this._isClipped !== isClipped) { + this._isClipped = isClipped; + createShaders(this, frameState, tileset.style); + } + if (updateModelMatrix) { Matrix4.clone(modelMatrix, this._modelMatrix); if (defined(this._rtcCenter)) { diff --git a/Source/Shaders/Builtin/Functions/discardIfClipped.glsl b/Source/Shaders/Builtin/Functions/discardIfClipped.glsl index 0a59ae5f7e64..dab7cae02566 100644 --- a/Source/Shaders/Builtin/Functions/discardIfClipped.glsl +++ b/Source/Shaders/Builtin/Functions/discardIfClipped.glsl @@ -1,36 +1,45 @@ /** - * Clip a fragment by an array of clipping planes. + * Clip a fragment by an array of clipping planes. Clipping plane regions are not combined, therefore if a fragment is + * clipped by any of the planes, it is discarded. * * @name czm_discardIfClipped * @glslFunction * * @param {vec4[]} clippingPlanes The array of planes used to clip, defined in eyespace. * @param {int} clippingPlanesLength The number of planes in the array of clipping planes. + * @returns {float} The distance away from a clipped fragment, in eyespace */ -void czm_discardIfClipped (vec4[czm_maxClippingPlanes] clippingPlanes, int clippingPlanesLength) +float czm_discardIfClipped(vec4[czm_maxClippingPlanes] clippingPlanes, int clippingPlanesLength) { if (clippingPlanesLength > 0) { - bool clipped = false; vec4 position = czm_windowToEyeCoordinates(gl_FragCoord); vec3 clipNormal = vec3(0.0); vec3 clipPosition = vec3(0.0); + float clipAmount = 0.0; + float pixelWidth = czm_metersPerPixel(position); for (int i = 0; i < czm_maxClippingPlanes; ++i) { - if (i == clippingPlanesLength || clipped) + if (i == clippingPlanesLength) { break; } clipNormal = clippingPlanes[i].xyz; clipPosition = -clippingPlanes[i].w * clipNormal; - clipped = dot(clipNormal, (position.xyz - clipPosition)) <= 0.0; - } - if (clipped) - { - discard; + float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth; + clipAmount = max(amount, clipAmount); + + if (amount <= 0.0) + { + discard; + } } + + return clipAmount; } + + return 0.0; } diff --git a/Source/Shaders/Builtin/Functions/discardIfClippedCombineRegions.glsl b/Source/Shaders/Builtin/Functions/discardIfClippedCombineRegions.glsl new file mode 100644 index 000000000000..9d69e3d49838 --- /dev/null +++ b/Source/Shaders/Builtin/Functions/discardIfClippedCombineRegions.glsl @@ -0,0 +1,48 @@ +/** + * Clip a fragment by an array of clipping planes. Clipping plane regions are combined, so a fragment must be clipped + * by all of the planes to be discarded. + * + * @name czm_discardIfClipped + * @glslFunction + * + * @param {vec4[]} clippingPlanes The array of planes used to clip, defined in eyespace. + * @param {int} clippingPlanesLength The number of planes in the array of clipping planes. + * @returns {float} The distance away from a clipped fragment, in eyespace + */ +float czm_discardIfClippedCombineRegions(vec4[czm_maxClippingPlanes] clippingPlanes, int clippingPlanesLength) +{ + if (clippingPlanesLength > 0) + { + bool clipped = true; + vec4 position = czm_windowToEyeCoordinates(gl_FragCoord); + vec3 clipNormal = vec3(0.0); + vec3 clipPosition = vec3(0.0); + float clipAmount = 0.0; + float pixelWidth = czm_metersPerPixel(position); + + for (int i = 0; i < czm_maxClippingPlanes; ++i) + { + if (i == clippingPlanesLength) + { + break; + } + + clipNormal = clippingPlanes[i].xyz; + clipPosition = -clippingPlanes[i].w * clipNormal; + + float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth; + clipAmount = max(amount, clipAmount); + + clipped = clipped && (amount <= 0.0); + } + + if (clipped) + { + discard; + } + + return clipAmount; + } + + return 0.0; +} diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl index 34f8bc02c4ca..056d6a2bf8d5 100644 --- a/Source/Shaders/GlobeFS.glsl +++ b/Source/Shaders/GlobeFS.glsl @@ -52,6 +52,12 @@ uniform sampler2D u_oceanNormalMap; uniform vec2 u_lightingFadeDistance; #endif +#ifdef ENABLE_CLIPPING_PLANES +uniform int u_clippingPlanesLength; +uniform vec4 u_clippingPlanes[czm_maxClippingPlanes]; +uniform vec4 u_clippingPlanesEdgeStyle; +#endif + varying vec3 v_positionMC; varying vec3 v_positionEC; varying vec3 v_textureCoordinates; @@ -141,6 +147,14 @@ vec4 computeWaterColor(vec3 positionEyeCoordinates, vec2 textureCoordinates, mat void main() { +#ifdef ENABLE_CLIPPING_PLANES + #ifdef COMBINE_CLIPPING_REGIONS + float clipDistance = czm_discardIfClippedCombineRegions(u_clippingPlanes, u_clippingPlanesLength); + #else + float clipDistance = czm_discardIfClipped(u_clippingPlanes, u_clippingPlanesLength); + #endif +#endif + // The clamp below works around an apparent bug in Chrome Canary v23.0.1241.0 // where the fragment shader sees textures coordinates < 0.0 and > 1.0 for the // fragments on the edges of tiles even though the vertex shader is outputting @@ -195,6 +209,15 @@ void main() vec4 finalColor = color; #endif +#ifdef ENABLE_CLIPPING_PLANES + vec4 clippingPlanesEdgeColor = vec4(1.0); + clippingPlanesEdgeColor.rgb = u_clippingPlanesEdgeStyle.rgb; + float clippingPlanesEdgeWidth = u_clippingPlanesEdgeStyle.a; + + if (clipDistance < clippingPlanesEdgeWidth) { + finalColor = clippingPlanesEdgeColor; + } +#endif #ifdef FOG const float fExposure = 2.0; diff --git a/Source/Workers/createPlaneGeometry.js b/Source/Workers/createPlaneGeometry.js new file mode 100644 index 000000000000..23785ac71d65 --- /dev/null +++ b/Source/Workers/createPlaneGeometry.js @@ -0,0 +1,15 @@ +define([ + '../Core/PlaneGeometry', + '../Core/defined' + ], function( + PlaneGeometry, + defined) { + 'use strict'; + + return function(planeGeometry, offset) { + if (defined(offset)) { + planeGeometry = PlaneGeometry.unpack(planeGeometry, offset); + } + return PlaneGeometry.createGeometry(planeGeometry); + }; +}); diff --git a/Source/Workers/createPlaneOutlineGeometry.js b/Source/Workers/createPlaneOutlineGeometry.js new file mode 100644 index 000000000000..c2f5a7a7ff4a --- /dev/null +++ b/Source/Workers/createPlaneOutlineGeometry.js @@ -0,0 +1,15 @@ +define([ + '../Core/PlaneOutlineGeometry', + '../Core/defined' + ], function( + PlaneOutlineGeometry, + defined) { + 'use strict'; + + return function(planeGeometry, offset) { + if (defined(offset)) { + planeGeometry = PlaneOutlineGeometry.unpack(planeGeometry, offset); + } + return PlaneOutlineGeometry.createGeometry(planeGeometry); + }; +}); diff --git a/Specs/Core/PlaneGeometrySpec.js b/Specs/Core/PlaneGeometrySpec.js new file mode 100644 index 000000000000..0a5561fb4389 --- /dev/null +++ b/Specs/Core/PlaneGeometrySpec.js @@ -0,0 +1,44 @@ +defineSuite([ + 'Core/PlaneGeometry', + 'Core/Cartesian3', + 'Core/VertexFormat', + 'Specs/createPackableSpecs' + ], function( + PlaneGeometry, + Cartesian3, + VertexFormat, + createPackableSpecs) { + 'use strict'; + + it('constructor creates optimized number of positions for VertexFormat.POSITIONS_ONLY', function() { + var m = PlaneGeometry.createGeometry(new PlaneGeometry({ + vertexFormat : VertexFormat.POSITION_ONLY + })); + + expect(m.attributes.position.values.length).toEqual(4 * 3); // 4 corners + expect(m.indices.length).toEqual(4 * 3); // 2 sides x 2 triangles per side + }); + + it('constructor computes all vertex attributes', function() { + var m = PlaneGeometry.createGeometry(new PlaneGeometry({ + vertexFormat : VertexFormat.ALL + })); + + var numVertices = 8; //2 sides x 4 corners + var numTriangles = 4; //2 sides x 2 triangles per side + expect(m.attributes.position.values.length).toEqual(numVertices * 3); + expect(m.attributes.normal.values.length).toEqual(numVertices * 3); + expect(m.attributes.tangent.values.length).toEqual(numVertices * 3); + expect(m.attributes.bitangent.values.length).toEqual(numVertices * 3); + expect(m.attributes.st.values.length).toEqual(numVertices * 2); + + expect(m.indices.length).toEqual(numTriangles * 3); + + expect(m.boundingSphere.center).toEqual(Cartesian3.ZERO); + expect(m.boundingSphere.radius).toEqual(Math.sqrt(2.0)/2.0); + }); + + createPackableSpecs(PlaneGeometry, new PlaneGeometry({ + vertexFormat : VertexFormat.POSITION_AND_NORMAL + }), [1.0, 1.0, 0.0, 0.0, 0.0, 0.0]); +}); diff --git a/Specs/Core/PlaneOutlineGeometrySpec.js b/Specs/Core/PlaneOutlineGeometrySpec.js new file mode 100644 index 000000000000..f1f1d49f6dc0 --- /dev/null +++ b/Specs/Core/PlaneOutlineGeometrySpec.js @@ -0,0 +1,19 @@ +defineSuite([ + 'Core/PlaneOutlineGeometry', + 'Core/Cartesian3', + 'Specs/createPackableSpecs' + ], function( + PlaneOutlineGeometry, + Cartesian3, + createPackableSpecs) { + 'use strict'; + + it('constructor creates positions', function() { + var m = PlaneOutlineGeometry.createGeometry(new PlaneOutlineGeometry()); + + expect(m.attributes.position.values.length).toEqual(4 * 3); + expect(m.indices.length).toEqual(4 * 2); + }); + + createPackableSpecs(PlaneOutlineGeometry, new PlaneOutlineGeometry(), []); +}); diff --git a/Specs/Core/PlaneSpec.js b/Specs/Core/PlaneSpec.js index 8e28801b179f..9c9419457b30 100644 --- a/Specs/Core/PlaneSpec.js +++ b/Specs/Core/PlaneSpec.js @@ -1,11 +1,17 @@ defineSuite([ 'Core/Plane', 'Core/Cartesian3', - 'Core/Cartesian4' + 'Core/Cartesian4', + 'Core/Math', + 'Core/Matrix3', + 'Core/Matrix4' ], function( Plane, Cartesian3, - Cartesian4) { + Cartesian4, + CesiumMath, + Matrix3, + Matrix4) { 'use strict'; it('constructs', function() { @@ -122,4 +128,33 @@ defineSuite([ return Plane.getPointDistance(plane, undefined); }).toThrowDeveloperError(); }); + + it('transforms a plane according to a transform', function() { + var normal = new Cartesian3(1.0, 2.0, 3.0); + normal = Cartesian3.normalize(normal, normal); + var plane = new Plane(normal, 12.34); + + var transform = Matrix4.fromUniformScale(2.0); + transform = Matrix4.multiplyByMatrix3(transform, Matrix3.fromRotationY(Math.PI), transform); + + var transformedPlane = Plane.transform(plane, transform); + expect(transformedPlane.distance).toEqual(-plane.distance * 2.0); + expect(transformedPlane.normal.x).toEqualEpsilon(-plane.normal.x, CesiumMath.EPSILON10); + expect(transformedPlane.normal.y).toEqual(plane.normal.y); + expect(transformedPlane.normal.z).toEqual(-plane.normal.z); + }); + + it('transform throws without a plane', function() { + var transform = Matrix4.IDENTITY; + expect(function() { + return Plane.transform(undefined, transform); + }).toThrowDeveloperError(); + }); + + it('transform throws without a transform', function() { + var plane = new Plane(Cartesian3.UNIT_X, 0.0); + expect(function() { + return Plane.transform(plane, undefined); + }).toThrowDeveloperError(); + }); }); diff --git a/Specs/DataSources/EntitySpec.js b/Specs/DataSources/EntitySpec.js index 5a4a4eb4738a..3ff931561312 100644 --- a/Specs/DataSources/EntitySpec.js +++ b/Specs/DataSources/EntitySpec.js @@ -19,6 +19,7 @@ defineSuite([ 'DataSources/LabelGraphics', 'DataSources/ModelGraphics', 'DataSources/PathGraphics', + 'DataSources/PlaneGraphics', 'DataSources/PointGraphics', 'DataSources/PolygonGraphics', 'DataSources/PolylineGraphics', @@ -46,6 +47,7 @@ defineSuite([ LabelGraphics, ModelGraphics, PathGraphics, + PlaneGraphics, PointGraphics, PolygonGraphics, PolylineGraphics, @@ -69,6 +71,7 @@ defineSuite([ expect(entity.model).toBeUndefined(); expect(entity.orientation).toBeUndefined(); expect(entity.path).toBeUndefined(); + expect(entity.plane).toBeUndefined(); expect(entity.point).toBeUndefined(); expect(entity.polygon).toBeUndefined(); expect(entity.polyline).toBeUndefined(); @@ -97,6 +100,7 @@ defineSuite([ model : {}, orientation : new Quaternion(1, 2, 3, 4), path : {}, + plane : {}, point : {}, polygon : {}, polyline : {}, @@ -126,6 +130,7 @@ defineSuite([ expect(entity.model).toBeInstanceOf(ModelGraphics); expect(entity.orientation).toBeInstanceOf(ConstantProperty); expect(entity.path).toBeInstanceOf(PathGraphics); + expect(entity.plane).toBeInstanceOf(PlaneGraphics); expect(entity.point).toBeInstanceOf(PointGraphics); expect(entity.polygon).toBeInstanceOf(PolygonGraphics); expect(entity.polyline).toBeInstanceOf(PolylineGraphics); diff --git a/Specs/DataSources/ModelGraphicsSpec.js b/Specs/DataSources/ModelGraphicsSpec.js index c5db38a7d99c..bda0e8cb9c73 100644 --- a/Specs/DataSources/ModelGraphicsSpec.js +++ b/Specs/DataSources/ModelGraphicsSpec.js @@ -9,6 +9,7 @@ defineSuite([ 'DataSources/NodeTransformationProperty', 'DataSources/PropertyBag', 'Scene/ColorBlendMode', + 'Scene/ClippingPlanesCollection', 'Scene/HeightReference', 'Scene/ShadowMode' ], function( @@ -22,6 +23,7 @@ defineSuite([ NodeTransformationProperty, PropertyBag, ColorBlendMode, + ClippingPlanesCollection, HeightReference, ShadowMode) { 'use strict'; @@ -43,6 +45,7 @@ defineSuite([ color : new Color(0.0, 1.0, 0.0, 0.2), colorBlendMode : ColorBlendMode.HIGHLIGHT, colorBlendAmount : 0.5, + clippingPlanes : new ClippingPlanesCollection(), nodeTransformations : { node1 : { translation : Cartesian3.UNIT_Y, @@ -67,6 +70,7 @@ defineSuite([ expect(model.color).toBeInstanceOf(ConstantProperty); expect(model.colorBlendMode).toBeInstanceOf(ConstantProperty); expect(model.colorBlendAmount).toBeInstanceOf(ConstantProperty); + expect(model.clippingPlanes).toBeInstanceOf(ConstantProperty); expect(model.runAnimations).toBeInstanceOf(ConstantProperty); expect(model.nodeTransformations).toBeInstanceOf(PropertyBag); @@ -85,6 +89,7 @@ defineSuite([ expect(model.color.getValue()).toEqual(options.color); expect(model.colorBlendMode.getValue()).toEqual(options.colorBlendMode); expect(model.colorBlendAmount.getValue()).toEqual(options.colorBlendAmount); + expect(model.clippingPlanes.getValue().planes).toEqual(options.clippingPlanes.planes); expect(model.runAnimations.getValue()).toEqual(options.runAnimations); var actualNodeTransformations = model.nodeTransformations.getValue(new JulianDate()); @@ -112,6 +117,7 @@ defineSuite([ source.color = new ConstantProperty(new Color(0.0, 1.0, 0.0, 0.2)); source.colorBlendMode = new ConstantProperty(ColorBlendMode.HIGHLIGHT); source.colorBlendAmount = new ConstantProperty(0.5); + source.clippingPlanes = new ConstantProperty(new ClippingPlanesCollection()); source.runAnimations = new ConstantProperty(true); source.nodeTransformations = { node1 : new NodeTransformationProperty({ @@ -141,6 +147,7 @@ defineSuite([ expect(target.color).toBe(source.color); expect(target.colorBlendMode).toBe(source.colorBlendMode); expect(target.colorBlendAmount).toBe(source.colorBlendAmount); + expect(target.clippingPlanes).toBe(source.clippingPlanes); expect(target.runAnimations).toBe(source.runAnimations); expect(target.nodeTransformations).toEqual(source.nodeTransformations); }); @@ -161,6 +168,7 @@ defineSuite([ source.color = new ConstantProperty(new Color(0.0, 1.0, 0.0, 0.2)); source.colorBlendMode = new ConstantProperty(ColorBlendMode.HIGHLIGHT); source.colorBlendAmount = new ConstantProperty(0.5); + source.clippingPlanes = new ConstantProperty(new ClippingPlanesCollection()); source.runAnimations = new ConstantProperty(true); source.nodeTransformations = { transform : new NodeTransformationProperty() @@ -180,6 +188,7 @@ defineSuite([ var color = new ConstantProperty(new Color(0.0, 1.0, 0.0, 0.2)); var colorBlendMode = new ConstantProperty(ColorBlendMode.HIGHLIGHT); var colorBlendAmount = new ConstantProperty(0.5); + var clippingPlanes = new ConstantProperty(new ClippingPlanesCollection()); var runAnimations = new ConstantProperty(true); var nodeTransformations = new PropertyBag({ transform : new NodeTransformationProperty() @@ -200,6 +209,7 @@ defineSuite([ target.color = color; target.colorBlendMode = colorBlendMode; target.colorBlendAmount = colorBlendAmount; + target.clippingPlanes = clippingPlanes; target.runAnimations = runAnimations; target.nodeTransformations = nodeTransformations; @@ -219,6 +229,7 @@ defineSuite([ expect(target.color).toBe(color); expect(target.colorBlendMode).toBe(colorBlendMode); expect(target.colorBlendAmount).toBe(colorBlendAmount); + expect(target.clippingPlanes).toBe(clippingPlanes); expect(target.runAnimations).toBe(runAnimations); expect(target.nodeTransformations).toBe(nodeTransformations); }); @@ -239,6 +250,7 @@ defineSuite([ source.color = new ConstantProperty(new Color(0.0, 1.0, 0.0, 0.2)); source.colorBlendMode = new ConstantProperty(ColorBlendMode.HIGHLIGHT); source.colorBlendAmount = new ConstantProperty(0.5); + source.clippingPlanes = new ConstantProperty(new ClippingPlanesCollection()); source.runAnimations = new ConstantProperty(true); source.nodeTransformations = { node1 : new NodeTransformationProperty(), @@ -260,6 +272,7 @@ defineSuite([ expect(result.color).toBe(source.color); expect(result.colorBlendMode).toBe(source.colorBlendMode); expect(result.colorBlendAmount).toBe(source.colorBlendAmount); + expect(result.clippingPlanes).toBe(source.clippingPlanes); expect(result.runAnimations).toBe(source.runAnimations); expect(result.nodeTransformations).toEqual(source.nodeTransformations); }); diff --git a/Specs/DataSources/ModelVisualizerSpec.js b/Specs/DataSources/ModelVisualizerSpec.js index 61fa33d7598d..d76cbb795e46 100644 --- a/Specs/DataSources/ModelVisualizerSpec.js +++ b/Specs/DataSources/ModelVisualizerSpec.js @@ -6,6 +6,7 @@ defineSuite([ 'Core/DistanceDisplayCondition', 'Core/JulianDate', 'Core/Matrix4', + 'Core/Plane', 'Core/Quaternion', 'Core/Transforms', 'DataSources/BoundingSphereState', @@ -14,6 +15,7 @@ defineSuite([ 'DataSources/EntityCollection', 'DataSources/ModelGraphics', 'DataSources/NodeTransformationProperty', + 'Scene/ClippingPlanesCollection', 'Scene/Globe', 'Specs/createScene', 'Specs/pollToPromise' @@ -25,6 +27,7 @@ defineSuite([ DistanceDisplayCondition, JulianDate, Matrix4, + Plane, Quaternion, Transforms, BoundingSphereState, @@ -33,6 +36,7 @@ defineSuite([ EntityCollection, ModelGraphics, NodeTransformationProperty, + ClippingPlanesCollection, Globe, createScene, pollToPromise) { @@ -136,6 +140,13 @@ defineSuite([ }; model.nodeTransformations = nodeTransforms; + var clippingPlanes = new ClippingPlanesCollection({ + planes: [ + new Plane(Cartesian3.UNIT_X, 0.0) + ] + }); + model.clippingPlanes = new ConstantProperty(clippingPlanes); + var testObject = entityCollection.getOrCreateEntity('test'); testObject.position = new ConstantPositionProperty(Cartesian3.fromDegrees(1, 2, 3)); testObject.model = model; @@ -151,6 +162,7 @@ defineSuite([ expect(primitive.minimumPixelSize).toEqual(24.0); expect(primitive.modelMatrix).toEqual(Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(1, 2, 3), scene.globe.ellipsoid)); expect(primitive.distanceDisplayCondition).toEqual(new DistanceDisplayCondition(10.0, 100.0)); + expect(primitive.clippingPlanes.planes).toEqual(clippingPlanes.planes); // wait till the model is loaded before we can check node transformations return pollToPromise(function() { diff --git a/Specs/DataSources/PlaneGeometryUpdaterSpec.js b/Specs/DataSources/PlaneGeometryUpdaterSpec.js new file mode 100644 index 000000000000..9f01ae6abc4c --- /dev/null +++ b/Specs/DataSources/PlaneGeometryUpdaterSpec.js @@ -0,0 +1,490 @@ +defineSuite([ + 'DataSources/PlaneGeometryUpdater', + 'Core/Cartesian2', + 'Core/Cartesian3', + 'Core/Color', + 'Core/ColorGeometryInstanceAttribute', + 'Core/DistanceDisplayCondition', + 'Core/DistanceDisplayConditionGeometryInstanceAttribute', + 'Core/JulianDate', + 'Core/Plane', + 'Core/ShowGeometryInstanceAttribute', + 'Core/TimeInterval', + 'Core/TimeIntervalCollection', + 'DataSources/PlaneGraphics', + 'DataSources/ColorMaterialProperty', + 'DataSources/ConstantPositionProperty', + 'DataSources/ConstantProperty', + 'DataSources/Entity', + 'DataSources/GridMaterialProperty', + 'DataSources/SampledProperty', + 'DataSources/TimeIntervalCollectionProperty', + 'Scene/PrimitiveCollection', + 'Scene/ShadowMode', + 'Specs/createDynamicGeometryBoundingSphereSpecs', + 'Specs/createDynamicProperty', + 'Specs/createScene' + ], function( + PlaneGeometryUpdater, + Cartesian2, + Cartesian3, + Color, + ColorGeometryInstanceAttribute, + DistanceDisplayCondition, + DistanceDisplayConditionGeometryInstanceAttribute, + JulianDate, + Plane, + ShowGeometryInstanceAttribute, + TimeInterval, + TimeIntervalCollection, + PlaneGraphics, + ColorMaterialProperty, + ConstantPositionProperty, + ConstantProperty, + Entity, + GridMaterialProperty, + SampledProperty, + TimeIntervalCollectionProperty, + PrimitiveCollection, + ShadowMode, + createDynamicGeometryBoundingSphereSpecs, + createDynamicProperty, + createScene) { + 'use strict'; + + var scene; + var time; + + beforeAll(function() { + scene = createScene(); + time = JulianDate.now(); + }); + + afterAll(function() { + scene.destroyForSpecs(); + }); + + function createBasicPlane() { + var planeGraphics = new PlaneGraphics(); + planeGraphics.plane = new ConstantProperty(new Plane(Cartesian3.UNIT_X, 0.0)); + planeGraphics.dimensions = new ConstantProperty(new Cartesian2(1.0, 2.0)); + var entity = new Entity(); + entity.position = new ConstantPositionProperty(Cartesian3.fromDegrees(0, 0, 0)); + entity.plane = planeGraphics; + return entity; + } + + it('Constructor sets expected defaults', function() { + var entity = new Entity(); + var updater = new PlaneGeometryUpdater(entity, scene); + + expect(updater.isDestroyed()).toBe(false); + expect(updater.entity).toBe(entity); + expect(updater.isClosed).toBe(true); + expect(updater.fillEnabled).toBe(false); + expect(updater.fillMaterialProperty).toBe(undefined); + expect(updater.outlineEnabled).toBe(false); + expect(updater.hasConstantFill).toBe(true); + expect(updater.hasConstantOutline).toBe(true); + expect(updater.outlineColorProperty).toBe(undefined); + expect(updater.outlineWidth).toBe(1.0); + expect(updater.shadowsProperty).toBe(undefined); + expect(updater.distanceDisplayConditionProperty).toBe(undefined); + expect(updater.isDynamic).toBe(false); + expect(updater.isOutlineVisible(time)).toBe(false); + expect(updater.isFilled(time)).toBe(false); + updater.destroy(); + expect(updater.isDestroyed()).toBe(true); + }); + + it('No geometry available when plane is undefined ', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + entity.plane = undefined; + + expect(updater.fillEnabled).toBe(false); + expect(updater.outlineEnabled).toBe(false); + expect(updater.isDynamic).toBe(false); + }); + + it('No geometry available when not filled or outline.', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + entity.plane.fill = new ConstantProperty(false); + entity.plane.outline = new ConstantProperty(false); + + expect(updater.fillEnabled).toBe(false); + expect(updater.outlineEnabled).toBe(false); + expect(updater.isDynamic).toBe(false); + }); + + it('Values correct when using default graphics', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + + expect(updater.isClosed).toBe(true); + expect(updater.fillEnabled).toBe(true); + expect(updater.fillMaterialProperty).toEqual(new ColorMaterialProperty(Color.WHITE)); + expect(updater.outlineEnabled).toBe(false); + expect(updater.hasConstantFill).toBe(true); + expect(updater.hasConstantOutline).toBe(true); + expect(updater.outlineColorProperty).toBe(undefined); + expect(updater.outlineWidth).toBe(1.0); + expect(updater.shadowsProperty).toEqual(new ConstantProperty(ShadowMode.DISABLED)); + expect(updater.distanceDisplayConditionProperty).toEqual(new ConstantProperty(new DistanceDisplayCondition())); + expect(updater.isDynamic).toBe(false); + }); + + it('Plane material is correctly exposed.', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + entity.plane.material = new GridMaterialProperty(Color.BLUE); + expect(updater.fillMaterialProperty).toBe(entity.plane.material); + }); + + it('A time-varying outlineWidth causes geometry to be dynamic', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + entity.plane.outlineWidth = createDynamicProperty(); + expect(updater.isDynamic).toBe(true); + }); + + it('A time-varying plane causes geometry to be dynamic', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + entity.plane.plane = createDynamicProperty(); + + expect(updater.isDynamic).toBe(true); + }); + + it('A time-varying dimensions causes geometry to be dynamic', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + entity.plane.dimensions = createDynamicProperty(); + + expect(updater.isDynamic).toBe(true); + }); + + function validateGeometryInstance(options) { + var entity = createBasicPlane(); + + var plane = entity.plane; + plane.show = new ConstantProperty(options.show); + plane.fill = new ConstantProperty(options.fill); + plane.material = options.material; + plane.outline = new ConstantProperty(options.outline); + plane.outlineColor = new ConstantProperty(options.outlineColor); + plane.plane = new ConstantProperty(options.plane); + plane.dimensions = new ConstantProperty(options.dimensions); + plane.distanceDisplayCondition = options.distanceDisplayCondition; + + var updater = new PlaneGeometryUpdater(entity, scene); + + var instance; + var attributes; + if (options.fill) { + instance = updater.createFillGeometryInstance(time); + + attributes = instance.attributes; + if (options.material instanceof ColorMaterialProperty) { + expect(attributes.color.value).toEqual(ColorGeometryInstanceAttribute.toValue(options.material.color.getValue(time))); + } else { + expect(attributes.color).toBeUndefined(); + } + expect(attributes.show.value).toEqual(ShowGeometryInstanceAttribute.toValue(options.fill)); + if (options.distanceDisplayCondition) { + expect(attributes.distanceDisplayCondition.value).toEqual(DistanceDisplayConditionGeometryInstanceAttribute.toValue(options.distanceDisplayCondition)); + } + } + + if (options.outline) { + instance = updater.createOutlineGeometryInstance(time); + + attributes = instance.attributes; + expect(attributes.color.value).toEqual(ColorGeometryInstanceAttribute.toValue(options.outlineColor)); + expect(attributes.show.value).toEqual(ShowGeometryInstanceAttribute.toValue(options.fill)); + if (options.distanceDisplayCondition) { + expect(attributes.distanceDisplayCondition.value).toEqual(DistanceDisplayConditionGeometryInstanceAttribute.toValue(options.distanceDisplayCondition)); + } + } + } + + it('Creates expected per-color geometry', function() { + validateGeometryInstance({ + show : true, + material : new ColorMaterialProperty(Color.RED), + fill : true, + outline : true, + outlineColor : Color.BLUE, + plane : new Plane(Cartesian3.UNIT_X, 0.0), + dimensions : new Cartesian2(1.0, 2.0) + }); + }); + + it('Creates expected per-material geometry', function() { + validateGeometryInstance({ + show : true, + material : new GridMaterialProperty(), + fill : true, + outline : true, + outlineColor : Color.BLUE, + plane : new Plane(Cartesian3.UNIT_X, 0.0), + dimensions : new Cartesian2(1.0, 2.0) + }); + }); + + it('Creates expected distance display condition geometry', function() { + validateGeometryInstance({ + show : true, + material : new ColorMaterialProperty(Color.RED), + fill : true, + outline : true, + outlineColor : Color.BLUE, + plane : new Plane(Cartesian3.UNIT_X, 0.0), + dimensions : new Cartesian2(1.0, 2.0), + distanceDisplayCondition : new DistanceDisplayCondition(10.0, 100.0) + }); + }); + + it('Correctly exposes outlineWidth', function() { + var entity = createBasicPlane(); + entity.plane.outlineWidth = new ConstantProperty(8); + var updater = new PlaneGeometryUpdater(entity, scene); + expect(updater.outlineWidth).toBe(8); + }); + + it('Attributes have expected values at creation time', function() { + var time1 = new JulianDate(0, 0); + var time2 = new JulianDate(10, 0); + var time3 = new JulianDate(20, 0); + + var fill = new TimeIntervalCollectionProperty(); + fill.intervals.addInterval(new TimeInterval({ + start : time1, + stop : time2, + data : false + })); + fill.intervals.addInterval(new TimeInterval({ + start : time2, + stop : time3, + isStartIncluded : false, + data : true + })); + + var colorMaterial = new ColorMaterialProperty(); + colorMaterial.color = new SampledProperty(Color); + colorMaterial.color.addSample(time, Color.YELLOW); + colorMaterial.color.addSample(time2, Color.BLUE); + colorMaterial.color.addSample(time3, Color.RED); + + var outline = new TimeIntervalCollectionProperty(); + outline.intervals.addInterval(new TimeInterval({ + start : time1, + stop : time2, + data : false + })); + outline.intervals.addInterval(new TimeInterval({ + start : time2, + stop : time3, + isStartIncluded : false, + data : true + })); + + var outlineColor = new SampledProperty(Color); + outlineColor.addSample(time, Color.BLUE); + outlineColor.addSample(time2, Color.RED); + outlineColor.addSample(time3, Color.YELLOW); + + var entity = createBasicPlane(); + entity.plane.fill = fill; + entity.plane.material = colorMaterial; + entity.plane.outline = outline; + entity.plane.outlineColor = outlineColor; + + var updater = new PlaneGeometryUpdater(entity, scene); + + var instance = updater.createFillGeometryInstance(time2); + var attributes = instance.attributes; + expect(attributes.color.value).toEqual(ColorGeometryInstanceAttribute.toValue(colorMaterial.color.getValue(time2))); + expect(attributes.show.value).toEqual(ShowGeometryInstanceAttribute.toValue(fill.getValue(time2))); + + instance = updater.createOutlineGeometryInstance(time2); + attributes = instance.attributes; + expect(attributes.color.value).toEqual(ColorGeometryInstanceAttribute.toValue(outlineColor.getValue(time2))); + expect(attributes.show.value).toEqual(ShowGeometryInstanceAttribute.toValue(outline.getValue(time2))); + }); + + it('createFillGeometryInstance obeys Entity.show is false.', function() { + var entity = createBasicPlane(); + entity.show = false; + entity.plane.fill = true; + var updater = new PlaneGeometryUpdater(entity, scene); + var instance = updater.createFillGeometryInstance(new JulianDate()); + var attributes = instance.attributes; + expect(attributes.show.value).toEqual(ShowGeometryInstanceAttribute.toValue(false)); + }); + + it('createOutlineGeometryInstance obeys Entity.show is false.', function() { + var entity = createBasicPlane(); + entity.show = false; + entity.plane.outline = true; + var updater = new PlaneGeometryUpdater(entity, scene); + var instance = updater.createFillGeometryInstance(new JulianDate()); + var attributes = instance.attributes; + expect(attributes.show.value).toEqual(ShowGeometryInstanceAttribute.toValue(false)); + }); + + it('dynamic updater sets properties', function() { + var entity = new Entity(); + entity.position = new ConstantPositionProperty(Cartesian3.fromDegrees(0, 0, 0)); + var planeGraphics = new PlaneGraphics(); + entity.plane = planeGraphics; + + planeGraphics.show = createDynamicProperty(true); + planeGraphics.plane = createDynamicProperty(new Plane(Cartesian3.UNIT_X, 0.0)); + planeGraphics.dimensions = createDynamicProperty(new Cartesian3(1.0, 2.0)); + planeGraphics.outline = createDynamicProperty(true); + planeGraphics.fill = createDynamicProperty(true); + + var updater = new PlaneGeometryUpdater(entity, scene); + var primitives = new PrimitiveCollection(); + var dynamicUpdater = updater.createDynamicUpdater(primitives); + expect(primitives.length).toBe(0); + + dynamicUpdater.update(JulianDate.now()); + expect(primitives.length).toBe(2); + expect(dynamicUpdater.isDestroyed()).toBe(false); + + expect(dynamicUpdater._options.id).toBe(entity); + expect(dynamicUpdater._options.plane).toEqual(planeGraphics.plane.getValue()); + expect(dynamicUpdater._options.dimensions).toEqual(planeGraphics.dimensions.getValue()); + + entity.show = false; + dynamicUpdater.update(JulianDate.now()); + expect(primitives.length).toBe(0); + entity.show = true; + + planeGraphics.show.setValue(false); + dynamicUpdater.update(JulianDate.now()); + expect(primitives.length).toBe(0); + + planeGraphics.show.setValue(true); + planeGraphics.fill.setValue(false); + dynamicUpdater.update(JulianDate.now()); + expect(primitives.length).toBe(1); + + planeGraphics.fill.setValue(true); + planeGraphics.outline.setValue(false); + dynamicUpdater.update(JulianDate.now()); + expect(primitives.length).toBe(1); + + dynamicUpdater.destroy(); + expect(primitives.length).toBe(0); + updater.destroy(); + }); + + it('geometryChanged event is raised when expected', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + var listener = jasmine.createSpy('listener'); + updater.geometryChanged.addEventListener(listener); + + entity.plane.dimensions = new ConstantProperty(); + expect(listener.calls.count()).toEqual(1); + + entity.availability = new TimeIntervalCollection(); + expect(listener.calls.count()).toEqual(2); + + entity.plane.dimensions = undefined; + expect(listener.calls.count()).toEqual(3); + + //Since there's no valid geometry, changing another property should not raise the event. + entity.plane.height = undefined; + + //Modifying an unrelated property should not have any effect. + entity.viewFrom = new ConstantProperty(Cartesian3.UNIT_X); + expect(listener.calls.count()).toEqual(3); + }); + + it('createFillGeometryInstance throws if object is not filled', function() { + var entity = new Entity(); + var updater = new PlaneGeometryUpdater(entity, scene); + expect(function() { + return updater.createFillGeometryInstance(time); + }).toThrowDeveloperError(); + }); + + it('createFillGeometryInstance throws if no time provided', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + expect(function() { + return updater.createFillGeometryInstance(undefined); + }).toThrowDeveloperError(); + }); + + it('createOutlineGeometryInstance throws if object is not outlined', function() { + var entity = new Entity(); + var updater = new PlaneGeometryUpdater(entity, scene); + expect(function() { + return updater.createOutlineGeometryInstance(time); + }).toThrowDeveloperError(); + }); + + it('createOutlineGeometryInstance throws if no time provided', function() { + var entity = createBasicPlane(); + entity.plane.outline = new ConstantProperty(true); + var updater = new PlaneGeometryUpdater(entity, scene); + expect(function() { + return updater.createOutlineGeometryInstance(undefined); + }).toThrowDeveloperError(); + }); + + it('createDynamicUpdater throws if not dynamic', function() { + var entity = createBasicPlane(); + var updater = new PlaneGeometryUpdater(entity, scene); + expect(function() { + return updater.createDynamicUpdater(new PrimitiveCollection()); + }).toThrowDeveloperError(); + }); + + it('createDynamicUpdater throws if primitives undefined', function() { + var entity = createBasicPlane(); + entity.plane.dimensions = createDynamicProperty(new Cartesian2(1.0, 2.0 + )); + var updater = new PlaneGeometryUpdater(entity, scene); + expect(updater.isDynamic).toBe(true); + expect(function() { + return updater.createDynamicUpdater(undefined); + }).toThrowDeveloperError(); + }); + + it('dynamicUpdater.update throws if no time specified', function() { + var entity = createBasicPlane(); + entity.plane.dimensions = createDynamicProperty(new Cartesian2(1.0, 2.0)); + var updater = new PlaneGeometryUpdater(entity, scene); + var dynamicUpdater = updater.createDynamicUpdater(new PrimitiveCollection()); + expect(function() { + dynamicUpdater.update(undefined); + }).toThrowDeveloperError(); + }); + + it('Constructor throws if no Entity supplied', function() { + expect(function() { + return new PlaneGeometryUpdater(undefined, scene); + }).toThrowDeveloperError(); + }); + + it('Constructor throws if no scene supplied', function() { + var entity = createBasicPlane(); + expect(function() { + return new PlaneGeometryUpdater(entity, undefined); + }).toThrowDeveloperError(); + }); + + var entity = createBasicPlane(); + entity.plane.plane = createDynamicProperty(new Plane(Cartesian3.UNIT_X, 0.0)); + entity.plane.dimensions = createDynamicProperty(new Cartesian2(1.0, 2.0)); + createDynamicGeometryBoundingSphereSpecs(PlaneGeometryUpdater, entity, entity.plane, function() { + return scene; + }); +}, 'WebGL'); diff --git a/Specs/DataSources/PlaneGraphicsSpec.js b/Specs/DataSources/PlaneGraphicsSpec.js new file mode 100644 index 000000000000..ba79c4cf1dad --- /dev/null +++ b/Specs/DataSources/PlaneGraphicsSpec.js @@ -0,0 +1,179 @@ +defineSuite([ + 'DataSources/PlaneGraphics', + 'Core/Cartesian2', + 'Core/Cartesian3', + 'Core/Color', + 'Core/DistanceDisplayCondition', + 'Core/Plane', + 'DataSources/ColorMaterialProperty', + 'DataSources/ConstantProperty', + 'Scene/ShadowMode', + 'Specs/testDefinitionChanged', + 'Specs/testMaterialDefinitionChanged' + ], function( + PlaneGraphics, + Cartesian2, + Cartesian3, + Color, + DistanceDisplayCondition, + Plane, + ColorMaterialProperty, + ConstantProperty, + ShadowMode, + testDefinitionChanged, + testMaterialDefinitionChanged) { + 'use strict'; + + it('creates expected instance from raw assignment and construction', function() { + var options = { + material : Color.BLUE, + show : true, + fill : false, + outline : false, + outlineColor : Color.RED, + outlineWidth : 1, + plane : new Plane(Cartesian3.UNIT_X, 0.0), + dimensions : new Cartesian2(2.0, 3.0), + shadows : ShadowMode.DISABLED, + distanceDisplayCondition : new DistanceDisplayCondition(10.0, 100.0) + }; + + var plane = new PlaneGraphics(options); + expect(plane.material).toBeInstanceOf(ColorMaterialProperty); + expect(plane.show).toBeInstanceOf(ConstantProperty); + expect(plane.fill).toBeInstanceOf(ConstantProperty); + expect(plane.outline).toBeInstanceOf(ConstantProperty); + expect(plane.outlineColor).toBeInstanceOf(ConstantProperty); + expect(plane.outlineWidth).toBeInstanceOf(ConstantProperty); + expect(plane.plane).toBeInstanceOf(ConstantProperty); + expect(plane.dimensions).toBeInstanceOf(ConstantProperty); + expect(plane.shadows).toBeInstanceOf(ConstantProperty); + expect(plane.distanceDisplayCondition).toBeInstanceOf(ConstantProperty); + + expect(plane.material.color.getValue()).toEqual(options.material); + expect(plane.show.getValue()).toEqual(options.show); + expect(plane.fill.getValue()).toEqual(options.fill); + expect(plane.outline.getValue()).toEqual(options.outline); + expect(plane.outlineColor.getValue()).toEqual(options.outlineColor); + expect(plane.outlineWidth.getValue()).toEqual(options.outlineWidth); + expect(plane.plane.getValue()).toEqual(options.plane); + expect(plane.dimensions.getValue()).toEqual(options.dimensions); + expect(plane.shadows.getValue()).toEqual(options.shadows); + expect(plane.distanceDisplayCondition.getValue()).toEqual(options.distanceDisplayCondition); + }); + + it('merge assigns unassigned properties', function() { + var source = new PlaneGraphics(); + source.material = new ColorMaterialProperty(); + source.show = new ConstantProperty(); + source.fill = new ConstantProperty(); + source.outline = new ConstantProperty(); + source.outlineColor = new ConstantProperty(); + source.outlineWidth = new ConstantProperty(); + source.plane = new ConstantProperty(); + source.dimensions = new ConstantProperty(); + source.shadows = new ConstantProperty(ShadowMode.ENABLED); + source.distanceDisplayCondition = new ConstantProperty(new DistanceDisplayCondition(10.0, 100.0)); + + var target = new PlaneGraphics(); + target.merge(source); + + expect(target.material).toBe(source.material); + expect(target.show).toBe(source.show); + expect(target.fill).toBe(source.fill); + expect(target.outline).toBe(source.outline); + expect(target.outlineColor).toBe(source.outlineColor); + expect(target.outlineWidth).toBe(source.outlineWidth); + expect(target.plane).toBe(source.plane); + expect(target.dimensions).toBe(source.dimensions); + expect(target.shadows).toBe(source.shadows); + expect(target.distanceDisplayCondition).toBe(source.distanceDisplayCondition); + }); + + it('merge does not assign assigned properties', function() { + var source = new PlaneGraphics(); + + var material = new ColorMaterialProperty(); + var show = new ConstantProperty(); + var fill = new ConstantProperty(); + var outline = new ConstantProperty(); + var outlineColor = new ConstantProperty(); + var outlineWidth = new ConstantProperty(); + var plane = new ConstantProperty(); + var dimensions = new ConstantProperty(); + var shadows = new ConstantProperty(); + var distanceDisplayCondition = new ConstantProperty(); + + var target = new PlaneGraphics(); + target.material = material; + target.show = show; + target.fill = fill; + target.outline = outline; + target.outlineColor = outlineColor; + target.outlineWidth = outlineWidth; + target.plane = plane; + target.dimensions = dimensions; + target.shadows = shadows; + target.distanceDisplayCondition = distanceDisplayCondition; + + target.merge(source); + + expect(target.material).toBe(material); + expect(target.show).toBe(show); + expect(target.fill).toBe(fill); + expect(target.outline).toBe(outline); + expect(target.outlineColor).toBe(outlineColor); + expect(target.outlineWidth).toBe(outlineWidth); + expect(target.plane).toBe(plane); + expect(target.dimensions).toBe(dimensions); + expect(target.shadows).toBe(shadows); + expect(target.distanceDisplayCondition).toBe(distanceDisplayCondition); + }); + + it('clone works', function() { + var source = new PlaneGraphics(); + source.material = new ColorMaterialProperty(); + source.show = new ConstantProperty(); + source.fill = new ConstantProperty(); + source.outline = new ConstantProperty(); + source.outlineColor = new ConstantProperty(); + source.outlineWidth = new ConstantProperty(); + source.plane = new ConstantProperty(); + source.dimensions = new ConstantProperty(); + source.shadows = new ConstantProperty(); + source.distanceDisplayCondition = new ConstantProperty(); + + var result = source.clone(); + expect(result.material).toBe(source.material); + expect(result.show).toBe(source.show); + expect(result.fill).toBe(source.fill); + expect(result.outline).toBe(source.outline); + expect(result.outlineColor).toBe(source.outlineColor); + expect(result.outlineWidth).toBe(source.outlineWidth); + expect(result.plane).toBe(source.plane); + expect(result.dimensions).toBe(source.dimensions); + expect(result.shadows).toBe(source.shadows); + expect(result.distanceDisplayCondition).toBe(source.distanceDisplayCondition); + }); + + it('merge throws if source undefined', function() { + var target = new PlaneGraphics(); + expect(function() { + target.merge(undefined); + }).toThrowDeveloperError(); + }); + + it('raises definitionChanged when a property is assigned or modified', function() { + var property = new PlaneGraphics(); + testMaterialDefinitionChanged(property, 'material', Color.RED, Color.BLUE); + testDefinitionChanged(property, 'show', true, false); + testDefinitionChanged(property, 'fill', false, true); + testDefinitionChanged(property, 'outline', true, false); + testDefinitionChanged(property, 'outlineColor', Color.RED, Color.BLUE); + testDefinitionChanged(property, 'outlineWidth', 2, 3); + testDefinitionChanged(property, 'plane', new Plane(Cartesian3.UNIT_X, 0.0), new Plane(Cartesian3.UNIT_Z, 1.0)); + testDefinitionChanged(property, 'dimensions', new Cartesian2(0.0, 0.0), new Cartesian2(1.0, 1.0)); + testDefinitionChanged(property, 'shadows', ShadowMode.ENABLED, ShadowMode.DISABLED); + testDefinitionChanged(property, 'distanceDisplayCondition', new DistanceDisplayCondition(), new DistanceDisplayCondition(10.0, 100.0)); + }); +}); diff --git a/Specs/Scene/Batched3DModel3DTileContentSpec.js b/Specs/Scene/Batched3DModel3DTileContentSpec.js index c5e948a855ed..1f77b3fba8ff 100644 --- a/Specs/Scene/Batched3DModel3DTileContentSpec.js +++ b/Specs/Scene/Batched3DModel3DTileContentSpec.js @@ -5,7 +5,9 @@ defineSuite([ 'Core/deprecationWarning', 'Core/HeadingPitchRange', 'Core/HeadingPitchRoll', + 'Core/Plane', 'Core/Transforms', + 'Scene/ClippingPlanesCollection', 'Specs/Cesium3DTilesTester', 'Specs/createScene' ], function( @@ -15,7 +17,9 @@ defineSuite([ deprecationWarning, HeadingPitchRange, HeadingPitchRoll, + Plane, Transforms, + ClippingPlanesCollection, Cesium3DTilesTester, createScene) { 'use strict'; @@ -288,6 +292,34 @@ defineSuite([ }); }); + it('Updates model\'s clipping planes', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + var tile = tileset._root; + var content = tile.content; + var model = content._model; + + expect(model.clippingPlanes).toBeDefined(); + expect(model.clippingPlanes.planes.length).toBe(0); + expect(model.clippingPlanes.enabled).toBe(false); + + tileset.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + new Plane(Cartesian3.UNIT_X, 0.0) + ] + }); + content.update(tileset, scene.frameState); + + expect(model.clippingPlanes).toBeDefined(); + expect(model.clippingPlanes.planes.length).toBe(1); + expect(model.clippingPlanes.enabled).toBe(true); + + tile._isClipped = false; + content.update(tileset, scene.frameState); + + expect(model.clippingPlanes.enabled).toBe(false); + }); + }); + it('destroys', function() { return Cesium3DTilesTester.tileDestroys(scene, withoutBatchTableUrl); }); diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index c6f802b8f315..1c87581a8a78 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -7,11 +7,13 @@ defineSuite([ 'Core/defined', 'Core/getStringFromTypedArray', 'Core/HeadingPitchRange', + 'Core/Intersect', 'Core/JulianDate', 'Core/loadWithXhr', 'Core/Math', 'Core/Matrix4', 'Core/PerspectiveFrustum', + 'Core/Plane', 'Core/PrimitiveType', 'Core/RequestScheduler', 'Renderer/ClearCommand', @@ -22,6 +24,7 @@ defineSuite([ 'Scene/Cesium3DTileOptimizations', 'Scene/Cesium3DTileRefine', 'Scene/Cesium3DTileStyle', + 'Scene/ClippingPlanesCollection', 'Scene/CullFace', 'Specs/Cesium3DTilesTester', 'Specs/createScene', @@ -36,11 +39,13 @@ defineSuite([ defined, getStringFromTypedArray, HeadingPitchRange, + Intersect, JulianDate, loadWithXhr, CesiumMath, Matrix4, PerspectiveFrustum, + Plane, PrimitiveType, RequestScheduler, ClearCommand, @@ -51,6 +56,7 @@ defineSuite([ Cesium3DTileOptimizations, Cesium3DTileRefine, Cesium3DTileStyle, + ClippingPlanesCollection, CullFace, Cesium3DTilesTester, createScene, @@ -2807,4 +2813,135 @@ defineSuite([ }); }); + it('clipping planes cull hidden tiles', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var visibility = tileset._root.visibility(scene.frameState, CullingVolume.MASK_INSIDE); + + expect(visibility).not.toBe(CullingVolume.MASK_OUTSIDE); + + var plane = new Plane(Cartesian3.UNIT_Z, 100000000.0); + tileset.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + plane + ] + }); + + visibility = tileset._root.visibility(scene.frameState, CullingVolume.MASK_INSIDE); + + expect(visibility).toBe(CullingVolume.MASK_OUTSIDE); + + plane.distance = 0.0; + visibility = tileset._root.visibility(scene.frameState, CullingVolume.MASK_INSIDE); + + expect(visibility).not.toBe(CullingVolume.MASK_OUTSIDE); + }); + }); + + it('clipping planes cull hidden content', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var visibility = tileset._root.contentVisibility(scene.frameState); + + expect(visibility).not.toBe(Intersect.OUTSIDE); + + var plane = new Plane(Cartesian3.UNIT_Z, 100000000.0); + tileset.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + plane + ] + }); + + visibility = tileset._root.contentVisibility(scene.frameState); + + expect(visibility).toBe(Intersect.OUTSIDE); + + plane.distance = 0.0; + visibility = tileset._root.contentVisibility(scene.frameState); + + expect(visibility).not.toBe(Intersect.OUTSIDE); + }); + }); + + it('clipping planes cull tiles completely inside clipping region', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var statistics = tileset._statistics; + var root = tileset._root; + + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(5); + + tileset.update(scene.frameState); + + var plane = new Plane(Cartesian3.UNIT_Z, 0.0); + tileset.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + plane + ] + }); + + tileset.update(scene.frameState); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(5); + expect(root._isClipped).toBe(false); + + plane.distance = 4081630.311150717; // center + + tileset.update(scene.frameState); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(3); + expect(root._isClipped).toBe(true); + + plane.distance = 4081630.31115071 + 287.0736139905632; // center + radius + + tileset.update(scene.frameState); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(0); + expect(root._isClipped).toBe(true); + }); + }); + + it('clipping planes cull tiles completely inside clipping region for i3dm', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetWithExternalResourcesUrl).then(function(tileset) { + var statistics = tileset._statistics; + var root = tileset._root; + + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(6); + + tileset.update(scene.frameState); + + var plane = new Plane(Cartesian3.UNIT_Z, 0.0); + tileset.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + plane + ] + }); + + tileset.update(scene.frameState); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(6); + expect(root._isClipped).toBe(false); + + plane.distance = 4081608.4377916814; // center + + tileset.update(scene.frameState); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(6); + expect(root._isClipped).toBe(true); + + plane.distance = 4081608.4377916814 + 142.19001637409772; // center + radius + + tileset.update(scene.frameState); + scene.renderForSpecs(); + + expect(statistics.numberOfCommands).toEqual(0); + expect(root._isClipped).toBe(true); + }); + }); }, 'WebGL'); diff --git a/Specs/Scene/ClippingPlanesCollectionSpec.js b/Specs/Scene/ClippingPlanesCollectionSpec.js new file mode 100644 index 000000000000..0645adaa6fd4 --- /dev/null +++ b/Specs/Scene/ClippingPlanesCollectionSpec.js @@ -0,0 +1,182 @@ +defineSuite([ + 'Scene/ClippingPlanesCollection', + 'Core/BoundingSphere', + 'Core/Cartesian3', + 'Core/Cartesian4', + 'Core/Color', + 'Core/Intersect', + 'Core/Matrix4', + 'Core/Plane' + ], function( + ClippingPlanesCollection, + BoundingSphere, + Cartesian3, + Cartesian4, + Color, + Intersect, + Matrix4, + Plane) { + 'use strict'; + + var clippingPlanes; + var planes = [ + new Plane(Cartesian3.UNIT_X, 1.0), + new Plane(Cartesian3.UNIT_Y, 2.0) + ]; + + var transform = new Matrix4.fromTranslation(new Cartesian3(1.0, 2.0, 3.0)); + var boundingVolume = new BoundingSphere(Cartesian3.ZERO, 1.0); + + it('default constructor', function() { + clippingPlanes = new ClippingPlanesCollection(); + expect(clippingPlanes.planes).toEqual([]); + expect(clippingPlanes.enabled).toEqual(true); + expect(clippingPlanes.modelMatrix).toEqual(Matrix4.IDENTITY); + expect(clippingPlanes.edgeColor).toEqual(Color.WHITE); + expect(clippingPlanes.edgeWidth).toEqual(0.0); + expect(clippingPlanes.combineClippingRegions).toEqual(true); + expect(clippingPlanes._testIntersection).not.toBeUndefined(); + }); + + it('transforms and packs planes into result paramter', function() { + clippingPlanes = new ClippingPlanesCollection({ + planes : planes + }); + + var result = clippingPlanes.transformAndPackPlanes(transform); + expect(result.length).toEqual(2); + expect(result[0]).toEqual(new Cartesian4(1.0, 0.0, 0.0, -2.0)); + expect(result[1]).toEqual(new Cartesian4(0.0, 1.0, 0.0, -4.0)); + }); + + it('transforms and packs planes with no result parameter creates new array', function() { + clippingPlanes = new ClippingPlanesCollection({ + planes : planes + }); + + var result = clippingPlanes.transformAndPackPlanes(transform); + expect(result.length).toEqual(2); + expect(result[0]).toBeInstanceOf(Cartesian4); + expect(result[1]).toBeInstanceOf(Cartesian4); + }); + + it('clone without a result parameter returns new identical copy', function() { + clippingPlanes = new ClippingPlanesCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + + var result = clippingPlanes.clone(); + expect(result).not.toBe(clippingPlanes); + expect(result.planes[0]).toEqual(planes[0]); + expect(result.planes[1]).toEqual(planes[1]); + expect(result.enabled).toEqual(false); + expect(result.modelMatrix).toEqual(transform); + expect(result.edgeColor).toEqual(Color.RED); + expect(result.edgeWidth).toEqual(0.0); + expect(result.combineClippingRegions).toEqual(true); + expect(result._testIntersection).not.toBeUndefined(); + }); + + it('clone stores copy in result parameter', function() { + clippingPlanes = new ClippingPlanesCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + var result = new ClippingPlanesCollection(); + var copy = clippingPlanes.clone(result); + expect(copy).toBe(result); + expect(result.planes).not.toBe(planes); + expect(result.planes[0]).toEqual(planes[0]); + expect(result.planes[1]).toEqual(planes[1]); + expect(result.enabled).toEqual(false); + expect(result.modelMatrix).toEqual(transform); + expect(result.edgeColor).toEqual(Color.RED); + expect(result.edgeWidth).toEqual(0.0); + expect(result.combineClippingRegions).toEqual(true); + expect(result._testIntersection).not.toBeUndefined(); + + // Only allocate a new array if needed + var previousPlanes = result.planes; + clippingPlanes.clone(result); + expect(result.planes).toBe(previousPlanes); + }); + + + it('setting combineClippingRegions updates testIntersection function', function() { + clippingPlanes = new ClippingPlanesCollection(); + var originalIntersectFunction = clippingPlanes._testIntersection; + + expect(clippingPlanes._testIntersection).not.toBeUndefined(); + + clippingPlanes.combineClippingRegions = false; + + expect(clippingPlanes._testIntersection).not.toBe(originalIntersectFunction); + }); + + it('computes intersections with bounding volumes when combining clipping regions', function() { + clippingPlanes = new ClippingPlanesCollection(); + + var intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.INSIDE); + + clippingPlanes.planes.push(new Plane(Cartesian3.UNIT_X, 2.0)); + intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.OUTSIDE); + + clippingPlanes.planes.push(new Plane(Cartesian3.UNIT_Y, 0.0)); + intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.INTERSECTING); + + clippingPlanes.planes.push(new Plane(Cartesian3.UNIT_Z, -1.0)); + intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.INSIDE); + + clippingPlanes.planes.push(new Plane(Cartesian3.UNIT_Z, 0.0)); + intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.INSIDE); + }); + + it('computes intersections with bounding volumes when not combining clipping regions', function() { + clippingPlanes = new ClippingPlanesCollection({ + combineClippingRegions : false + }); + + var intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.INSIDE); + + clippingPlanes.planes.push(new Plane(Cartesian3.UNIT_Z, -1.0)); + intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.INSIDE); + + clippingPlanes.planes.push(new Plane(Cartesian3.UNIT_Y, 2.0)); + intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.OUTSIDE); + + clippingPlanes.planes.push(new Plane(Cartesian3.UNIT_X, 0.0)); + intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.OUTSIDE); + + var plane = clippingPlanes.planes.pop(); + clippingPlanes.planes.pop(); + + clippingPlanes.planes.push(plane); + intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); + expect(intersect).toEqual(Intersect.INTERSECTING); + }); + + it('computes intersections applies optional transform to planes', function() { + clippingPlanes = new ClippingPlanesCollection(); + + var intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume, transform); + expect(intersect).toEqual(Intersect.INSIDE); + + clippingPlanes.planes.push(new Plane(Cartesian3.UNIT_X, -1.0)); + intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume, transform); + expect(intersect).not.toEqual(Intersect.INSIDE); + }); +}); diff --git a/Specs/Scene/GlobeSurfaceTileProviderSpec.js b/Specs/Scene/GlobeSurfaceTileProviderSpec.js index 40b865886b56..e21ddc426df1 100644 --- a/Specs/Scene/GlobeSurfaceTileProviderSpec.js +++ b/Specs/Scene/GlobeSurfaceTileProviderSpec.js @@ -8,11 +8,14 @@ defineSuite([ 'Core/Ellipsoid', 'Core/EllipsoidTerrainProvider', 'Core/GeographicProjection', + 'Core/Intersect', + 'Core/Plane', 'Core/Rectangle', 'Core/WebMercatorProjection', 'Renderer/ContextLimits', 'Renderer/RenderState', 'Scene/BlendingState', + 'Scene/ClippingPlanesCollection', 'Scene/Fog', 'Scene/Globe', 'Scene/GlobeSurfaceShaderSet', @@ -35,11 +38,14 @@ defineSuite([ Ellipsoid, EllipsoidTerrainProvider, GeographicProjection, + Intersect, + Plane, Rectangle, WebMercatorProjection, ContextLimits, RenderState, BlendingState, + ClippingPlanesCollection, Fog, Globe, GlobeSurfaceShaderSet, @@ -692,4 +698,163 @@ defineSuite([ }).toThrowDeveloperError(); }); + it('clipping planes selectively disable rendering globe surface', function() { + expect(scene).toRender([0, 0, 0, 255]); + + switchViewMode(SceneMode.SCENE3D, new GeographicProjection(Ellipsoid.WGS84)); + + return updateUntilDone(scene.globe).then(function() { + expect(scene).notToRender([0, 0, 0, 255]); + + var result; + expect(scene).toRenderAndCall(function(rgba) { + result = rgba; + expect(rgba).not.toEqual([0, 0, 0, 255]); + }); + + var clipPlane = new Plane(Cartesian3.UNIT_Z, 10000.0); + scene.globe.clippingPlanes = new ClippingPlanesCollection ({ + planes : [ + clipPlane + ] + }); + + expect(scene).notToRender(result); + + clipPlane.distance = 0.0; + + expect(scene).toRender(result); + + scene.globe.clippingPlanes = undefined; + }); + }); + + it('renders with clipping planes edge styling on globe surface', function() { + expect(scene).toRender([0, 0, 0, 255]); + + switchViewMode(SceneMode.SCENE3D, new GeographicProjection(Ellipsoid.WGS84)); + + return updateUntilDone(scene.globe).then(function() { + expect(scene).notToRender([0, 0, 0, 255]); + + var result; + expect(scene).toRenderAndCall(function(rgba) { + result = rgba; + expect(rgba).not.toEqual([0, 0, 0, 255]); + }); + + var clipPlane = new Plane(Cartesian3.UNIT_Z, 1000.0); + scene.globe.clippingPlanes = new ClippingPlanesCollection ({ + planes : [ + clipPlane + ], + edgeWidth : 20.0, + edgeColor : Color.RED + }); + + expect(scene).notToRender(result); + + clipPlane.distance = 0.0; + + expect(scene).toRender([255, 0, 0, 255]); + + scene.globe.clippingPlanes = undefined; + }); + }); + + it('renders with multiple clipping planes and combined regions', function() { + expect(scene).toRender([0, 0, 0, 255]); + + switchViewMode(SceneMode.SCENE3D, new GeographicProjection(Ellipsoid.WGS84)); + + return updateUntilDone(scene.globe).then(function() { + expect(scene).notToRender([0, 0, 0, 255]); + + var result; + expect(scene).toRenderAndCall(function(rgba) { + result = rgba; + expect(rgba).not.toEqual([0, 0, 0, 255]); + }); + + scene.globe.clippingPlanes = new ClippingPlanesCollection ({ + planes : [ + new Plane(Cartesian3.UNIT_Z, 10000.0), + new Plane(Cartesian3.UNIT_X, 1000.0) + ], + combineClippingRegions: false + }); + + expect(scene).notToRender(result); + + scene.globe.clippingPlanes.combineClippingRegions = true; + + expect(scene).toRender(result); + + scene.globe.clippingPlanes = undefined; + }); + }); + + it('No extra tiles culled with no clipping planes', function() { + var globe = scene.globe; + switchViewMode(SceneMode.SCENE3D, new GeographicProjection(Ellipsoid.WGS84)); + + return updateUntilDone(globe).then(function() { + expect(scene.frameState.commandList.length).toBe(4); + }); + }); + + it('Culls tiles when completely inside clipping region', function() { + var globe = scene.globe; + globe.clippingPlanes = new ClippingPlanesCollection ({ + planes : [ + new Plane(Cartesian3.UNIT_Z, 1000000.0) + ] + }); + + switchViewMode(SceneMode.SCENE3D, new GeographicProjection(Ellipsoid.WGS84)); + + return updateUntilDone(globe).then(function() { + var surface = globe._surface; + var tile = surface._levelZeroTiles[0]; + expect(tile.isClipped).toBe(true); + expect(scene.frameState.commandList.length).toBe(2); + }); + }); + + it('Doesn\'t cull, but clips tiles when intersecting clipping plane', function() { + var globe = scene.globe; + globe.clippingPlanes = new ClippingPlanesCollection ({ + planes : [ + new Plane(Cartesian3.UNIT_Z, 0.0) + ] + }); + + switchViewMode(SceneMode.SCENE3D, new GeographicProjection(Ellipsoid.WGS84)); + + return updateUntilDone(globe).then(function() { + var surface = globe._surface; + var tile = surface._levelZeroTiles[0]; + expect(tile.isClipped).toBe(true); + expect(scene.frameState.commandList.length).toBe(4); + }); + }); + + it('Doesn\'t cull or clip tiles when completely outside clipping region', function() { + var globe = scene.globe; + globe.clippingPlanes = new ClippingPlanesCollection ({ + planes : [ + new Plane(Cartesian3.UNIT_Z, -10000000.0) + ] + }); + + switchViewMode(SceneMode.SCENE3D, new GeographicProjection(Ellipsoid.WGS84)); + + return updateUntilDone(globe).then(function() { + var surface = globe._surface; + var tile = surface._levelZeroTiles[0]; + expect(tile.isClipped).toBe(false); + expect(scene.frameState.commandList.length).toBe(4); + }); + }); + }, 'WebGL'); diff --git a/Specs/Scene/Instanced3DModel3DTileContentSpec.js b/Specs/Scene/Instanced3DModel3DTileContentSpec.js index ab03463ac95e..4c00160251a6 100644 --- a/Specs/Scene/Instanced3DModel3DTileContentSpec.js +++ b/Specs/Scene/Instanced3DModel3DTileContentSpec.js @@ -4,8 +4,10 @@ defineSuite([ 'Core/Color', 'Core/HeadingPitchRange', 'Core/HeadingPitchRoll', + 'Core/Plane', 'Core/Transforms', 'Scene/Cesium3DTileContentState', + 'Scene/ClippingPlanesCollection', 'Scene/TileBoundingSphere', 'Specs/Cesium3DTilesTester', 'Specs/createScene' @@ -15,8 +17,10 @@ defineSuite([ Color, HeadingPitchRange, HeadingPitchRoll, + Plane, Transforms, Cesium3DTileContentState, + ClippingPlanesCollection, TileBoundingSphere, Cesium3DTilesTester, createScene) { @@ -305,6 +309,32 @@ defineSuite([ }); }); + it('Updates model\'s clipping planes', function() { + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + var tile = tileset._root; + var content = tile.content; + var model = content._modelInstanceCollection._model; + + expect(model.clippingPlanes).toBeUndefined(); + + tileset.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + new Plane(Cartesian3.UNIT_X, 0.0) + ] + }); + content.update(tileset, scene.frameState); + + expect(model.clippingPlanes).toBeDefined(); + expect(model.clippingPlanes.planes.length).toBe(1); + expect(model.clippingPlanes.enabled).toBe(true); + + tile._isClipped = false; + content.update(tileset, scene.frameState); + + expect(model.clippingPlanes.enabled).toBe(false); + }); + }); + it('destroys', function() { return Cesium3DTilesTester.tileDestroys(scene, withoutBatchTableUrl); }); diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js index 7ae7ba9e3385..c52fdb0a614b 100644 --- a/Specs/Scene/ModelSpec.js +++ b/Specs/Scene/ModelSpec.js @@ -22,6 +22,7 @@ defineSuite([ 'Core/Matrix3', 'Core/Matrix4', 'Core/PerspectiveFrustum', + 'Core/Plane', 'Core/PrimitiveType', 'Core/Transforms', 'Core/WebGLConstants', @@ -29,6 +30,7 @@ defineSuite([ 'Renderer/RenderState', 'Renderer/ShaderSource', 'Scene/Axis', + 'Scene/ClippingPlanesCollection', 'Scene/ColorBlendMode', 'Scene/HeightReference', 'Scene/ModelAnimationLoop', @@ -59,6 +61,7 @@ defineSuite([ Matrix3, Matrix4, PerspectiveFrustum, + Plane, PrimitiveType, Transforms, WebGLConstants, @@ -66,6 +69,7 @@ defineSuite([ RenderState, ShaderSource, Axis, + ClippingPlanesCollection, ColorBlendMode, HeightReference, ModelAnimationLoop, @@ -2397,6 +2401,8 @@ defineSuite([ scene.renderForSpecs(); var commands = scene.frameState.commandList; expect(commands.length).toBe(0); + + primitives.remove(model); }); }); @@ -2503,9 +2509,9 @@ defineSuite([ model.silhouetteColor = Color.GREEN; // Load a second model - return loadModel(boxUrl).then(function(model) { - model.show = true; - model.silhouetteSize = 1.0; + return loadModel(boxUrl).then(function(model2) { + model2.show = true; + model2.silhouetteSize = 1.0; scene.renderForSpecs(); expect(commands.length).toBe(4); expect(commands[0].renderState.stencilTest.enabled).toBe(true); @@ -2520,7 +2526,139 @@ defineSuite([ var reference1 = commands[0].renderState.stencilTest.reference; var reference2 = commands[2].renderState.stencilTest.reference; expect(reference2).toEqual(reference1 + 1); + + primitives.remove(model); + primitives.remove(model2); + }); + }); + }); + + it('Updates clipping planes when clipping planes are enabled', function () { + return loadModel(boxUrl).then(function(model) { + model.show = true; + model.zoomTo(); + + scene.renderForSpecs(); + + expect(model._packedClippingPlanes).toBeDefined(); + expect(model._packedClippingPlanes.length).toBe(0); + expect(model._modelViewMatrix).toEqual(Matrix4.IDENTITY); + + model.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + new Plane(Cartesian3.UNIT_X, 0.0) + ] }); + + model.update(scene.frameState); + scene.renderForSpecs(); + + expect(model._packedClippingPlanes.length).toBe(1); + expect(model._modelViewMatrix).not.toEqual(Matrix4.IDENTITY); + + primitives.remove(model); + }); + }); + + it('Clipping planes selectively disable rendering', function () { + return loadModel(boxUrl).then(function(model) { + model.show = true; + model.zoomTo(); + + var modelColor; + model.update(scene.frameState); + expect(scene).toRenderAndCall(function(rgba) { + modelColor = rgba; + }); + + var plane = new Plane(Cartesian3.UNIT_X, 0.0); + model.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + plane + ] + }); + + model.update(scene.frameState); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba).not.toEqual(modelColor); + }); + + plane.distance = -10.0; + model.update(scene.frameState); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba).toEqual(modelColor); + }); + + primitives.remove(model); + }); + }); + + it('Clipping planes apply edge styling', function () { + return loadModel(boxUrl).then(function(model) { + model.show = true; + model.zoomTo(); + + var modelColor; + model.update(scene.frameState); + expect(scene).toRenderAndCall(function(rgba) { + modelColor = rgba; + }); + + var plane = new Plane(Cartesian3.UNIT_X, 0.0); + model.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + plane + ], + edgeWidth : 5.0, + edgeColor : Color.BLUE + }); + + model.update(scene.frameState); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba).not.toEqual(modelColor); + }); + + plane.distance = -5.0; + model.update(scene.frameState); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba).toEqual([0, 0, 255, 255]); + }); + + primitives.remove(model); + }); + }); + + it('Clipping planes combien regions', function () { + return loadModel(boxUrl).then(function(model) { + model.show = true; + model.zoomTo(); + + var modelColor; + model.update(scene.frameState); + expect(scene).toRenderAndCall(function(rgba) { + modelColor = rgba; + }); + + model.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + new Plane(Cartesian3.UNIT_Z, -5.0), + new Plane(Cartesian3.UNIT_X, 0.0) + ], + combineClippingRegions: false + }); + + model.update(scene.frameState); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba).not.toEqual(modelColor); + }); + + model.clippingPlanes._combineClippingRegions = true; + model.update(scene.frameState); + expect(scene).toRenderAndCall(function(rgba) { + expect(rgba).toEqual(modelColor); + }); + + primitives.remove(model); }); }); diff --git a/Specs/Scene/PointCloud3DTileContentSpec.js b/Specs/Scene/PointCloud3DTileContentSpec.js index f70b614c718c..b45c5c706682 100644 --- a/Specs/Scene/PointCloud3DTileContentSpec.js +++ b/Specs/Scene/PointCloud3DTileContentSpec.js @@ -7,9 +7,12 @@ defineSuite([ 'Core/HeadingPitchRange', 'Core/HeadingPitchRoll', 'Core/Math', + 'Core/Matrix4', 'Core/PerspectiveFrustum', + 'Core/Plane', 'Core/Transforms', 'Scene/Cesium3DTileStyle', + 'Scene/ClippingPlanesCollection', 'Scene/Expression', 'Specs/Cesium3DTilesTester', 'Specs/createScene', @@ -23,9 +26,12 @@ defineSuite([ HeadingPitchRange, HeadingPitchRoll, CesiumMath, + Matrix4, PerspectiveFrustum, + Plane, Transforms, Cesium3DTileStyle, + ClippingPlanesCollection, Expression, Cesium3DTilesTester, createScene, @@ -685,6 +691,118 @@ defineSuite([ }); }); + it('Updates clipping planes when clipping planes are enabled', function () { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var content = tileset._root.content; + + expect(content._packedClippingPlanes).toBeDefined(); + expect(content._packedClippingPlanes.length).toBe(0); + expect(content._modelViewMatrix).toEqual(Matrix4.IDENTITY); + + tileset.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + new Plane(Cartesian3.UNIT_X, 0.0) + ] + }); + + content.update(tileset, scene.frameState); + + expect(content._packedClippingPlanes.length).toBe(1); + expect(content._modelViewMatrix).not.toEqual(Matrix4.IDENTITY); + }); + }); + + it('Does not update clipping planes when tile is not clipped', function () { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var tile = tileset._root; + tile._isClipped = false; + var content = tile.content; + + expect(content._packedClippingPlanes).toBeDefined(); + expect(content._packedClippingPlanes.length).toBe(0); + expect(content._modelViewMatrix).toEqual(Matrix4.IDENTITY); + + tileset.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + new Plane(Cartesian3.UNIT_X, 0.0) + ] + }); + + content.update(tileset, scene.frameState); + + expect(content._packedClippingPlanes.length).toBe(0); + expect(content._modelViewMatrix).toEqual(Matrix4.IDENTITY); + }); + }); + + it('Clipping planes selectively disable rendering', function () { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var color; + expect(scene).toRenderAndCall(function(rgba) { + color = rgba; + }); + + var clipPlane = new Plane(Cartesian3.UNIT_Z, 10.0); + tileset.clippingPlanes = new ClippingPlanesCollection({ + planes : [ + clipPlane + ], + modelMatrix : Transforms.eastNorthUpToFixedFrame(tileset.boundingSphere.center) + }); + + expect(scene).notToRender(color); + + clipPlane.distance = 0.0; + + expect(scene).toRender(color); + }); + }); + + it('clipping planes apply edge styling', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var color; + expect(scene).toRenderAndCall(function(rgba) { + color = rgba; + }); + + var clipPlane = new Plane(Cartesian3.UNIT_Z, 10.0); + tileset.clippingPlanes = new ClippingPlanesCollection ({ + planes : [ + clipPlane + ], + modelMatrix : Transforms.eastNorthUpToFixedFrame(tileset.boundingSphere.center), + edgeWidth : 20.0, + edgeColor : Color.RED + }); + + expect(scene).notToRender(color); + }); + }); + + it('clipping planes combine regions', function() { + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var color; + expect(scene).toRenderAndCall(function(rgba) { + color = rgba; + }); + + tileset.clippingPlanes = new ClippingPlanesCollection ({ + planes : [ + new Plane(Cartesian3.UNIT_Z, 10.0), + new Plane(Cartesian3.UNIT_X, 0.0) + ], + modelMatrix : Transforms.eastNorthUpToFixedFrame(tileset.boundingSphere.center), + combineClippingRegions: false + }); + + expect(scene).notToRender(color); + + tileset.clippingPlanes.combineClippingRegions = true; + + expect(scene).toRender(color); + }); + }); + it('destroys', function() { return Cesium3DTilesTester.tileDestroys(scene, pointCloudRGBUrl); });