diff --git a/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.html b/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.html index ca5698c3faa5..df2f4e429d3e 100644 --- a/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.html +++ b/Apps/Sandcastle/gallery/3D Tiles Clipping Planes.html @@ -48,7 +48,6 @@ // Add a clipping plane, a plane geometry to show the representation of the // plane, and control the magnitude of the plane distance with the mouse. -// Clipping planes are not currently supported in Internet Explorer. var viewer = new Cesium.Viewer('cesiumContainer', { infoBox: false, @@ -103,7 +102,7 @@ } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); -var scratchPlane = new Cesium.Plane(Cesium.Cartesian3.UNIT_X, 0.0); +var scratchPlane = new Cesium.ClippingPlane(Cesium.Cartesian3.UNIT_X, 0.0); function createPlaneUpdateFunction(plane, transform) { return function () { plane.distance = targetY; @@ -114,7 +113,7 @@ var tileset; function loadTileset(url) { var clippingPlanes = [ - new Cesium.Plane(new Cesium.Cartesian3(0.0, 0.0, -1.0), -100.0) + new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 0.0, -1.0), -100.0) ]; tileset = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({ @@ -156,7 +155,7 @@ var modelEntityClippingPlanes; function loadModel(url) { var clippingPlanes = [ - new Cesium.Plane(new Cesium.Cartesian3(0.0, 0.0, -1.0), -100.0) + new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 0.0, -1.0), -100.0) ]; modelEntityClippingPlanes = new Cesium.ClippingPlaneCollection({ diff --git a/Apps/Sandcastle/gallery/Terrain Clipping Planes.html b/Apps/Sandcastle/gallery/Terrain Clipping Planes.html index faf4a6762e82..ecfa7394c8b0 100644 --- a/Apps/Sandcastle/gallery/Terrain Clipping Planes.html +++ b/Apps/Sandcastle/gallery/Terrain Clipping Planes.html @@ -40,7 +40,6 @@ 'use strict'; //Sandcastle_Begin // Use clipping planes to selectively hide parts of the globe surface. -// Clipping planes are not currently supported in Internet Explorer. var viewer = new Cesium.Viewer('cesiumContainer', { skyAtmosphere: false, @@ -74,10 +73,10 @@ globe.clippingPlanes = new Cesium.ClippingPlaneCollection({ modelMatrix : entity.computeModelMatrix(Cesium.JulianDate.now()), planes : [ - new Cesium.Plane(new Cesium.Cartesian3( 1.0, 0.0, 0.0), -700.0), - new Cesium.Plane(new Cesium.Cartesian3(-1.0, 0.0, 0.0), -700.0), - new Cesium.Plane(new Cesium.Cartesian3( 0.0, 1.0, 0.0), -700.0), - new Cesium.Plane(new Cesium.Cartesian3( 0.0, -1.0, 0.0), -700.0) + new Cesium.ClippingPlane(new Cesium.Cartesian3( 1.0, 0.0, 0.0), -700.0), + new Cesium.ClippingPlane(new Cesium.Cartesian3(-1.0, 0.0, 0.0), -700.0), + new Cesium.ClippingPlane(new Cesium.Cartesian3( 0.0, 1.0, 0.0), -700.0), + new Cesium.ClippingPlane(new Cesium.Cartesian3( 0.0, -1.0, 0.0), -700.0) ], edgeWidth: 1.0, edgeColor: Cesium.Color.WHITE diff --git a/Apps/Sandcastle/gallery/development/Many Clipping Planes.html b/Apps/Sandcastle/gallery/development/Many Clipping Planes.html new file mode 100644 index 000000000000..09bb82d8b791 --- /dev/null +++ b/Apps/Sandcastle/gallery/development/Many Clipping Planes.html @@ -0,0 +1,286 @@ + + + + + + + + + Cesium Demo + + + + + + +
+

Loading...

+
+ + + + + + + +
Distance + + + Plane Count + + +
+ +
+ + + diff --git a/Apps/Sandcastle/gallery/development/Many Clipping Planes.jpg b/Apps/Sandcastle/gallery/development/Many Clipping Planes.jpg new file mode 100644 index 000000000000..7a2f4bb481bd Binary files /dev/null and b/Apps/Sandcastle/gallery/development/Many Clipping Planes.jpg differ diff --git a/CHANGES.md b/CHANGES.md index d5df82b472d9..538530a211a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,15 +3,23 @@ Change Log ### 1.44 - 2018-04-02 +##### Deprecated :hourglass_flowing_sand: +* `ClippingPlaneCollection` is now supported in Internet Explorer, so `ClippingPlaneCollection.isSupported` has been deprecated and will be removed in Cesium 1.45. +* `ClippingPlaneCollection` should now be used with `ClippingPlane` objects instead of `Plane`. Use of `Plane` objects has been deprecated and will be removed in Cesium 1.45. + ##### Additions :tada: * Added support for glTF models with [Draco geometry compression](https://github.com/fanzhanggoogle/glTF/blob/KHR_mesh_compression/extensions/Khronos/KHR_draco_mesh_compression/README.md). +* `ClippingPlaneCollection` updates [#6201](https://github.com/AnalyticalGraphicsInc/cesium/pull/6201) + * Removed the 6-clipping-plane limit. + * Added support for Internet Explorer. + * Added a `ClippingPlane` object to be used with `ClippingPlaneCollection`. ##### Fixes :wrench: * Fixed support of glTF-supplied tangent vectors. [#6302](https://github.com/AnalyticalGraphicsInc/cesium/pull/6302) * Fixed improper zoom during model load failure. [#6305](https://github.com/AnalyticalGraphicsInc/cesium/pull/6305) #### Additions :tada: -* Updated `WebMapServiceImageryProvider` so it can take an srs or crs string to pass to the resource query parameters based on the WMS version. [#6223](https://github.com/AnalyticalGraphicsInc/cesium/issues/6223) +* Updated `WebMapServiceImageryProvider` so it can take an srs or crs string to pass to the resource query parameters based on the WMS version. [#6223](https://github.com/AnalyticalGraphicsInc/cesium/issues/6223) ### 1.43 - 2018-03-01 diff --git a/Source/Core/Cartesian4.js b/Source/Core/Cartesian4.js index a48d1f810dac..479ce4d1f517 100644 --- a/Source/Core/Cartesian4.js +++ b/Source/Core/Cartesian4.js @@ -817,5 +817,93 @@ define([ return '(' + this.x + ', ' + this.y + ', ' + this.z + ', ' + this.w + ')'; }; + var scratchFloatArray = new Float32Array(1); + var SHIFT_LEFT_8 = 256.0; + var SHIFT_LEFT_16 = 65536.0; + var SHIFT_LEFT_24 = 16777216.0; + + var SHIFT_RIGHT_8 = 1.0 / SHIFT_LEFT_8; + var SHIFT_RIGHT_16 = 1.0 / SHIFT_LEFT_16; + var SHIFT_RIGHT_24 = 1.0 / SHIFT_LEFT_24; + + var BIAS = 38.0; + + /** + * Packs an arbitrary floating point value to 4 values representable using uint8. + * + * @param {Number} value A floating point number + * @param {Cartesian4} [result] The Cartesian4 that will contain the packed float. + * @returns {Cartesian4} A Cartesian4 representing the float packed to values in x, y, z, and w. + */ + Cartesian4.packFloat = function(value, result) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('value', value); + //>>includeEnd('debug'); + + if (!defined(result)) { + result = new Cartesian4(); + } + + // Force the value to 32 bit precision + scratchFloatArray[0] = value; + value = scratchFloatArray[0]; + + if (value === 0.0) { + return Cartesian4.clone(Cartesian4.ZERO, result); + } + + var sign = value < 0.0 ? 1.0 : 0.0; + var exponent; + + if (!isFinite(value)) { + value = 0.1; + exponent = BIAS; + } else { + value = Math.abs(value); + exponent = Math.floor(CesiumMath.logBase(value, 10)) + 1.0; + value = value / Math.pow(10.0, exponent); + } + + var temp = value * SHIFT_LEFT_8; + result.x = Math.floor(temp); + temp = (temp - result.x) * SHIFT_LEFT_8; + result.y = Math.floor(temp); + temp = (temp - result.y) * SHIFT_LEFT_8; + result.z = Math.floor(temp); + result.w = (exponent + BIAS) * 2.0 + sign; + + return result; + }; + + /** + * Unpacks a float packed using Cartesian4.packFloat. + * + * @param {Cartesian4} packedFloat A Cartesian4 containing a float packed to 4 values representable using uint8. + * @returns {Number} The unpacked float. + */ + Cartesian4.unpackFloat = function(packedFloat) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('packedFloat', packedFloat); + //>>includeEnd('debug'); + + var temp = packedFloat.w / 2.0; + var exponent = Math.floor(temp); + var sign = (temp - exponent) * 2.0; + exponent = exponent - BIAS; + + sign = sign * 2.0 - 1.0; + sign = -sign; + + if (exponent >= BIAS) { + return sign < 0.0 ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY; + } + + var unpacked = sign * packedFloat.x * SHIFT_RIGHT_8; + unpacked += sign * packedFloat.y * SHIFT_RIGHT_16; + unpacked += sign * packedFloat.z * SHIFT_RIGHT_24; + + return unpacked * Math.pow(10.0, exponent); + }; + return Cartesian4; }); diff --git a/Source/Core/ClippingPlane.js b/Source/Core/ClippingPlane.js new file mode 100644 index 000000000000..08b652212cc4 --- /dev/null +++ b/Source/Core/ClippingPlane.js @@ -0,0 +1,155 @@ +define([ + './Cartesian3', + './Check', + './defined', + './defineProperties' + ], function( + Cartesian3, + Check, + defined, + defineProperties) { + 'use strict'; + + /** + * A Plane in Hessian Normal form to be used with ClippingPlaneCollection. + * Compatible with mathematics functions in Plane.js + * + * @alias ClippingPlane + * @constructor + * + * @param {Cartesian3} normal The plane's normal (normalized). + * @param {Number} distance The shortest distance from the origin to the plane. The sign of + * distance determines which side of the plane the origin + * is on. If distance is positive, the origin is in the half-space + * in the direction of the normal; if negative, the origin is in the half-space + * opposite to the normal; if zero, the plane passes through the origin. + */ + function ClippingPlane(normal, distance) { + this._distance = distance; + this._normal = new UpdateChangedCartesian3(normal, this); + this.onChangeCallback = undefined; + this.index = -1; // to be set by ClippingPlaneCollection + } + + defineProperties(ClippingPlane.prototype, { + /** + * The shortest distance from the origin to the plane. The sign of + * distance determines which side of the plane the origin + * is on. If distance is positive, the origin is in the half-space + * in the direction of the normal; if negative, the origin is in the half-space + * opposite to the normal; if zero, the plane passes through the origin. + * + * @type {Number} + * @memberof ClippingPlane.prototype + */ + distance : { + get : function() { + return this._distance; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('value', value); + //>>includeEnd('debug'); + if (defined(this.onChangeCallback) && value !== this._distance) { + this.onChangeCallback(this.index); + } + this._distance = value; + } + }, + /** + * The plane's normal. + * + * @type {Cartesian3} + * @memberof ClippingPlane.prototype + */ + normal : { + get : function() { + return this._normal; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('value', value); + //>>includeEnd('debug'); + if (defined(this.onChangeCallback) && !Cartesian3.equals(this._normal._cartesian3, value)) { + this.onChangeCallback(this.index); + } + // Set without firing callback again + Cartesian3.clone(value, this._normal._cartesian3); + } + } + }); + + /** + * Create a ClippingPlane from a Plane object. + * + * @param {Plane} plane The plane containing parameters to copy + * @param {ClippingPlane} [result] The object on which to store the result + * @returns {ClippingPlane} The ClippingPlane generated from the plane's parameters. + */ + ClippingPlane.fromPlane = function(plane, result) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('plane', plane); + //>>includeEnd('debug'); + + if (!defined(result)) { + result = new ClippingPlane(plane.normal, plane.distance); + } else { + result.normal = plane.normal; + result.distance = plane.distance; + } + return result; + }; + + /** + * Clones the ClippingPlane without setting its ownership. + * @param {ClippingPlane} clippingPlane The ClippingPlane to be cloned + * @param {ClippingPlane} [result] The object on which to store the cloned parameters. + * @returns {ClippingPlane} a clone of the input ClippingPlane + */ + ClippingPlane.clone = function(clippingPlane, result) { + if (!defined(result)) { + return new ClippingPlane(clippingPlane.normal, clippingPlane.distance); + } + result.normal = clippingPlane.normal; + result.distance = clippingPlane.distance; + return result; + }; + + /** + * Wrapper on Cartesian3 that allows detection of Plane changes from "members of members," for example: + * + * var clippingPlane = new ClippingPlane(...); + * clippingPlane.normal.z = -1.0; + * + * @private + */ + function UpdateChangedCartesian3(normal, clippingPlane) { + this._clippingPlane = clippingPlane; + this._cartesian3 = Cartesian3.clone(normal); + } + + function makeGetterStter(key) { + return { + get : function() { + return this._cartesian3[key]; + }, + set : function(value) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.number('value', value); + //>>includeEnd('debug'); + if (defined(this._clippingPlane.onChangeCallback) && value !== this._cartesian3[key]) { + this._clippingPlane.onChangeCallback(this._clippingPlane.index); + } + this._cartesian3[key] = value; + } + }; + } + + defineProperties(UpdateChangedCartesian3.prototype, { + x : makeGetterStter('x'), + y : makeGetterStter('y'), + z : makeGetterStter('z') + }); + + return ClippingPlane; +}); diff --git a/Source/Core/ClippingPlaneCollection.js b/Source/Core/ClippingPlaneCollection.js index 56369d69202f..c694b0bbe61d 100644 --- a/Source/Core/ClippingPlaneCollection.js +++ b/Source/Core/ClippingPlaneCollection.js @@ -1,40 +1,66 @@ define([ + './AttributeCompression', + './Cartesian2', './Cartesian3', './Cartesian4', + './Math', './Check', + './ClippingPlane', './Color', './defaultValue', './defined', './defineProperties', + './destroyObject', './DeveloperError', './FeatureDetection', './Intersect', './Matrix4', - './Plane' + './PixelFormat', + './Plane', + '../Renderer/ContextLimits', + '../Renderer/PixelDatatype', + '../Renderer/Sampler', + '../Renderer/Texture', + '../Renderer/TextureMagnificationFilter', + '../Renderer/TextureMinificationFilter', + '../Renderer/TextureWrap' ], function( + AttributeCompression, + Cartesian2, Cartesian3, Cartesian4, + CesiumMath, Check, + ClippingPlane, Color, defaultValue, defined, defineProperties, + destroyObject, DeveloperError, FeatureDetection, Intersect, Matrix4, - Plane) { + PixelFormat, + Plane, + ContextLimits, + PixelDatatype, + Sampler, + Texture, + TextureMagnificationFilter, + TextureMinificationFilter, + TextureWrap) { '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. + * outside of the specified list of {@link ClippingPlane} objects for a single gltf model, 3D Tileset, or the globe. * * @alias ClippingPlaneCollection * @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 {ClippingPlane[]} [options.planes=[]] An array of {@link ClippingPlane} 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.unionClippingRegions=false] If true, a region will be clipped if included in any plane in the collection. Otherwise, the region to be clipped must intersect the regions defined by all planes in this collection. @@ -44,20 +70,26 @@ define([ function ClippingPlaneCollection(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); + this._planes = []; + this._containsUntrackablePlanes = false; + + // Do partial texture updates if just one plane is dirty. + // If many planes are dirty, refresh the entire texture. + this._dirtyIndex = -1; + this._manyDirtyPlanes = false; + + // Add each plane to check if it's actually a Plane object instead of a ClippingPlane. + // Use of Plane objects will be deprecated. var planes = options.planes; if (defined(planes)) { - this._planes = planes.slice(0); - } else { - this._planes = []; + var planesLength = planes.length; + for (var i = 0; i < planesLength; ++i) { + this.add(planes[i]); + } } - /** - * Determines whether the clipping planes are active. - * - * @type {Boolean} - * @default true - */ - this.enabled = defaultValue(options.enabled, true); + this._enabled = false; + this.enabled = defaultValue(options.enabled, true); // set using setter /** * The 4x4 transformation matrix specifying an additional transform relative to the clipping planes @@ -84,9 +116,18 @@ define([ */ this.edgeWidth = defaultValue(options.edgeWidth, 0.0); + // If this ClippingPlaneCollection has an owner, only its owner should update or destroy it. + // This is because in a Cesium3DTileset multiple models may reference the tileset's ClippingPlaneCollection. + this._owner = undefined; + this._testIntersection = undefined; this._unionClippingRegions = undefined; - this.unionClippingRegions = defaultValue(options.unionClippingRegions, false); + this.unionClippingRegions = defaultValue(options.unionClippingRegions, false); // set using setter + + this._uint8View = undefined; + this._float32View = undefined; + + this._clippingPlanesTexture = undefined; } function unionIntersectFunction(value) { @@ -126,34 +167,91 @@ define([ return this._unionClippingRegions; }, set : function(value) { - if (this._unionClippingRegions !== value) { - this._unionClippingRegions = value; - this._testIntersection = value ? unionIntersectFunction : defaultIntersectFunction; + if (this._unionClippingRegions === value) { + return; } + this._unionClippingRegions = value; + this._testIntersection = value ? unionIntersectFunction : defaultIntersectFunction; + } + }, + + /** + * If true, clipping will be enabled. + * + * @memberof ClippingPlaneCollection.prototype + * @type {Boolean} + * @default false + */ + enabled : { + get : function() { + return this._enabled; + }, + set : function(value) { + if (this._enabled === value) { + return; + } + this._enabled = value; + } + }, + + /** + * Returns a texture containing packed, untransformed clipping planes. + * + * @memberof ClippingPlaneCollection.prototype + * @type {Texture} + * @readonly + * @private + */ + texture : { + get : function() { + return this._clippingPlanesTexture; + } + }, + + /** + * A reference to the ClippingPlaneCollection's owner, if any. + * + * @memberof ClippingPlaneCollection.prototype + * @readonly + * @private + */ + owner : { + get : function() { + return this._owner; } } }); + function setIndexDirty(collection, index) { + // If there's already a different _dirtyIndex set, more than one plane has changed since update. + // Entire texture must be reloaded + collection._manyDirtyPlanes = collection._manyDirtyPlanes || (collection._dirtyIndex !== -1 && collection._dirtyIndex !== index); + collection._dirtyIndex = index; + } + /** - * Adds the specified {@link Plane} to the collection to be used to selectively disable rendering + * Adds the specified {@link ClippingPlane} to the collection to be used to selectively disable rendering * on the outside of each plane. Use {@link ClippingPlaneCollection#unionClippingRegions} to modify * how modify the clipping behavior of multiple planes. * - * @param {Plane} plane The plane to add to the collection. - * - * @exception {DeveloperError} The plane added exceeds the maximum number of supported clipping planes. + * @param {ClippingPlane} plane The ClippingPlane to add to the collection. * * @see ClippingPlaneCollection#unionClippingRegions * @see ClippingPlaneCollection#remove * @see ClippingPlaneCollection#removeAll */ ClippingPlaneCollection.prototype.add = function(plane) { - //>>includeStart('debug', pragmas.debug); - if (this.length >= ClippingPlaneCollection.MAX_CLIPPING_PLANES) { - throw new DeveloperError('The maximum number of clipping planes supported is ' + ClippingPlaneCollection.MAX_CLIPPING_PLANES); + var newPlaneIndex = this._planes.length; + if (plane instanceof ClippingPlane) { + var that = this; + plane.onChangeCallback = function(index) { + setIndexDirty(that, index); + }; + plane.index = newPlaneIndex; + } else { + this._containsUntrackablePlanes = true; } - //>>includeEnd('debug'); - + setIndexDirty(this, newPlaneIndex); this._planes.push(plane); }; @@ -165,7 +263,7 @@ define([ * in the collection. * * @param {Number} index The zero-based index of the plane. - * @returns {Plane} The plane at the specified index. + * @returns {ClippingPlane} The ClippingPlane at the specified index. * * @see ClippingPlaneCollection#length */ @@ -189,39 +287,53 @@ define([ } /** - * Checks whether this collection contains the given plane. + * Checks whether this collection contains a ClippingPlane equal to the given ClippingPlane. * - * @param {Plane} [plane] The plane to check for. - * @returns {Boolean} true if this collection contains the plane, false otherwise. + * @param {ClippingPlane} [clippingPlane] The ClippingPlane to check for. + * @returns {Boolean} true if this collection contains the ClippingPlane, false otherwise. * * @see ClippingPlaneCollection#get */ - ClippingPlaneCollection.prototype.contains = function(plane) { - return indexOf(this._planes, plane) !== -1; + ClippingPlaneCollection.prototype.contains = function(clippingPlane) { + return indexOf(this._planes, clippingPlane) !== -1; }; /** - * Removes the first occurrence of the given plane from the collection. + * Removes the first occurrence of the given ClippingPlane from the collection. * - * @param {Plane} plane + * @param {ClippingPlane} clippingPlane * @returns {Boolean} true if the plane was removed; false if the plane was not found in the collection. * * @see ClippingPlaneCollection#add * @see ClippingPlaneCollection#contains * @see ClippingPlaneCollection#removeAll */ - ClippingPlaneCollection.prototype.remove = function(plane) { + ClippingPlaneCollection.prototype.remove = function(clippingPlane) { var planes = this._planes; - var index = indexOf(planes, plane); + var index = indexOf(planes, clippingPlane); if (index === -1) { return false; } + // Unlink this ClippingPlaneCollection from the ClippingPlane + if (clippingPlane instanceof ClippingPlane) { + clippingPlane.onChangeCallback = undefined; + clippingPlane.index = -1; + } + + // Shift and update indices var length = planes.length - 1; for (var i = index; i < length; ++i) { - planes[i] = planes[i + 1]; + var planeToKeep = planes[i + 1]; + planes[i] = planeToKeep; + if (planeToKeep instanceof ClippingPlane) { + planeToKeep.index = i; + } } + + // Indicate planes texture is dirty + this._manyDirtyPlanes = true; planes.length = length; return true; @@ -234,53 +346,203 @@ define([ * @see ClippingPlaneCollection#remove */ ClippingPlaneCollection.prototype.removeAll = function() { + // Dereference this ClippingPlaneCollection from all ClippingPlanes + var planes = this._planes; + var planesCount = planes.length; + for (var i = 0; i < planesCount; ++i) { + var plane = planes[i]; + if (plane instanceof ClippingPlane) { + plane.onChangeCallback = undefined; + plane.index = -1; + } + } + this._manyDirtyPlanes = true; this._planes = []; }; - var scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0); - var scratchMatrix = new Matrix4(); + var octEncodeScratch = new Cartesian2(); + var rightShift = 1.0 / 256.0; /** - * Applies the transformations to each plane and packs it into an array. - * @private - * - * @param {Matrix4} viewMatrix The 4x4 matrix to transform the plane into eyespace. - * @param {Cartesian4[]} [array] The array into which the planes will be packed. - * @returns {Cartesian4[]} The array of packed planes. + * Encodes a normalized vector into 4 SNORM values in the range [0-255] following the 'oct' encoding. + * oct32 precision is higher than the default oct16, hence the additional 2 uint16 values. */ - ClippingPlaneCollection.prototype.transformAndPackPlanes = function(viewMatrix, array) { - //>>includeStart('debug', pragmas.debug); - Check.typeOf.object('viewMatrix', viewMatrix); - //>>includeEnd('debug'); + function oct32EncodeNormal(vector, result) { + AttributeCompression.octEncodeInRange(vector, 65535, octEncodeScratch); + result.x = octEncodeScratch.x * rightShift; + result.y = octEncodeScratch.x; + result.z = octEncodeScratch.y * rightShift; + result.w = octEncodeScratch.y; + return result; + } - var planes = this._planes; - var length = planes.length; + var distanceEncodeScratch = new Cartesian4(); + var oct32EncodeScratch = new Cartesian4(); + function packPlanesAsUint8(clippingPlaneCollection, startIndex, endIndex) { + var uint8View = clippingPlaneCollection._uint8View; + var planes = clippingPlaneCollection._planes; + var byteIndex = 0; + for (var i = startIndex; i < endIndex; ++i) { + var plane = planes[i]; - var index = 0; - if (!defined(array)) { - array = new Array(length); - } else { - index = array.length; - array.length = length; - } + var oct32Normal = oct32EncodeNormal(plane.normal, oct32EncodeScratch); + uint8View[byteIndex] = oct32Normal.x; + uint8View[byteIndex + 1] = oct32Normal.y; + uint8View[byteIndex + 2] = oct32Normal.z; + uint8View[byteIndex + 3] = oct32Normal.w; - var i; - for (i = index; i < length; ++i) { - array[i] = new Cartesian4(); + var encodedDistance = Cartesian4.packFloat(plane.distance, distanceEncodeScratch); + uint8View[byteIndex + 4] = encodedDistance.x; + uint8View[byteIndex + 5] = encodedDistance.y; + uint8View[byteIndex + 6] = encodedDistance.z; + uint8View[byteIndex + 7] = encodedDistance.w; + + byteIndex += 8; } + } - var transform = Matrix4.multiply(viewMatrix, this.modelMatrix, scratchMatrix); + // Pack starting at the beginning of the buffer to allow partial update + function packPlanesAsFloats(clippingPlaneCollection, startIndex, endIndex) { + var float32View = clippingPlaneCollection._float32View; + var planes = clippingPlaneCollection._planes; - for (i = 0; i < length; ++i) { + var floatIndex = 0; + for (var i = startIndex; i < endIndex; ++i) { var plane = planes[i]; - var packedPlane = array[i]; + var normal = plane.normal; - Plane.transform(plane, transform, scratchPlane); + float32View[floatIndex] = normal.x; + float32View[floatIndex + 1] = normal.y; + float32View[floatIndex + 2] = normal.z; + float32View[floatIndex + 3] = plane.distance; - Cartesian3.clone(scratchPlane.normal, packedPlane); - packedPlane.w = scratchPlane.distance; + floatIndex += 4; // each plane is 4 floats } + } + + function computeTextureResolution(pixelsNeeded, result) { + var maxSize = ContextLimits.maximumTextureSize; + var width = Math.min(pixelsNeeded, maxSize); + var height = Math.ceil(pixelsNeeded / width); + result.x = width; + result.y = height; + return result; + } - return array; + var textureResolutionScratch = new Cartesian2(); + /** + * Called when {@link Viewer} or {@link CesiumWidget} render the scene to + * build the resources for clipping planes. + *

+ * Do not call this function directly. + *

+ */ + ClippingPlaneCollection.prototype.update = function(frameState) { + var clippingPlanesTexture = this._clippingPlanesTexture; + var context = frameState.context; + var useFloatTexture = ClippingPlaneCollection.useFloatTexture(context); + + // Compute texture requirements for current planes + // In RGBA FLOAT, A plane is 4 floats packed to a RGBA. + // In RGBA UNSIGNED_BYTE, A plane is a float in [0, 1) packed to RGBA and an Oct32 quantized normal, + // so 8 bits or 2 pixels in RGBA. + var pixelsNeeded = useFloatTexture ? this.length : this.length * 2; + var requiredResolution = computeTextureResolution(pixelsNeeded, textureResolutionScratch); + + if (defined(clippingPlanesTexture)) { + var currentPixelCount = clippingPlanesTexture.width * clippingPlanesTexture.height; + // Recreate the texture if it isn't big enough or is 4 times larger than it needs to be + if (currentPixelCount < pixelsNeeded || + pixelsNeeded < 0.25 * currentPixelCount) { + clippingPlanesTexture.destroy(); + clippingPlanesTexture = undefined; + } + } + + if (!defined(clippingPlanesTexture)) { + // Allocate twice as much space as needed to avoid frequent texture reallocation. + requiredResolution.x *= 2; + + var sampler = new Sampler({ + wrapS : TextureWrap.CLAMP_TO_EDGE, + wrapT : TextureWrap.CLAMP_TO_EDGE, + minificationFilter : TextureMinificationFilter.NEAREST, + magnificationFilter : TextureMagnificationFilter.NEAREST + }); + + if (useFloatTexture) { + clippingPlanesTexture = new Texture({ + context : context, + width : requiredResolution.x, + height : requiredResolution.y, + pixelFormat : PixelFormat.RGBA, + pixelDatatype : PixelDatatype.FLOAT, + sampler : sampler, + flipY : false + }); + this._float32View = new Float32Array(requiredResolution.x * requiredResolution.y * 4); + } else { + clippingPlanesTexture = new Texture({ + context : context, + width : requiredResolution.x, + height : requiredResolution.y, + pixelFormat : PixelFormat.RGBA, + pixelDatatype : PixelDatatype.UNSIGNED_BYTE, + sampler : sampler, + flipY : false + }); + this._uint8View = new Uint8Array(requiredResolution.x * requiredResolution.y * 4); + } + + this._clippingPlanesTexture = clippingPlanesTexture; + this._manyDirtyPlanes = true; + } + + // Use of Plane objects will be deprecated. + // But until then, we have no way of telling if they changed since last frame, so we have to do a full udpate. + var refreshFullTexture = this._manyDirtyPlanes || this._containsUntrackablePlanes; + var dirtyIndex = this._dirtyIndex; + + if (!refreshFullTexture && dirtyIndex === -1) { + return; + } + + if (!refreshFullTexture) { + // partial updates possible + var offsetY = Math.floor(dirtyIndex / clippingPlanesTexture.width); + var offsetX = Math.floor(dirtyIndex - offsetY * clippingPlanesTexture.width); + if (useFloatTexture) { + packPlanesAsFloats(this, dirtyIndex, dirtyIndex + 1); + clippingPlanesTexture.copyFrom({ + width : 1, + height : 1, + arrayBufferView : this._float32View + }, offsetX, offsetY); + } else { + packPlanesAsUint8(this, dirtyIndex, dirtyIndex + 1); + clippingPlanesTexture.copyFrom({ + width : 2, + height : 1, + arrayBufferView : this._uint8View + }, offsetX, offsetY); + } + } else if (useFloatTexture) { + packPlanesAsFloats(this, 0, this._planes.length); + clippingPlanesTexture.copyFrom({ + width : clippingPlanesTexture.width, + height : clippingPlanesTexture.height, + arrayBufferView : this._float32View + }); + } else { + packPlanesAsUint8(this, 0, this._planes.length); + clippingPlanesTexture.copyFrom({ + width : clippingPlanesTexture.width, + height : clippingPlanesTexture.height, + arrayBufferView : this._uint8View + }); + } + + this._manyDirtyPlanes = false; + this._dirtyIndex = -1; }; /** @@ -302,12 +564,17 @@ define([ planes.length = length; for (i = index; i < length; ++i) { - result._planes[i] = new Plane(Cartesian3.UNIT_X, 0.0); + result._planes[i] = new ClippingPlane(Cartesian3.UNIT_X, 0.0); } } for (i = 0; i < length; ++i) { - Plane.clone(this._planes[i], result._planes[i]); + var resultPlane = result._planes[i]; + resultPlane.index = i; + resultPlane.onChangeCallback = function(index) { + setIndexDirty(result, index); + }; + ClippingPlane.clone(this._planes[i], resultPlane); } result.enabled = this.enabled; @@ -319,18 +586,20 @@ define([ return result; }; + var scratchMatrix = new Matrix4(); + var scratchPlane = new Plane(Cartesian3.UNIT_X, 0.0); /** - * Determines the type intersection with the planes of this ClippingPlaneCollection instance and the specified {@link BoundingVolume}. + * Determines the type intersection with the planes of this ClippingPlaneCollection instance and the specified {@link TileBoundingVolume}. * @private * - * @param {Object} boundingVolume The volume to determine the intersection with the planes. + * @param {Object} tileBoundingVolume 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. */ - ClippingPlaneCollection.prototype.computeIntersectionWithBoundingVolume = function(boundingVolume, transform) { + ClippingPlaneCollection.prototype.computeIntersectionWithBoundingVolume = function(tileBoundingVolume, transform) { var planes = this._planes; var length = planes.length; @@ -351,9 +620,9 @@ define([ for (var i = 0; i < length; ++i) { var plane = planes[i]; - Plane.transform(plane, modelMatrix, scratchPlane); + Plane.transform(plane, modelMatrix, scratchPlane); // ClippingPlane can be used for Plane math - var value = boundingVolume.intersectPlane(scratchPlane); + var value = tileBoundingVolume.intersectPlane(scratchPlane); if (value === Intersect.INTERSECTING) { intersection = value; } else if (this._testIntersection(value)) { @@ -364,22 +633,103 @@ define([ return intersection; }; + /** + * Returns a Number encapsulating the state for this ClippingPlaneCollection. + * + * Clipping mode is encoded in the sign of the number, which is just the plane count. + * Used for checking if shader regeneration is necessary. + * + * @returns {Number} A Number that describes the ClippingPlaneCollection's state. + * @private + */ + ClippingPlaneCollection.prototype.clippingPlanesState = function() { + return this._unionClippingRegions ? this._planes.length : -this._planes.length; + }; + + /** + * Sets the owner for the input ClippingPlaneCollection if there wasn't another owner. + * Destroys the owner's previous ClippingPlaneCollection if setting is successful. + * + * @param {ClippingPlaneCollection} [clippingPlaneCollection] A ClippingPlaneCollection (or undefined) being attached to an object + * @param {Object} owner An Object that should receive the new ClippingPlaneCollection + * @param {String} key The Key for the Object to reference the ClippingPlaneCollection + * @private + */ + ClippingPlaneCollection.setOwnership = function(clippingPlaneCollection, owner, key) { + // Don't destroy the ClippingPlaneCollection if it is already owned by newOwner + if (clippingPlaneCollection === owner[key]) { + return; + } + // Destroy the existing ClippingPlaneCollection, if any + owner[key] = owner[key] && owner[key].destroy(); + if (defined(clippingPlaneCollection)) { + //>>includeStart('debug', pragmas.debug); + if (defined(clippingPlaneCollection._owner)) { + throw new DeveloperError('ClippingPlaneCollection should only be assigned to one object'); + } + //>>includeEnd('debug'); + clippingPlaneCollection._owner = owner; + owner[key] = clippingPlaneCollection; + } + }; + /** * Determines if rendering with clipping planes is supported. * - * @returns {Boolean} true if ClippingPlaneCollections are supported; otherwise, returns false + * @returns {Boolean} true if ClippingPlaneCollections are supported + * @deprecated */ ClippingPlaneCollection.isSupported = function() { - return !FeatureDetection.isInternetExplorer(); + return true; + }; + + /** + * Function for checking if the context will allow clipping planes with floating point textures. + * + * @param {Context} context The Context that will contain clipped objects and clipping textures. + * @returns {Boolean} true if floating point textures can be used for clipping planes. + * @private + */ + ClippingPlaneCollection.useFloatTexture = function(context) { + return context.floatingPointTexture; + }; + + /** + * Returns true if this object was destroyed; otherwise, false. + *

+ * If this object was destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. + * + * @returns {Boolean} true if this object was destroyed; otherwise, false. + * + * @see ClippingPlaneCollection#destroy + */ + ClippingPlaneCollection.prototype.isDestroyed = function() { + return false; }; /** - * The maximum number of supported clipping planes. + * Destroys the WebGL resources held by this object. Destroying an object allows for deterministic + * release of WebGL resources, instead of relying on the garbage collector to destroy this object. + *

+ * Once an object is destroyed, it should not be used; calling any function other than + * isDestroyed will result in a {@link DeveloperError} exception. Therefore, + * assign the return value (undefined) to the object as done in the example. + * + * @returns {undefined} * - * @type {number} - * @constant + * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called. + * + * + * @example + * clippingPlanes = clippingPlanes && clippingPlanes .destroy(); + * + * @see ClippingPlaneCollection#isDestroyed */ - ClippingPlaneCollection.MAX_CLIPPING_PLANES = 6; + ClippingPlaneCollection.prototype.destroy = function() { + this._clippingPlanesTexture = this._clippingPlanesTexture && this._clippingPlanesTexture.destroy(); + return destroyObject(this); + }; return ClippingPlaneCollection; }); diff --git a/Source/DataSources/ModelGraphics.js b/Source/DataSources/ModelGraphics.js index 5dedd97c6f94..1b6c77f82b32 100644 --- a/Source/DataSources/ModelGraphics.js +++ b/Source/DataSources/ModelGraphics.js @@ -55,7 +55,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 the {@link ClippingPlaneCollection} used to selectively disable rendering the model. Clipping planes are not currently supported in Internet Explorer. + * @param {Property} [options.clippingPlanes] A property specifying the {@link ClippingPlaneCollection} used to selectively disable rendering the model. * * @see {@link https://cesiumjs.org/tutorials/3D-Models-Tutorial/|3D Models Tutorial} * @demo {@link https://cesiumjs.org/Cesium/Apps/Sandcastle/index.html?src=3D%20Models.html|Cesium Sandcastle 3D Models Demo} diff --git a/Source/Renderer/AutomaticUniforms.js b/Source/Renderer/AutomaticUniforms.js index 3e218afd105e..194af4c1a20e 100644 --- a/Source/Renderer/AutomaticUniforms.js +++ b/Source/Renderer/AutomaticUniforms.js @@ -912,6 +912,20 @@ define([ } }), + /** + * An automatic GLSL uniform that indicates if the current camera is orthographic in 3D. + * + * @alias czm_orthographicIn3D + * @see UniformState#orthographicIn3D + */ + czm_orthographicIn3D : new AutomaticUniform({ + size : 1, + datatype : WebGLConstants.FLOAT, + getValue : function(uniformState) { + return uniformState.orthographicIn3D ? 1 : 0; + } + }), + /** * An automatic GLSL uniform representing a 3x3 normal transformation matrix that * transforms normal vectors in model coordinates to eye coordinates. diff --git a/Source/Renderer/UniformState.js b/Source/Renderer/UniformState.js index 89c87d7a88b8..00eddf4756f3 100644 --- a/Source/Renderer/UniformState.js +++ b/Source/Renderer/UniformState.js @@ -861,6 +861,18 @@ define([ get : function() { return this._invertClassificationColor; } + }, + + /** + * Whether or not the current projection is orthographic in 3D. + * + * @memberOf UniformState.prototype + * @type {Boolean} + */ + orthographicIn3D : { + get : function() { + return this._orthographicIn3D; + } } }); diff --git a/Source/Scene/BatchTable.js b/Source/Scene/BatchTable.js index e32799823b04..bbe799c529c9 100644 --- a/Source/Scene/BatchTable.js +++ b/Source/Scene/BatchTable.js @@ -16,7 +16,8 @@ define([ '../Renderer/Sampler', '../Renderer/Texture', '../Renderer/TextureMagnificationFilter', - '../Renderer/TextureMinificationFilter' + '../Renderer/TextureMinificationFilter', + './getUnpackFloatFunction' ], function( Cartesian2, Cartesian3, @@ -35,7 +36,8 @@ define([ Sampler, Texture, TextureMagnificationFilter, - TextureMinificationFilter) { + TextureMinificationFilter, + getUnpackFloatFunction) { 'use strict'; /** @@ -226,48 +228,18 @@ define([ var scratchPackedFloatCartesian4 = new Cartesian4(); - var SHIFT_LEFT_8 = 256.0; - var SHIFT_LEFT_16 = 65536.0; - var SHIFT_LEFT_24 = 16777216.0; - - var SHIFT_RIGHT_8 = 1.0 / SHIFT_LEFT_8; - var SHIFT_RIGHT_16 = 1.0 / SHIFT_LEFT_16; - var SHIFT_RIGHT_24 = 1.0 / SHIFT_LEFT_24; - - var BIAS = 38.0; - - function unpackFloat(value) { - var temp = value.w / 2.0; - var exponent = Math.floor(temp); - var sign = (temp - exponent) * 2.0; - exponent = exponent - BIAS; - - sign = sign * 2.0 - 1.0; - sign = -sign; - - if (exponent >= BIAS) { - return sign < 0.0 ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY; - } - - var unpacked = sign * value.x * SHIFT_RIGHT_8; - unpacked += sign * value.y * SHIFT_RIGHT_16; - unpacked += sign * value.z * SHIFT_RIGHT_24; - - return unpacked * Math.pow(10.0, exponent); - } - function getPackedFloat(array, index, result) { var packed = Cartesian4.unpack(array, index, scratchPackedFloatCartesian4); - var x = unpackFloat(packed); + var x = Cartesian4.unpackFloat(packed); packed = Cartesian4.unpack(array, index + 4, scratchPackedFloatCartesian4); - var y = unpackFloat(packed); + var y = Cartesian4.unpackFloat(packed); packed = Cartesian4.unpack(array, index + 8, scratchPackedFloatCartesian4); - var z = unpackFloat(packed); + var z = Cartesian4.unpackFloat(packed); packed = Cartesian4.unpack(array, index + 12, scratchPackedFloatCartesian4); - var w = unpackFloat(packed); + var w = Cartesian4.unpackFloat(packed); return Cartesian4.fromElements(x, y, z, w, result); } @@ -275,50 +247,18 @@ define([ if (!FeatureDetection.supportsTypedArrays()) { return; } - var scratchFloatArray = new Float32Array(1); - - function packFloat(value, result) { - scratchFloatArray[0] = value; - value = scratchFloatArray[0]; - - if (value === 0.0) { - return Cartesian4.clone(Cartesian4.ZERO, result); - } - - var sign = value < 0.0 ? 1.0 : 0.0; - var exponent; - - if (!isFinite(value)) { - value = 0.1; - exponent = BIAS; - } else { - value = Math.abs(value); - exponent = Math.floor(CesiumMath.logBase(value, 10)) + 1.0; - value = value / Math.pow(10.0, exponent); - } - - var temp = value * SHIFT_LEFT_8; - result.x = Math.floor(temp); - temp = (temp - result.x) * SHIFT_LEFT_8; - result.y = Math.floor(temp); - temp = (temp - result.y) * SHIFT_LEFT_8; - result.z = Math.floor(temp); - result.w = (exponent + BIAS) * 2.0 + sign; - - return result; - } function setPackedAttribute(value, array, index) { - var packed = packFloat(value.x, scratchPackedFloatCartesian4); + var packed = Cartesian4.packFloat(value.x, scratchPackedFloatCartesian4); Cartesian4.pack(packed, array, index); - packed = packFloat(value.y, packed); + packed = Cartesian4.packFloat(value.y, packed); Cartesian4.pack(packed, array, index + 4); - packed = packFloat(value.z, packed); + packed = Cartesian4.packFloat(value.z, packed); Cartesian4.pack(packed, array, index + 8); - packed = packFloat(value.w, packed); + packed = Cartesian4.packFloat(value.w, packed); Cartesian4.pack(packed, array, index + 12); } @@ -527,20 +467,7 @@ define([ return ''; } - return 'float unpackFloat(vec4 value) \n' + - '{ \n' + - ' value *= 255.0; \n' + - ' float temp = value.w / 2.0; \n' + - ' float exponent = floor(temp); \n' + - ' float sign = (temp - exponent) * 2.0; \n' + - ' exponent = exponent - float(' + BIAS + '); \n' + - ' sign = sign * 2.0 - 1.0; \n' + - ' sign = -sign; \n' + - ' float unpacked = sign * value.x * float(' + SHIFT_RIGHT_8 + '); \n' + - ' unpacked += sign * value.y * float(' + SHIFT_RIGHT_16 + '); \n' + - ' unpacked += sign * value.z * float(' + SHIFT_RIGHT_24 + '); \n' + - ' return unpacked * pow(10.0, exponent); \n' + - '} \n'; + return getUnpackFloatFunction('unpackFloat'); } function getComponentType(componentsPerAttribute) { diff --git a/Source/Scene/Batched3DModel3DTileContent.js b/Source/Scene/Batched3DModel3DTileContent.js index 5ecb891e1a73..2f6f2a350dd0 100644 --- a/Source/Scene/Batched3DModel3DTileContent.js +++ b/Source/Scene/Batched3DModel3DTileContent.js @@ -1,5 +1,4 @@ define([ - '../Core/ClippingPlaneCollection', '../Core/Color', '../Core/defaultValue', '../Core/defined', @@ -20,7 +19,6 @@ define([ './Model', './ModelUtility' ], function( - ClippingPlaneCollection, Color, defaultValue, defined, @@ -69,6 +67,10 @@ define([ this._batchTable = undefined; this._features = undefined; + // Populate from gltf when available + this._batchIdAttributeName = undefined; + this._diffuseAttributeOrUniformName = {}; + /** * @inheritdoc Cesium3DTileContent#featurePropertiesDirty */ @@ -206,11 +208,15 @@ define([ function getVertexShaderCallback(content) { return function(vs, programId) { var batchTable = content._batchTable; - var gltf = content._model.gltf; var handleTranslucent = !defined(content._tileset.classificationType); - var batchIdAttributeName = getBatchIdAttributeName(gltf); - var diffuseAttributeOrUniformName = ModelUtility.getDiffuseAttributeOrUniform(gltf, programId); - var callback = batchTable.getVertexShaderCallback(handleTranslucent, batchIdAttributeName, diffuseAttributeOrUniformName); + + var gltf = content._model.gltf; + if (defined(gltf)) { + content._batchIdAttributeName = getBatchIdAttributeName(gltf); + content._diffuseAttributeOrUniformName[programId] = ModelUtility.getDiffuseAttributeOrUniform(gltf, programId); + } + + var callback = batchTable.getVertexShaderCallback(handleTranslucent, content._batchIdAttributeName, content._diffuseAttributeOrUniformName[programId]); return defined(callback) ? callback(vs) : vs; }; } @@ -218,9 +224,13 @@ define([ function getPickVertexShaderCallback(content) { return function(vs) { var batchTable = content._batchTable; + var gltf = content._model.gltf; - var batchIdAttributeName = getBatchIdAttributeName(gltf); - var callback = batchTable.getPickVertexShaderCallback(batchIdAttributeName); + if (defined(gltf)) { + content._batchIdAttributeName = getBatchIdAttributeName(gltf); + } + + var callback = batchTable.getPickVertexShaderCallback(content._batchIdAttributeName); return defined(callback) ? callback(vs) : vs; }; } @@ -228,10 +238,13 @@ define([ function getFragmentShaderCallback(content) { return function(fs, programId) { var batchTable = content._batchTable; - var gltf = content._model.gltf; var handleTranslucent = !defined(content._tileset.classificationType); - var diffuseAttributeOrUniformName = ModelUtility.getDiffuseAttributeOrUniform(gltf, programId); - var callback = batchTable.getFragmentShaderCallback(handleTranslucent, diffuseAttributeOrUniformName); + + var gltf = content._model.gltf; + if (defined(gltf)) { + content._diffuseAttributeOrUniformName[programId] = ModelUtility.getDiffuseAttributeOrUniform(gltf, programId); + } + var callback = batchTable.getFragmentShaderCallback(handleTranslucent, content._diffuseAttributeOrUniformName[programId]); return defined(callback) ? callback(fs) : fs; }; } @@ -379,15 +392,6 @@ define([ }; if (!defined(tileset.classificationType)) { - var clippingPlanes; - if (defined(tileset.clippingPlanes)) { - clippingPlanes = tileset.clippingPlanes.clone(); - } else { - clippingPlanes = new ClippingPlaneCollection({ - enabled : false - }); - } - // PERFORMANCE_IDEA: patch the shader on demand, e.g., the first time show/color changes. // The pick shader still needs to be patched. content._model = new Model({ @@ -409,8 +413,7 @@ define([ pickFragmentShaderLoaded : batchTable.getPickFragmentShaderCallback(), pickUniformMapLoaded : batchTable.getPickUniformMapCallback(), addBatchIdToGeneratedShaders : (batchLength > 0), // If the batch table has values in it, generated shaders will need a batchId attribute - pickObject : pickObject, - clippingPlanes : clippingPlanes + pickObject : pickObject }); } else { // This transcodes glTF to an internal representation for geometry so we can take advantage of the re-batching of vector data. @@ -503,12 +506,11 @@ define([ // Update clipping planes var tilesetClippingPlanes = this._tileset.clippingPlanes; - var modelClippingPlanes = this._model.clippingPlanes; - if (defined(tilesetClippingPlanes)) { - tilesetClippingPlanes.clone(modelClippingPlanes); - modelClippingPlanes.enabled = tilesetClippingPlanes.enabled && this._tile._isClipped; - } else if (defined(modelClippingPlanes) && modelClippingPlanes.enabled) { - modelClippingPlanes.enabled = false; + if (this._tile.clippingPlanesDirty && defined(tilesetClippingPlanes)) { + // Dereference the clipping planes from the model if they are irrelevant. + // Link/Dereference directly to avoid ownership checks. + // This will also trigger synchronous shader regeneration to remove or add the clipping plane and color blending code. + this._model._clippingPlanes = (tilesetClippingPlanes.enabled && this._tile._isClipped) ? tilesetClippingPlanes : undefined; } this._model.update(frameState); diff --git a/Source/Scene/Cesium3DTile.js b/Source/Scene/Cesium3DTile.js index 5ffb0114e887..e386a997586b 100644 --- a/Source/Scene/Cesium3DTile.js +++ b/Source/Scene/Cesium3DTile.js @@ -310,6 +310,16 @@ define([ */ this._optimChildrenWithinParent = Cesium3DTileOptimizationHint.NOT_COMPUTED; + /** + * Tracks if the tile's relationship with a ClippingPlaneCollection has changed with regards + * to the ClippingPlaneCollection's state. + * + * @type {Boolean} + * + * @private + */ + this.clippingPlanesDirty = false; + // Members that are updated every frame for tree traversal and rendering optimizations: this._distanceToCamera = 0; this._visibilityPlaneMask = 0; @@ -329,6 +339,7 @@ define([ this._ancestorWithContent = undefined; this._ancestorWithLoadedContent = undefined; this._isClipped = true; + this._clippingPlanesState = 0; // encapsulates (_isClipped, clippingPlanes.enabled) and number/function this._debugBoundingVolume = undefined; this._debugContentBoundingVolume = undefined; @@ -1048,6 +1059,23 @@ define([ content.update(tileset, frameState); } + function updateClippingPlanes(tile, tileset) { + // Compute and compare ClippingPlanes state: + // - enabled-ness - are clipping planes enabled? is this tile clipped? + // - clipping plane count + // - clipping function (union v. intersection) + var clippingPlanes = tileset.clippingPlanes; + var currentClippingPlanesState = 0; + if (defined(clippingPlanes) && tile._isClipped && clippingPlanes.enabled) { + currentClippingPlanesState = clippingPlanes.clippingPlanesState(); + } + // If clippingPlaneState for tile changed, mark clippingPlanesDirty so content can update + if (currentClippingPlanesState !== tile._clippingPlanesState) { + tile._clippingPlanesState = currentClippingPlanesState; + tile.clippingPlanesDirty = true; + } + } + /** * Get the draw commands needed to render this tile. * @@ -1055,9 +1083,12 @@ define([ */ Cesium3DTile.prototype.update = function(tileset, frameState) { var initCommandLength = frameState.commandList.length; + updateClippingPlanes(this, tileset); applyDebugSettings(this, tileset, frameState); updateContent(this, tileset, frameState); this._commandsLength = frameState.commandList.length - initCommandLength; + + this.clippingPlanesDirty = false; // reset after content update }; var scratchCommandList = []; diff --git a/Source/Scene/Cesium3DTileBatchTable.js b/Source/Scene/Cesium3DTileBatchTable.js index 345e5125cdc0..42c32407f0c9 100644 --- a/Source/Scene/Cesium3DTileBatchTable.js +++ b/Source/Scene/Cesium3DTileBatchTable.js @@ -1331,10 +1331,12 @@ define([ for (var i = commandStart; i < commandEnd; ++i) { var command = commandList[i]; var derivedCommands = command.derivedCommands.tileset; - if (!defined(derivedCommands)) { + // Command may be marked dirty from Model shader recompilation for clipping planes + if (!defined(derivedCommands) || command.dirty) { derivedCommands = {}; command.derivedCommands.tileset = derivedCommands; derivedCommands.originalCommand = deriveCommand(command); + command.dirty = false; } updateDerivedCommand(derivedCommands.originalCommand, command); diff --git a/Source/Scene/Cesium3DTileset.js b/Source/Scene/Cesium3DTileset.js index 699fc5d41595..ddfbdcab4d21 100644 --- a/Source/Scene/Cesium3DTileset.js +++ b/Source/Scene/Cesium3DTileset.js @@ -3,6 +3,7 @@ define([ '../Core/Cartesian3', '../Core/Cartographic', '../Core/Check', + '../Core/ClippingPlaneCollection', '../Core/defaultValue', '../Core/defined', '../Core/defineProperties', @@ -46,6 +47,7 @@ define([ Cartesian3, Cartographic, Check, + ClippingPlaneCollection, defaultValue, defined, defineProperties, @@ -111,7 +113,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 {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset. Clipping planes are not currently supported in Internet Explorer. + * @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset. * @param {ClassificationType} [options.classificationType] Determines whether terrain, 3D Tiles or both will be classified by this tileset. See {@link Cesium3DTileset#classificationType} for details about restrictions and limitations. * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid determining the size and shape of the globe. * @param {Boolean} [options.debugFreezeFrame=false] For debugging only. Determines if only the tiles from last frame should be used for rendering. @@ -547,11 +549,7 @@ define([ */ this.loadSiblings = defaultValue(options.loadSiblings, false); - /** - * The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset. Clipping planes are not currently supported in Internet Explorer. - * - * @type {ClippingPlaneCollection} - */ + this._clippingPlanes = undefined; this.clippingPlanes = options.clippingPlanes; /** @@ -814,6 +812,19 @@ define([ return this._asset; } }, + /** + * The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset. + * + * @type {ClippingPlaneCollection} + */ + clippingPlanes : { + get : function() { + return this._clippingPlanes; + }, + set : function(value) { + ClippingPlaneCollection.setOwnership(value, this, '_clippingPlanes'); + } + }, /** * Gets the tileset's properties dictionary object, which contains metadata about per-feature properties. @@ -1846,6 +1857,12 @@ define([ this._loadTimestamp = JulianDate.clone(frameState.time); } + // Update clipping planes + var clippingPlanes = this._clippingPlanes; + if (defined(clippingPlanes) && clippingPlanes.enabled) { + clippingPlanes.update(frameState); + } + this._timeSinceLoad = Math.max(JulianDate.secondsDifference(frameState.time, this._loadTimestamp) * 1000, 0.0); this._skipLevelOfDetail = this.skipLevelOfDetail && !defined(this._classificationType) && !this._disableSkipLevelOfDetail; @@ -1930,6 +1947,9 @@ define([ // Destroy debug labels this._tileDebugLabels = this._tileDebugLabels && this._tileDebugLabels.destroy(); + // Destroy clipping plane collection + this._clippingPlanes = this._clippingPlanes && this._clippingPlanes.destroy(); + // Traverse the tree and destroy all tiles if (defined(this._root)) { var stack = scratchStack; diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 343295c50c1d..5d391dbebf31 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -266,7 +266,7 @@ define([ } }, /** - * A property specifying a {@link ClippingPlaneCollection} used to selectively disable rendering on the outside of each plane. Clipping planes are not currently supported in Internet Explorer. + * A property specifying a {@link ClippingPlaneCollection} used to selectively disable rendering on the outside of each plane. * * @memberof Globe.prototype * @type {ClippingPlaneCollection} diff --git a/Source/Scene/GlobeSurfaceShaderSet.js b/Source/Scene/GlobeSurfaceShaderSet.js index c46fe650b6bf..85c775b1c00c 100644 --- a/Source/Scene/GlobeSurfaceShaderSet.js +++ b/Source/Scene/GlobeSurfaceShaderSet.js @@ -1,22 +1,27 @@ define([ + '../Core/ClippingPlaneCollection', '../Core/defined', '../Core/destroyObject', '../Core/TerrainQuantization', '../Renderer/ShaderProgram', - '../Scene/SceneMode' + '../Scene/SceneMode', + './getClippingFunction' ], function( + ClippingPlaneCollection, defined, destroyObject, TerrainQuantization, ShaderProgram, - SceneMode) { + SceneMode, + getClippingFunction) { 'use strict'; - function GlobeSurfaceShader(numberOfDayTextures, flags, material, shaderProgram) { + function GlobeSurfaceShader(numberOfDayTextures, flags, material, shaderProgram, clippingShaderState) { this.numberOfDayTextures = numberOfDayTextures; this.flags = flags; this.material = material; this.shaderProgram = shaderProgram; + this.clippingShaderState = clippingShaderState; } /** @@ -64,7 +69,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, enableClippingPlanes, unionClippingRegions) { + GlobeSurfaceShaderSet.prototype.getShaderProgram = function(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, enableLighting, hasVertexNormals, useWebMercatorProjection, enableFog, enableClippingPlanes, clippingPlanes) { var quantization = 0; var quantizationDefine = ''; @@ -93,27 +98,36 @@ define([ (applySplit << 15) | (enableClippingPlanes << 16); + var currentClippingShaderState = 0; + if (defined(clippingPlanes)) { + currentClippingShaderState = enableClippingPlanes ? clippingPlanes.clippingPlanesState() : 0; + } var surfaceShader = surfaceTile.surfaceShader; if (defined(surfaceShader) && surfaceShader.numberOfDayTextures === numberOfDayTextures && surfaceShader.flags === flags && - surfaceShader.material === this.material) { + surfaceShader.material === this.material && + surfaceShader.clippingShaderState === currentClippingShaderState) { return surfaceShader.shaderProgram; } - // New tile, or tile changed number of textures or flags. + // New tile, or tile changed number of textures, flags, or clipping planes var shadersByFlags = this._shadersByTexturesFlags[numberOfDayTextures]; if (!defined(shadersByFlags)) { shadersByFlags = this._shadersByTexturesFlags[numberOfDayTextures] = []; } surfaceShader = shadersByFlags[flags]; - if (!defined(surfaceShader) || surfaceShader.material !== this.material) { + if (!defined(surfaceShader) || surfaceShader.material !== this.material || surfaceShader.clippingShaderState !== currentClippingShaderState) { // Cache miss - we've never seen this combination of numberOfDayTextures and flags before. var vs = this.baseVertexShaderSource.clone(); var fs = this.baseFragmentShaderSource.clone(); + if (currentClippingShaderState !== 0) { + fs.sources.unshift(getClippingFunction(clippingPlanes)); // Need to go before GlobeFS + } + vs.defines.push(quantizationDefine); fs.defines.push('TEXTURE_UNITS ' + numberOfDayTextures); @@ -167,10 +181,6 @@ define([ if (enableClippingPlanes) { fs.defines.push('ENABLE_CLIPPING_PLANES'); - - if (unionClippingRegions) { - fs.defines.push('UNION_CLIPPING_REGIONS'); - } } var computeDayColor = '\ @@ -212,7 +222,7 @@ define([ attributeLocations : terrainEncoding.getAttributeLocations() }); - surfaceShader = shadersByFlags[flags] = new GlobeSurfaceShader(numberOfDayTextures, flags, this.material, shader); + surfaceShader = shadersByFlags[flags] = new GlobeSurfaceShader(numberOfDayTextures, flags, this.material, shader, currentClippingShaderState); } surfaceTile.surfaceShader = surfaceShader; diff --git a/Source/Scene/GlobeSurfaceTileProvider.js b/Source/Scene/GlobeSurfaceTileProvider.js index ddf1fc10d641..9d9ea96e7e7b 100644 --- a/Source/Scene/GlobeSurfaceTileProvider.js +++ b/Source/Scene/GlobeSurfaceTileProvider.js @@ -172,7 +172,7 @@ define([ * @type {ClippingPlaneCollection} * @private */ - this.clippingPlanes = undefined; + this._clippingPlanes = undefined; } defineProperties(GlobeSurfaceTileProvider.prototype, { @@ -302,6 +302,21 @@ define([ this._quadtree.invalidateAllTiles(); } } + }, + /** + * The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset. + * + * @type {ClippingPlaneCollection} + * + * @private + */ + clippingPlanes : { + get : function() { + return this._clippingPlanes; + }, + set : function(value) { + ClippingPlaneCollection.setOwnership(value, this, '_clippingPlanes'); + } } }); @@ -396,7 +411,11 @@ define([ tiles.length = 0; } } - + // update clipping planes + var clippingPlanes = this._clippingPlanes; + if (defined(clippingPlanes) && clippingPlanes.enabled) { + clippingPlanes.update(frameState); + } this._usedDrawCommands = 0; }; @@ -548,7 +567,7 @@ define([ } } - var clippingPlanes = this.clippingPlanes; + var clippingPlanes = this._clippingPlanes; if (defined(clippingPlanes) && clippingPlanes.enabled) { var planeIntersection = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); tile.isClipped = (planeIntersection !== Intersect.INSIDE); @@ -666,6 +685,8 @@ define([ */ GlobeSurfaceTileProvider.prototype.destroy = function() { this._tileProvider = this._tileProvider && this._tileProvider.destroy(); + this._clippingPlanes = this._clippingPlanes && this._clippingPlanes.destroy(); + return destroyObject(this); }; @@ -826,7 +847,8 @@ define([ } }; - function createTileUniformMap(frameState) { + var scratchClippingPlaneMatrix = new Matrix4(); + function createTileUniformMap(frameState, globeSurfaceTileProvider) { var uniformMap = { u_initialColor : function() { return this.properties.initialColor; @@ -914,11 +936,17 @@ define([ u_dayTextureSplit : function() { return this.properties.dayTextureSplit; }, - u_clippingPlanesLength : function() { - return this.properties.clippingPlanes.length; - }, u_clippingPlanes : function() { - return this.properties.clippingPlanes; + var clippingPlanes = globeSurfaceTileProvider._clippingPlanes; + if (defined(clippingPlanes) && defined(clippingPlanes.texture)) { + // Check in case clippingPlanes hasn't been updated yet. + return clippingPlanes.texture; + } + return frameState.context.defaultTexture; + }, + u_clippingPlanesMatrix : function() { + var clippingPlanes = globeSurfaceTileProvider._clippingPlanes; + return defined(clippingPlanes) ? Matrix4.multiply(frameState.context.uniformState.view, clippingPlanes.modelMatrix, scratchClippingPlaneMatrix) : Matrix4.IDENTITY; }, u_clippingPlanesEdgeStyle : function() { var style = this.properties.clippingPlanesEdgeColor; @@ -963,7 +991,6 @@ define([ minMaxHeight : new Cartesian2(), scaleAndBias : new Matrix4(), - clippingPlanes : [], clippingPlanesEdgeColor : Color.clone(Color.WHITE), clippingPlanesEdgeWidth : 0.0 } @@ -1211,7 +1238,7 @@ define([ command.boundingVolume = new BoundingSphere(); command.orientedBoundingBox = undefined; - uniformMap = createTileUniformMap(frameState); + uniformMap = createTileUniformMap(frameState, tileProvider); tileProvider._drawCommands.push(command); tileProvider._uniformMaps.push(uniformMap); @@ -1342,37 +1369,18 @@ define([ Matrix4.clone(encoding.matrix, uniformMapProperties.scaleAndBias); // update clipping planes - var clippingPlanes = tileProvider.clippingPlanes; - var length = 0; - - if (defined(clippingPlanes) && tile.isClipped) { - length = clippingPlanes.length; - } - - var packedPlanes = uniformMapProperties.clippingPlanes; - var packedLength = packedPlanes.length; - if (packedLength !== length) { - packedPlanes.length = length; - - for (var i = packedLength; i < length; ++i) { - packedPlanes[i] = new Cartesian4(); - } - } - - if (defined(clippingPlanes) && clippingPlanes.enabled && tile.isClipped) { - clippingPlanes.transformAndPackPlanes(context.uniformState.view, packedPlanes); + var clippingPlanes = tileProvider._clippingPlanes; + var clippingPlanesEnabled = defined(clippingPlanes) && clippingPlanes.enabled && tile.isClipped; + if (clippingPlanesEnabled) { uniformMapProperties.clippingPlanesEdgeColor = Color.clone(clippingPlanes.edgeColor, uniformMapProperties.clippingPlanesEdgeColor); uniformMapProperties.clippingPlanesEdgeWidth = clippingPlanes.edgeWidth; } - var clippingPlanesEnabled = defined(clippingPlanes) && clippingPlanes.enabled && (uniformMapProperties.clippingPlanes.length > 0) && ClippingPlaneCollection.isSupported(); - var unionClippingRegions = clippingPlanesEnabled ? clippingPlanes.unionClippingRegions : false; - if (defined(tileProvider.uniformMap)) { uniformMap = combine(uniformMap, tileProvider.uniformMap); } - command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog, clippingPlanesEnabled, unionClippingRegions); + command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(frameState, surfaceTile, numberOfDayTextures, applyBrightness, applyContrast, applyHue, applySaturation, applyGamma, applyAlpha, applySplit, showReflectiveOcean, showOceanWaves, tileProvider.enableLighting, hasVertexNormals, useWebMercatorProjection, applyFog, clippingPlanesEnabled, clippingPlanes); command.castShadows = castShadows; command.receiveShadows = receiveShadows; command.renderState = renderState; diff --git a/Source/Scene/Instanced3DModel3DTileContent.js b/Source/Scene/Instanced3DModel3DTileContent.js index 1373144e21bb..01f65513809f 100644 --- a/Source/Scene/Instanced3DModel3DTileContent.js +++ b/Source/Scene/Instanced3DModel3DTileContent.js @@ -1,7 +1,6 @@ define([ '../Core/AttributeCompression', '../Core/Cartesian3', - '../Core/ClippingPlaneCollection', '../Core/Color', '../Core/ComponentDatatype', '../Core/defaultValue', @@ -29,7 +28,6 @@ define([ ], function( AttributeCompression, Cartesian3, - ClippingPlaneCollection, Color, ComponentDatatype, defaultValue, @@ -511,24 +509,21 @@ define([ this._modelInstanceCollection.modelMatrix = this._tile.computedTransform; this._modelInstanceCollection.shadows = this._tileset.shadows; this._modelInstanceCollection.debugWireframe = this._tileset.debugWireframe; - this._modelInstanceCollection.update(frameState); - // Update clipping planes - var tilesetClippingPlanes = this._tileset.clippingPlanes; var model = this._modelInstanceCollection._model; - var modelClippingPlanes = model.clippingPlanes; - if (defined(tilesetClippingPlanes)) { - if (!defined(modelClippingPlanes)) { - model.clippingPlanes = new ClippingPlaneCollection(); - modelClippingPlanes = model.clippingPlanes; - } - tilesetClippingPlanes.clone(modelClippingPlanes); - modelClippingPlanes.enabled = tilesetClippingPlanes.enabled && this._tile._isClipped; - } else if (defined(modelClippingPlanes) && modelClippingPlanes.enabled) { - modelClippingPlanes.enabled = false; + if (defined(model)) { + // Update for clipping planes + var tilesetClippingPlanes = this._tileset.clippingPlanes; + if (this._tile.clippingPlanesDirty && defined(tilesetClippingPlanes)) { + // Dereference the clipping planes from the model if they are irrelevant - saves on shading + // Link/Dereference directly to avoid ownership checks. + model._clippingPlanes = (tilesetClippingPlanes.enabled && this._tile._isClipped) ? tilesetClippingPlanes : undefined; + } } + this._modelInstanceCollection.update(frameState); + // 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 97c47808ac3b..efdc3966523e 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -64,6 +64,7 @@ define([ './BlendingState', './ColorBlendMode', './DracoLoader', + './getClippingFunction', './HeightReference', './JobType', './ModelAnimationCache', @@ -141,6 +142,7 @@ define([ BlendingState, ColorBlendMode, DracoLoader, + getClippingFunction, HeightReference, JobType, ModelAnimationCache, @@ -269,7 +271,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 {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the model. Clipping planes are not currently supported in Internet Explorer. + * @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} 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. @@ -492,6 +494,7 @@ define([ this.color = defaultValue(options.color, Color.WHITE); this._color = new Color(); this._colorPreviousAlpha = 1.0; + this._hasPremultipliedAlpha = false; /** * Defines how the color blends with the model. @@ -513,12 +516,12 @@ define([ */ this.colorBlendAmount = defaultValue(options.colorBlendAmount, 0.5); - /** - * The {@link ClippingPlaneCollection} used to selectively disable rendering the model. Clipping planes are not currently supported in Internet Explorer. - * - * @type {ClippingPlaneCollection} - */ + this._colorShadingEnabled = isColorShadingEnabled(this); + + this._clippingPlanes = undefined; this.clippingPlanes = options.clippingPlanes; + // Used for checking if shaders need to be regenerated due to clipping plane changes. + this._clippingPlanesState = 0; /** * This property is for debugging only; it is not for production use nor is it optimized. @@ -626,6 +629,12 @@ define([ this._texturesByteLength = 0; this._trianglesLength = 0; + // Hold references to programs and shaders for shader reconstruction. + // Hold these separately because _cachedGltf may get released (this.releaseGltfJson) + this._sourcePrograms = undefined; + this._sourceShaders = undefined; + this._quantizedVertexShaders = {}; + this._nodeCommands = []; this._pickIds = []; @@ -634,8 +643,6 @@ define([ this._rtcCenterEye = undefined; // in eye coordinates this._rtcCenter3D = undefined; // in world coordinates this._rtcCenter2D = undefined; // in projected world coordinates - - this._packedClippingPlanes = []; } defineProperties(Model.prototype, { @@ -1002,6 +1009,26 @@ define([ get : function() { return this._cachedTexturesByteLength; } + }, + + /** + * The {@link ClippingPlaneCollection} used to selectively disable rendering the model. + * + * @memberof Model.prototype + * + * @type {ClippingPlaneCollection} + */ + clippingPlanes : { + get : function() { + return this._clippingPlanes; + }, + set : function(value) { + if (value === this._clippingPlanes) { + return; + } + // Handle destroying, checking of unknown, checking for existing ownership + ClippingPlaneCollection.setOwnership(value, this, '_clippingPlanes'); + } } }); @@ -1009,6 +1036,15 @@ define([ return context.stencilBuffer; } + function isColorShadingEnabled(model) { + return !Color.equals(model.color, Color.WHITE) || (model.colorBlendMode !== ColorBlendMode.HIGHLIGHT); + } + + function isClippingEnabled(model) { + var clippingPlanes = model._clippingPlanes; + return defined(clippingPlanes) && clippingPlanes.enabled; + } + /** * Determines if silhouettes are supported. * @@ -1063,7 +1099,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 {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the model. Clipping planes are not currently supported in Internet Explorer. + * @param {ClippingPlaneCollection} [options.clippingPlanes] The {@link ClippingPlaneCollection} used to selectively disable rendering the model. * * @returns {Model} The newly created model. * @@ -1885,39 +1921,99 @@ define([ /////////////////////////////////////////////////////////////////////////// + // When building programs for the first time, do not include modifiers for clipping planes and color + // since this is the version of the program that will be cached. function createProgram(id, model, context) { - var programs = model.gltf.programs; - var shaders = model.gltf.shaders; - var program = programs[id]; + var program = model._sourcePrograms[id]; + var shaders = model._sourceShaders; + var quantizedVertexShaders = model._quantizedVertexShaders; - var attributeLocations = createAttributeLocations(model, program.attributes); var vs = shaders[program.vertexShader].extras._pipeline.source; var fs = shaders[program.fragmentShader].extras._pipeline.source; - // Add pre-created attributes to attributeLocations - var attributesLength = program.attributes.length; - var precreatedAttributes = model._precreatedAttributes; - if (defined(precreatedAttributes)) { - for (var attrName in precreatedAttributes) { - if (precreatedAttributes.hasOwnProperty(attrName)) { - attributeLocations[attrName] = attributesLength++; - } + if (model.extensionsUsed.WEB3D_quantized_attributes) { + var quantizedVS = quantizedVertexShaders[id]; + if (!defined(quantizedVS)) { + quantizedVS = modifyShaderForQuantizedAttributes(vs, id, model); + quantizedVertexShaders[id] = quantizedVS; } + vs = quantizedVS; } + var drawVS = modifyShader(vs, id, model._vertexShaderLoaded); + var drawFS = modifyShader(fs, id, model._fragmentShaderLoaded); + + var pickFS, pickVS; + if (model.allowPicking) { + // PERFORMANCE_IDEA: Can optimize this shader with a glTF hint. https://github.com/KhronosGroup/glTF/issues/181 + pickVS = modifyShader(vs, id, model._pickVertexShaderLoaded); + pickFS = modifyShader(fs, id, model._pickFragmentShaderLoaded); + + if (!model._pickFragmentShaderLoaded) { + pickFS = ShaderSource.createPickFragmentShaderSource(fs, 'uniform'); + } + } + createAttributesAndProgram(id, drawFS, drawVS, pickFS, pickVS, model, context); + } + + function recreateProgram(id, model, context) { + var program = model._sourcePrograms[id]; + var shaders = model._sourceShaders; + var quantizedVertexShaders = model._quantizedVertexShaders; + + var clippingPlaneCollection = model.clippingPlanes; + var addClippingPlaneCode = isClippingEnabled(model); + + var vs = shaders[program.vertexShader].extras._pipeline.source; + var fs = shaders[program.fragmentShader].extras._pipeline.source; + if (model.extensionsUsed.WEB3D_quantized_attributes) { - vs = modifyShaderForQuantizedAttributes(vs, id, model); + vs = quantizedVertexShaders[id]; } - var premultipliedAlpha = hasPremultipliedAlpha(model); - var finalFS = modifyShaderForColor(fs, premultipliedAlpha); - if (ClippingPlaneCollection.isSupported()) { - finalFS = modifyShaderForClippingPlanes(finalFS); + var finalFS = fs; + if (isColorShadingEnabled(model)) { + finalFS = Model._modifyShaderForColor(finalFS, model._hasPremultipliedAlpha); + } + if (addClippingPlaneCode) { + finalFS = modifyShaderForClippingPlanes(finalFS, clippingPlaneCollection); } var drawVS = modifyShader(vs, id, model._vertexShaderLoaded); var drawFS = modifyShader(finalFS, id, model._fragmentShaderLoaded); + var pickFS, pickVS; + if (model.allowPicking) { + // PERFORMANCE_IDEA: Can optimize this shader with a glTF hint. https://github.com/KhronosGroup/glTF/issues/181 + pickVS = modifyShader(vs, id, model._pickVertexShaderLoaded); + pickFS = modifyShader(fs, id, model._pickFragmentShaderLoaded); + + if (!model._pickFragmentShaderLoaded) { + pickFS = ShaderSource.createPickFragmentShaderSource(fs, 'uniform'); + } + + if (addClippingPlaneCode) { + pickFS = modifyShaderForClippingPlanes(pickFS, clippingPlaneCollection); + } + } + createAttributesAndProgram(id, drawFS, drawVS, pickFS, pickVS, model, context); + } + + function createAttributesAndProgram(id, drawFS, drawVS, pickFS, pickVS, model, context) { + var program = model._sourcePrograms[id]; + var attributeLocations = createAttributeLocations(model, program.attributes); + + // Add pre-created attributes to attributeLocations + var attributesLength = program.attributes.length; + var precreatedAttributes = model._precreatedAttributes; + if (defined(precreatedAttributes)) { + for (var attrName in precreatedAttributes) { + if (precreatedAttributes.hasOwnProperty(attrName)) { + attributeLocations[attrName] = attributesLength++; + } + } + } + model._rendererResources.programs[id] = ShaderProgram.fromCache({ context : context, vertexShaderSource : drawVS, @@ -1926,14 +2022,6 @@ define([ }); if (model.allowPicking) { - // PERFORMANCE_IDEA: Can optimize this shader with a glTF hint. https://github.com/KhronosGroup/glTF/issues/181 - var pickVS = modifyShader(vs, id, model._pickVertexShaderLoaded); - var pickFS = modifyShader(fs, id, model._pickFragmentShaderLoaded); - - if (!model._pickFragmentShaderLoaded) { - pickFS = ShaderSource.createPickFragmentShaderSource(fs, 'uniform'); - } - model._rendererResources.pickPrograms[id] = ShaderProgram.fromCache({ context : context, vertexShaderSource : pickVS, @@ -2547,7 +2635,7 @@ define([ var polygonOffset = defaultValue(statesFunctions.polygonOffset, [0.0, 0.0]); // Change the render state to use traditional alpha blending instead of premultiplied alpha blending - if (booleanStates[WebGLConstants.BLEND] && hasPremultipliedAlpha(model)) { + if (booleanStates[WebGLConstants.BLEND] && model._hasPremultipliedAlpha) { if ((blendFuncSeparate[0] === WebGLConstants.ONE) && (blendFuncSeparate[1] === WebGLConstants.ONE_MINUS_SRC_ALPHA)) { blendFuncSeparate[0] = WebGLConstants.SRC_ALPHA; blendFuncSeparate[1] = WebGLConstants.ONE_MINUS_SRC_ALPHA; @@ -2832,33 +2920,21 @@ define([ }; } - function createClippingPlanesLengthFunction(model) { - return function() { - return model._packedClippingPlanes.length; - }; - } - - function createClippingPlanesUnionRegionsFunction(model) { + var scratchClippingPlaneMatrix = new Matrix4(); + function createClippingPlanesMatrixFunction(model) { return function() { var clippingPlanes = model.clippingPlanes; if (!defined(clippingPlanes)) { - return false; + return Matrix4.IDENTITY; } - - return clippingPlanes.unionClippingRegions; + return Matrix4.multiply(model._modelViewMatrix, clippingPlanes.modelMatrix, scratchClippingPlaneMatrix); }; } function createClippingPlanesFunction(model) { return function() { var clippingPlanes = model.clippingPlanes; - var packedPlanes = model._packedClippingPlanes; - - if (defined(clippingPlanes) && clippingPlanes.enabled) { - clippingPlanes.transformAndPackPlanes(model._modelViewMatrix, packedPlanes); - } - - return packedPlanes; + return (!defined(clippingPlanes) || !clippingPlanes.enabled) ? model._defaultTexture : clippingPlanes.texture; }; } @@ -2977,10 +3053,9 @@ define([ uniformMap = combine(uniformMap, { gltf_color : createColorFunction(model), gltf_colorBlend : createColorBlendFunction(model), - gltf_clippingPlanesLength: createClippingPlanesLengthFunction(model), - gltf_clippingPlanesUnionRegions: createClippingPlanesUnionRegionsFunction(model), - gltf_clippingPlanes: createClippingPlanesFunction(model, context), - gltf_clippingPlanesEdgeStyle: createClippingPlanesEdgeStyleFunction(model) + gltf_clippingPlanes: createClippingPlanesFunction(model), + gltf_clippingPlanesEdgeStyle: createClippingPlanesEdgeStyleFunction(model), + gltf_clippingPlanesMatrix: createClippingPlanesMatrixFunction(model) }); // Allow callback to modify the uniformMap @@ -3020,7 +3095,7 @@ define([ vertexArray : vertexArray, count : count, offset : offset, - shaderProgram : rendererPrograms[technique.program], + shaderProgram : rendererPrograms[programId], castShadows : castShadows, receiveShadows : receiveShadows, uniformMap : uniformMap, @@ -3061,7 +3136,7 @@ define([ vertexArray : vertexArray, count : count, offset : offset, - shaderProgram : rendererPickPrograms[technique.program], + shaderProgram : rendererPickPrograms[programId], uniformMap : pickUniformMap, renderState : rs, owner : owner, @@ -3097,12 +3172,13 @@ define([ silhouetteColorCommand2D : undefined, // Generated on demand when color alpha is less than 1.0 translucentCommand : undefined, - translucentCommand2D : undefined + translucentCommand2D : undefined, + // For updating node commands on shader reconstruction + programId : programId }; runtimeNode.commands.push(nodeCommand); nodeCommands.push(nodeCommand); } - } function createRuntimeNodes(model, context, scene3DOnly) { @@ -3231,6 +3307,11 @@ define([ var context = frameState.context; var scene3DOnly = frameState.scene3DOnly; + // Retain references to updated source shaders and programs for rebuilding as needed + model._sourcePrograms = model.gltf.programs; + model._sourceShaders = model.gltf.shaders; + model._hasPremultipliedAlpha = hasPremultipliedAlpha(model); + ModelUtility.checkSupportedGlExtensions(model.gltf.glExtensionsUsed, context); if (model._loadRendererResourcesFromCache) { var resources = model._rendererResources; @@ -3554,14 +3635,14 @@ define([ return translucentCommand; } - function updateColor(model, frameState) { + function updateColor(model, frameState, forceDerive) { // Generate translucent commands when the blend color has an alpha in the range (0.0, 1.0) exclusive var scene3DOnly = frameState.scene3DOnly; var alpha = model.color.alpha; if ((alpha > 0.0) && (alpha < 1.0)) { var nodeCommands = model._nodeCommands; var length = nodeCommands.length; - if (!defined(nodeCommands[0].translucentCommand)) { + if (!defined(nodeCommands[0].translucentCommand) || forceDerive) { for (var i = 0; i < length; ++i) { var nodeCommand = nodeCommands[i]; var command = nodeCommand.command; @@ -3774,42 +3855,29 @@ define([ } } - function modifyShaderForClippingPlanes(shader) { + function modifyShaderForClippingPlanes(shader, clippingPlaneCollection) { shader = ShaderSource.replaceMain(shader, 'gltf_clip_main'); + shader += Model._getClippingFunction(clippingPlaneCollection) + '\n'; shader += - 'uniform int gltf_clippingPlanesLength; \n' + - 'uniform bool gltf_clippingPlanesUnionRegions; \n' + - 'uniform vec4 gltf_clippingPlanes[czm_maxClippingPlanes]; \n' + + 'uniform sampler2D gltf_clippingPlanes; \n' + 'uniform vec4 gltf_clippingPlanesEdgeStyle; \n' + + 'uniform mat4 gltf_clippingPlanesMatrix; \n' + 'void main() \n' + '{ \n' + ' gltf_clip_main(); \n' + - ' if (gltf_clippingPlanesLength > 0) \n' + + ' float clipDistance = clip(gl_FragCoord, gltf_clippingPlanes, gltf_clippingPlanesMatrix);' + + ' 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' + - ' float clipDistance; \n' + - ' if (gltf_clippingPlanesUnionRegions) \n' + - ' { \n' + - ' clipDistance = czm_discardIfClippedWithUnion(gltf_clippingPlanes, gltf_clippingPlanesLength); \n' + - ' } \n' + - ' else \n' + - ' { \n' + - ' clipDistance = czm_discardIfClippedWithIntersect(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' + + ' gl_FragColor = clippingPlanesEdgeColor;\n' + ' } \n' + '} \n'; - return shader; } - function updateSilhouette(model, frameState) { + function updateSilhouette(model, frameState, force) { // Generate silhouette commands when the silhouette size is greater than 0.0 and the alpha is greater than 0.0 // There are two silhouette commands: // 1. silhouetteModelCommand : render model normally while enabling stencil mask @@ -3826,25 +3894,16 @@ define([ model._colorPreviousAlpha = model.color.alpha; model._silhouetteColorPreviousAlpha = model.silhouetteColor.alpha; - if (dirty) { + if (dirty || force) { createSilhouetteCommands(model, frameState); } } - function updateClippingPlanes(model) { - var clippingPlanes = model.clippingPlanes; - var length = 0; - if (defined(clippingPlanes) && clippingPlanes.enabled) { - length = clippingPlanes.length; - } - - var packedPlanes = model._packedClippingPlanes; - var packedLength = packedPlanes.length; - if (packedLength !== length) { - packedPlanes.length = length; - - for (var i = packedLength; i < length; ++i) { - packedPlanes[i] = new Cartesian4(); + function updateClippingPlanes(model, frameState) { + var clippingPlanes = model._clippingPlanes; + if (defined(clippingPlanes) && clippingPlanes.owner === model) { + if (clippingPlanes.enabled) { + clippingPlanes.update(frameState); } } } @@ -4076,7 +4135,7 @@ define([ // Use renderer resources from cache instead of loading/creating them? var cachedRendererResources; var cacheKey = this.cacheKey; - if (defined(cacheKey)) { + if (defined(cacheKey)) { // cache key given? this model will pull from or contribute to context level cache context.cache.modelRendererResourceCache = defaultValue(context.cache.modelRendererResourceCache, {}); var modelCaches = context.cache.modelRendererResourceCache; @@ -4096,7 +4155,7 @@ define([ modelCaches[this.cacheKey] = cachedRendererResources; } this._cachedRendererResources = cachedRendererResources; - } else { + } else { // cache key not given? this model doesn't care about context level cache at all. Cache is here to simplify freeing on destroy. cachedRendererResources = new CachedRendererResources(context); cachedRendererResources.count = 1; this._cachedRendererResources = cachedRendererResources; @@ -4280,11 +4339,6 @@ define([ } } - var clippingPlanes = this.clippingPlanes; - if (defined(clippingPlanes) && clippingPlanes.enabled) { - Matrix4.multiply(context.uniformState.view3D, modelMatrix, this._modelViewMatrix); - } - // Update modelMatrix throughout the graph as needed if (animated || modelTransformChanged || justLoaded) { updateNodeHierarchyModelMatrix(this, modelTransformChanged, justLoaded, frameState.mapProjection); @@ -4304,9 +4358,32 @@ define([ updateWireframe(this); updateShowBoundingVolume(this); updateShadows(this); - updateColor(this, frameState); - updateSilhouette(this, frameState); updateClippingPlanes(this, frameState); + + // Regnerate shaders if ClippingPlaneCollection state changed or it was removed + var clippingPlanes = this._clippingPlanes; + var currentClippingPlanesState = 0; + if (defined(clippingPlanes) && clippingPlanes.enabled) { + Matrix4.multiply(context.uniformState.view3D, modelMatrix, this._modelViewMatrix); + currentClippingPlanesState = clippingPlanes.clippingPlanesState(); + } + + var shouldRegenerateShaders = this._clippingPlanesState !== currentClippingPlanesState; + this._clippingPlanesState = currentClippingPlanesState; + + // Regenerate shaders if color shading changed from last update + var currentlyColorShadingEnabled = isColorShadingEnabled(this); + if (currentlyColorShadingEnabled !== this._colorShadingEnabled) { + this._colorShadingEnabled = currentlyColorShadingEnabled; + shouldRegenerateShaders = true; + } + + if (shouldRegenerateShaders) { + regenerateShaders(this, frameState); + } else { + updateColor(this, frameState, false); + updateSilhouette(this, frameState, false); + } } if (justLoaded) { @@ -4385,6 +4462,83 @@ define([ } }; + function destroyIfNotCached(rendererResources, cachedRendererResources) { + if (rendererResources.programs !== cachedRendererResources.programs) { + destroy(rendererResources.programs); + } + if (rendererResources.pickPrograms !== cachedRendererResources.pickPrograms) { + destroy(rendererResources.pickPrograms); + } + if (rendererResources.silhouettePrograms !== cachedRendererResources.silhouettePrograms) { + destroy(rendererResources.silhouettePrograms); + } + } + + // Run from update iff: + // - everything is loaded + // - clipping planes state change OR color state set + // Run this from destructor after removing color state and clipping plane state + function regenerateShaders(model, frameState) { + // In regards to _cachedRendererResources: + // Fair to assume that this is data that should just never get modified due to clipping planes or model color. + // So if clipping planes or model color active: + // - delink _rendererResources.*programs and create new dictionaries. + // - do NOT destroy any programs - might be used by copies of the model or by might be needed in the future if clipping planes/model color is deactivated + + // If clipping planes and model color inactive: + // - destroy _rendererResources.*programs + // - relink _rendererResources.*programs to _cachedRendererResources + + // In both cases, need to mark commands as dirty, re-run derived commands (elsewhere) + + var rendererResources = model._rendererResources; + var cachedRendererResources = model._cachedRendererResources; + destroyIfNotCached(rendererResources, cachedRendererResources); + + if (isClippingEnabled(model) || isColorShadingEnabled(model)) { + rendererResources.programs = {}; + rendererResources.pickPrograms = {}; + rendererResources.silhouettePrograms = {}; + + var sourcePrograms = model._sourcePrograms; + + ForEach.object(sourcePrograms, function(program, id) { + recreateProgram(id, model, frameState.context); + }); + } else { + rendererResources.programs = cachedRendererResources.programs; + rendererResources.pickPrograms = cachedRendererResources.pickPrograms; + rendererResources.silhouettePrograms = cachedRendererResources.silhouettePrograms; + } + + // Fix all the commands, marking them as dirty so everything that derives will re-derive + var rendererPrograms = rendererResources.programs; + var rendererPickPrograms = rendererResources.pickPrograms; + + var nodeCommands = model._nodeCommands; + var commandCount = nodeCommands.length; + for (var i = 0; i < commandCount; ++i) { + var nodeCommand = nodeCommands[i]; + var programId = nodeCommand.programId; + + var renderProgram = rendererPrograms[programId]; + var pickProgram = rendererPickPrograms[programId]; + + nodeCommand.command.shaderProgram = renderProgram; + nodeCommand.pickCommand.shaderProgram = pickProgram; + if (defined(nodeCommand.command2D)) { + nodeCommand.command2D.shaderProgram = renderProgram; + } + if (defined(nodeCommand.pickCommand2D)) { + nodeCommand.pickCommand2D.shaderProgram = pickProgram; + } + } + + // Force update silhouette commands/shaders + updateColor(model, frameState, true); + updateSilhouette(model, frameState, true); + } + /** * Returns true if this object was destroyed; otherwise, false. *

@@ -4433,6 +4587,11 @@ define([ this._terrainProviderChangedCallback = undefined; } + // Shaders modified for clipping and for color don't get cached, so destroy these manually + if (defined(this._cachedRendererResources)) { + destroyIfNotCached(this._rendererResources, this._cachedRendererResources); + } + this._rendererResources = undefined; this._cachedRendererResources = this._cachedRendererResources && this._cachedRendererResources.release(); @@ -4443,9 +4602,24 @@ define([ } releaseCachedGltf(this); + this._sourcePrograms = undefined; + this._sourceShaders = undefined; + this._quantizedVertexShaders = undefined; + + // Only destroy the ClippingPlaneCollection if this is the owner - if this model is part of a Cesium3DTileset, + // _clippingPlanes references a ClippingPlaneCollection that this model does not own. + var clippingPlaneCollection = this._clippingPlanes; + if (defined(clippingPlaneCollection) && !clippingPlaneCollection.isDestroyed() && clippingPlaneCollection.owner === this) { + clippingPlaneCollection.destroy(); + } + this._clippingPlanes = undefined; return destroyObject(this); }; + // exposed for testing + Model._getClippingFunction = getClippingFunction; + Model._modifyShaderForColor = modifyShaderForColor; + return Model; }); diff --git a/Source/Scene/ModelInstanceCollection.js b/Source/Scene/ModelInstanceCollection.js index 061abb675f32..e771dfae5857 100644 --- a/Source/Scene/ModelInstanceCollection.js +++ b/Source/Scene/ModelInstanceCollection.js @@ -871,6 +871,32 @@ define([ }; } + function commandsDirty(model) { + var nodeCommands = model._nodeCommands; + var length = nodeCommands.length; + + for (var i = 0; i < length; i++) { + var nc = nodeCommands[i]; + if (nc.command.dirty) { + return true; + } + } + return false; + } + + function generateModelCommands(modelInstanceCollection, instancingSupported) { + modelInstanceCollection._drawCommands = []; + modelInstanceCollection._pickCommands = []; + + var modelCommands = getModelCommands(modelInstanceCollection._model); + if (instancingSupported) { + createCommands(modelInstanceCollection, modelCommands.draw, modelCommands.pick); + } else { + createCommandsNonInstanced(modelInstanceCollection, modelCommands.draw, modelCommands.pick); + updateCommandsNonInstanced(modelInstanceCollection); + } + } + function updateShadows(collection) { if (collection.shadows !== collection._shadows) { collection._shadows = collection.shadows; @@ -916,6 +942,7 @@ define([ var instancingSupported = this._instancingSupported; var model = this._model; + model.update(frameState); if (model.ready && (this._state === LoadState.LOADING)) { @@ -929,12 +956,7 @@ define([ var modelCommands = getModelCommands(model); this._modelCommands = modelCommands.draw; - if (instancingSupported) { - createCommands(this, modelCommands.draw, modelCommands.pick); - } else { - createCommandsNonInstanced(this, modelCommands.draw, modelCommands.pick); - updateCommandsNonInstanced(this); - } + generateModelCommands(this, instancingSupported); this._readyPromise.resolve(this); return; @@ -967,6 +989,11 @@ define([ updateVertexBuffer(this); } + // If the model was set to rebuild shaders during update, rebuild instanced commands. + if (commandsDirty(model)) { + generateModelCommands(this, instancingSupported); + } + // If any node changes due to an animation, update the commands. This could be inefficient if the model is // composed of many nodes and only one changes, however it is probably fine in the general use case. // Only applies when instancing is disabled. The instanced shader automatically handles node transformations. diff --git a/Source/Scene/PointCloud3DTileContent.js b/Source/Scene/PointCloud3DTileContent.js index 4941ee3240af..d50eb6507733 100644 --- a/Source/Scene/PointCloud3DTileContent.js +++ b/Source/Scene/PointCloud3DTileContent.js @@ -35,6 +35,7 @@ define([ './Cesium3DTileBatchTable', './Cesium3DTileFeature', './Cesium3DTileFeatureTable', + './getClippingFunction', './SceneMode', './ShadowMode' ], function( @@ -74,6 +75,7 @@ define([ Cesium3DTileBatchTable, Cesium3DTileFeature, Cesium3DTileFeatureTable, + getClippingFunction, SceneMode, ShadowMode) { 'use strict'; @@ -123,10 +125,6 @@ define([ this._hasNormals = false; this._hasBatchIds = false; - // Used to regenerate shader when clipping on this tile changes - this._isClipped = false; - this._unionClippingRegions = false; - // Use per-point normals to hide back-facing points. this.backFaceCulling = false; this._backFaceCulling = false; @@ -148,7 +146,6 @@ define([ this._features = undefined; - this._packedClippingPlanes = []; this._modelViewMatrix = Matrix4.clone(Matrix4.IDENTITY); /** @@ -479,6 +476,7 @@ define([ var batchIdLocation = 3; var numberOfAttributes = 4; + var scratchClippingPlaneMatrix = new Matrix4(); function createResources(content, frameState) { var context = frameState.context; var parsedContent = content._parsedContent; @@ -541,7 +539,6 @@ define([ } } } - var uniformMap = { u_pointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier : function() { var scratch = scratchPointSizeAndTilesetTimeAndGeometricErrorAndDepthMultiplier; @@ -574,15 +571,12 @@ define([ u_constantColor : function() { return content._constantColor; }, - u_clippingPlanesLength : function() { - return content._packedClippingPlanes.length; - }, u_clippingPlanes : function() { - return content._packedClippingPlanes; + var clippingPlanes = content._tileset.clippingPlanes; + return (!defined(clippingPlanes) || !clippingPlanes.enabled) ? context.defaultTexture : clippingPlanes.texture; }, u_clippingPlanesEdgeStyle : function() { var clippingPlanes = content._tileset.clippingPlanes; - if (!defined(clippingPlanes)) { return Color.WHITE.withAlpha(0.0); } @@ -590,6 +584,13 @@ define([ var style = Color.clone(clippingPlanes.edgeColor); style.alpha = clippingPlanes.edgeWidth; return style; + }, + u_clippingPlanesMatrix : function() { + var clippingPlanes = content._tileset.clippingPlanes; + if (!defined(clippingPlanes)) { + return Matrix4.IDENTITY; + } + return Matrix4.multiply(content._modelViewMatrix, clippingPlanes.modelMatrix, scratchClippingPlaneMatrix); } }; @@ -886,7 +887,7 @@ define([ var hasColorStyle = defined(colorStyleFunction); var hasShowStyle = defined(showStyleFunction); var hasPointSizeStyle = defined(pointSizeStyleFunction); - var hasClippedContent = defined(clippingPlanes) && clippingPlanes.enabled && content._tile._isClipped && ClippingPlaneCollection.isSupported(); + var hasClippedContent = defined(clippingPlanes) && clippingPlanes.enabled && content._tile._isClipped; // Get the properties in use by the style var styleableProperties = []; @@ -1119,9 +1120,12 @@ define([ var fs = 'varying vec4 v_color; \n'; if (hasClippedContent) { - fs += 'uniform int u_clippingPlanesLength;' + - 'uniform vec4 u_clippingPlanes[czm_maxClippingPlanes]; \n' + + fs += 'uniform sampler2D u_clippingPlanes; \n' + + 'uniform mat4 u_clippingPlanesMatrix; \n' + 'uniform vec4 u_clippingPlanesEdgeStyle; \n'; + fs += '\n'; + fs += getClippingFunction(clippingPlanes); + fs += '\n'; } fs += 'void main() \n' + @@ -1129,8 +1133,7 @@ define([ ' gl_FragColor = v_color; \n'; if (hasClippedContent) { - var clippingFunction = clippingPlanes.unionClippingRegions ? 'czm_discardIfClippedWithUnion' : 'czm_discardIfClippedWithIntersect'; - fs += ' float clipDistance = ' + clippingFunction + '(u_clippingPlanes, u_clippingPlanesLength); \n' + + fs += ' float clipDistance = clip(gl_FragCoord, u_clippingPlanes, u_clippingPlanesMatrix);\n' + ' vec4 clippingPlanesEdgeColor = vec4(1.0); \n' + ' clippingPlanesEdgeColor.rgb = u_clippingPlanesEdgeStyle.rgb; \n' + ' float clippingPlanesEdgeWidth = u_clippingPlanesEdgeStyle.a; \n' + @@ -1274,27 +1277,6 @@ define([ var context = frameState.context; - // update clipping planes - var clippingPlanes = this._tileset.clippingPlanes; - var clippingEnabled = defined(clippingPlanes) && clippingPlanes.enabled && this._tile._isClipped; - - var unionClippingRegions = false; - var length = 0; - if (clippingEnabled) { - unionClippingRegions = clippingPlanes.unionClippingRegions; - length = clippingPlanes.length; - } - - var packedPlanes = this._packedClippingPlanes; - var packedLength = packedPlanes.length; - if (packedLength !== length) { - packedPlanes.length = length; - - for (var i = 0; i < length; ++i) { - packedPlanes[i] = new Cartesian4(); - } - } - if (!defined(this._drawCommand)) { createResources(this, frameState); createShaders(this, frameState, tileset.style); @@ -1304,13 +1286,18 @@ define([ this._parsedContent = undefined; // Unload } - if (this._isClipped !== clippingEnabled || this._unionClippingRegions !== unionClippingRegions) { - this._isClipped = clippingEnabled; - this._unionClippingRegions = unionClippingRegions; - + // update for clipping planes + if (this._tile.clippingPlanesDirty) { createShaders(this, frameState, tileset.style); } + var clippingPlanes = this._tileset.clippingPlanes; + var clippingEnabled = defined(clippingPlanes) && clippingPlanes.enabled && this._tile._isClipped; + + if (clippingEnabled) { + Matrix4.multiply(context.uniformState.view3D, modelMatrix, this._modelViewMatrix); + } + // Update attenuation var pointCloudShading = this._tileset.pointCloudShading; if (defined(pointCloudShading)) { @@ -1360,11 +1347,6 @@ define([ this._pickCommand.boundingVolume = boundingVolume; } - if (clippingEnabled) { - Matrix4.multiply(context.uniformState.view3D, modelMatrix, this._modelViewMatrix); - clippingPlanes.transformAndPackPlanes(this._modelViewMatrix, packedPlanes); - } - this._drawCommand.castShadows = ShadowMode.castShadows(tileset.shadows); this._drawCommand.receiveShadows = ShadowMode.receiveShadows(tileset.shadows); diff --git a/Source/Scene/getClippingFunction.js b/Source/Scene/getClippingFunction.js new file mode 100644 index 000000000000..495700e3eb87 --- /dev/null +++ b/Source/Scene/getClippingFunction.js @@ -0,0 +1,168 @@ +define([ + '../Core/Check', + '../Renderer/PixelDatatype', + './getUnpackFloatFunction' + ], function( + Check, + PixelDatatype, + getUnpackFloatFunction) { + 'use strict'; + + /** + * Gets the glsl functions needed to retrieve clipping planes from a ClippingPlaneCollection's texture. + * + * @param {ClippingPlaneCollection} clippingPlaneCollection ClippingPlaneCollection with a defined texture. + * @returns {String} A string containing glsl functions for retrieving clipping planes. + * @private + */ + function getClippingFunction(clippingPlaneCollection) { + //>>includeStart('debug', pragmas.debug); + Check.typeOf.object('clippingPlaneCollection', clippingPlaneCollection); + //>>includeEnd('debug'); + var unionClippingRegions = clippingPlaneCollection.unionClippingRegions; + var clippingPlanesLength = clippingPlaneCollection.length; + var texture = clippingPlaneCollection.texture; + var usingFloatTexture = texture.pixelDatatype === PixelDatatype.FLOAT; + var width = texture.width; + var height = texture.height; + + var functions = usingFloatTexture ? getClippingPlaneFloat(width, height) : getClippingPlaneUint8(width, height); + functions += '\n'; + functions += unionClippingRegions ? clippingFunctionUnion(usingFloatTexture, clippingPlanesLength) : clippingFunctionIntersect(usingFloatTexture, clippingPlanesLength); + return functions; + } + + function clippingFunctionUnion(usingFloatTexture, clippingPlanesLength) { + var functionString = + 'float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix)\n' + + '{\n' + + ' vec4 position = czm_windowToEyeCoordinates(fragCoord);\n' + + ' vec3 clipNormal = vec3(0.0);\n' + + ' vec3 clipPosition = vec3(0.0);\n' + + ' float clipAmount = 0.0;\n' + + ' float pixelWidth = czm_metersPerPixel(position);\n' + + ' bool breakAndDiscard = false;\n' + + + ' for (int i = 0; i < ' + clippingPlanesLength + '; ++i)\n' + + ' {\n' + + ' vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix);\n' + + + ' clipNormal = clippingPlane.xyz;\n' + + ' clipPosition = -clippingPlane.w * clipNormal;\n' + + + ' float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth;\n' + + ' clipAmount = max(amount, clipAmount);\n' + + + ' if (amount <= 0.0)\n' + + ' {\n' + + ' breakAndDiscard = true;\n' + + ' break;\n' + // HLSL compiler bug if we discard here: https://bugs.chromium.org/p/angleproject/issues/detail?id=1945#c6 + ' }\n' + + ' }\n' + + + ' if (breakAndDiscard) {\n' + + ' discard;\n' + + ' }\n' + + ' return clipAmount;\n' + + '}\n'; + return functionString; + } + + function clippingFunctionIntersect(usingFloatTexture, clippingPlanesLength) { + var functionString = + 'float clip(vec4 fragCoord, sampler2D clippingPlanes, mat4 clippingPlanesMatrix)\n' + + '{\n' + + ' bool clipped = true;\n' + + ' vec4 position = czm_windowToEyeCoordinates(fragCoord);\n' + + ' vec3 clipNormal = vec3(0.0);\n' + + ' vec3 clipPosition = vec3(0.0);\n' + + ' float clipAmount = 0.0;\n' + + ' float pixelWidth = czm_metersPerPixel(position);\n' + + + ' for (int i = 0; i < ' + clippingPlanesLength + '; ++i)\n' + + ' {\n' + + ' vec4 clippingPlane = getClippingPlane(clippingPlanes, i, clippingPlanesMatrix);\n' + + + ' clipNormal = clippingPlane.xyz;\n' + + ' clipPosition = -clippingPlane.w * clipNormal;\n' + + + ' float amount = dot(clipNormal, (position.xyz - clipPosition)) / pixelWidth;\n' + + ' clipAmount = max(amount, clipAmount);\n' + + + ' clipped = clipped && (amount <= 0.0);\n' + + ' }\n' + + + ' if (clipped)\n' + + ' {\n' + + ' discard;\n' + + ' }\n' + + + ' return clipAmount;\n' + + '}\n'; + return functionString; + } + + function getClippingPlaneFloat(width, height) { + var pixelWidth = 1.0 / width; + var pixelHeight = 1.0 / height; + + var pixelWidthString = pixelWidth + ''; + if (pixelWidthString.indexOf('.') === -1) { + pixelWidthString += '.0'; + } + var pixelHeightString = pixelHeight + ''; + if (pixelHeightString.indexOf('.' === -1)) { + pixelHeightString += '.0'; + } + + var functionString = + 'vec4 getClippingPlane(sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)\n' + + '{\n' + + ' int pixY = clippingPlaneNumber / ' + width + ';\n' + + ' int pixX = clippingPlaneNumber - (pixY * ' + width + ');\n' + + ' float u = (float(pixX) + 0.5) * ' + pixelWidthString + ';\n' + // sample from center of pixel + ' float v = (float(pixY) + 0.5) * ' + pixelHeightString + ';\n' + + ' vec4 plane = texture2D(packedClippingPlanes, vec2(u, v));\n' + + ' return czm_transformPlane(transform, plane);\n' + + '}\n'; + return functionString; + } + + function getClippingPlaneUint8(width, height) { + var pixelWidth = 1.0 / width; + var pixelHeight = 1.0 / height; + + var pixelWidthString = pixelWidth + ''; + if (pixelWidthString.indexOf('.') === -1) { + pixelWidthString += '.0'; + } + var pixelHeightString = pixelHeight + ''; + if (pixelHeightString.indexOf('.' === -1)) { + pixelHeightString += '.0'; + } + + var functionString = + getUnpackFloatFunction('unpackFloatDistance') + + '\n' + + 'vec4 getClippingPlane(sampler2D packedClippingPlanes, int clippingPlaneNumber, mat4 transform)\n' + + '{\n' + + ' int clippingPlaneStartIndex = clippingPlaneNumber * 2;\n' + // clipping planes are two pixels each + ' int pixY = clippingPlaneStartIndex / ' + width + ';\n' + + ' int pixX = clippingPlaneStartIndex - (pixY * ' + width + ');\n' + + ' float u = (float(pixX) + 0.5) * ' + pixelWidthString + ';\n' + // sample from center of pixel + ' float v = (float(pixY) + 0.5) * ' + pixelHeightString + ';\n' + + + ' vec4 oct32 = texture2D(packedClippingPlanes, vec2(u, v)) * 255.0;\n' + + ' vec2 oct = vec2(oct32.x * 256.0 + oct32.y, oct32.z * 256.0 + oct32.w);\n' + + + ' vec4 plane;\n' + + ' plane.xyz = czm_octDecode(oct, 65535.0);\n' + + ' plane.w = unpackFloatDistance(texture2D(packedClippingPlanes, vec2(u + ' + pixelWidthString + ', v)));\n' + + + ' return czm_transformPlane(transform, plane);\n' + + '}\n'; + return functionString; + } + + return getClippingFunction; +}); diff --git a/Source/Scene/getUnpackFloatFunction.js b/Source/Scene/getUnpackFloatFunction.js new file mode 100644 index 000000000000..dae8fb286cab --- /dev/null +++ b/Source/Scene/getUnpackFloatFunction.js @@ -0,0 +1,42 @@ +define([ + '../Core/defined' + ], function( + defined) { + 'use strict'; + + var SHIFT_RIGHT_8 = 1.0 / 256.0; + var SHIFT_RIGHT_16 = 1.0 / 65536.0; + var SHIFT_RIGHT_24 = 1.0 / 16777216.0; + + var BIAS = 38.0; + + /** + * Gets a glsl function that takes a vec4 representing a float packed to Uint8 RGBA and returns the unpacked float. + * + * @param {String} [functionName='unpackFloat'] An optional name for the glsl function. + * @returns {String} A string containing a glsl function that takes a vec4 and returns a float. + * @private + * @see Cartesian4#packFloat + */ + function getUnpackFloatFunction(functionName) { + if (!defined(functionName)) { + functionName = 'unpackFloat'; + } + return 'float ' + functionName + '(vec4 value) \n' + + '{ \n' + + ' value *= 255.0; \n' + + ' float temp = value.w / 2.0; \n' + + ' float exponent = floor(temp); \n' + + ' float sign = (temp - exponent) * 2.0; \n' + + ' exponent = exponent - float(' + BIAS + '); \n' + + ' sign = sign * 2.0 - 1.0; \n' + + ' sign = -sign; \n' + + ' float unpacked = sign * value.x * float(' + SHIFT_RIGHT_8 + '); \n' + + ' unpacked += sign * value.y * float(' + SHIFT_RIGHT_16 + '); \n' + + ' unpacked += sign * value.z * float(' + SHIFT_RIGHT_24 + '); \n' + + ' return unpacked * pow(10.0, exponent); \n' + + '} \n'; + } + + return getUnpackFloatFunction; +}); diff --git a/Source/Shaders/Builtin/Constants/maxClippingPlanes.glsl b/Source/Shaders/Builtin/Constants/maxClippingPlanes.glsl deleted file mode 100644 index 30c377df1668..000000000000 --- a/Source/Shaders/Builtin/Constants/maxClippingPlanes.glsl +++ /dev/null @@ -1,8 +0,0 @@ -/** - * The maximum amount of planes that can be clipped by czm_clipPlanes. - * - * @name czm_maxClippingPlanes - * @glslConstant - * @see czm_clipPlanes - */ -const int czm_maxClippingPlanes = 6; diff --git a/Source/Shaders/Builtin/Functions/discardIfClippedWithIntersect.glsl b/Source/Shaders/Builtin/Functions/discardIfClippedWithIntersect.glsl deleted file mode 100644 index 987dd8d354c8..000000000000 --- a/Source/Shaders/Builtin/Functions/discardIfClippedWithIntersect.glsl +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Clip a fragment by an array of clipping planes. Clipping plane regions are joined by the intersect operation, so - * a fragment must be clipped by all of the planes to be discarded. - * - * @name czm_discardIfClippedWithIntersect - * @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_discardIfClippedWithIntersect(vec4 clippingPlanes[czm_maxClippingPlanes], 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/Builtin/Functions/discardIfClippedWithUnion.glsl b/Source/Shaders/Builtin/Functions/discardIfClippedWithUnion.glsl deleted file mode 100644 index ae6110f8346f..000000000000 --- a/Source/Shaders/Builtin/Functions/discardIfClippedWithUnion.glsl +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Clip a fragment by an array of clipping planes. Clipping plane regions are joined with the union operation, - * therefore if a fragment is clipped by any of the planes, it is discarded. - * - * @name czm_discardIfClippedWithUnion - * @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_discardIfClippedWithUnion(vec4 clippingPlanes[czm_maxClippingPlanes], int clippingPlanesLength) -{ - if (clippingPlanesLength > 0) - { - 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); - - if (amount <= 0.0) - { - discard; - } - } - - return clipAmount; - } - - return 0.0; -} diff --git a/Source/Shaders/Builtin/Functions/metersPerPixel.glsl b/Source/Shaders/Builtin/Functions/metersPerPixel.glsl index 953008963e28..f6066d21e0b5 100644 --- a/Source/Shaders/Builtin/Functions/metersPerPixel.glsl +++ b/Source/Shaders/Builtin/Functions/metersPerPixel.glsl @@ -1,6 +1,6 @@ /** * Computes the size of a pixel in meters at a distance from the eye. - + * @name czm_metersPerPixel * @glslFunction * @@ -14,13 +14,13 @@ float czm_metersPerPixel(vec4 positionEC) float height = czm_viewport.w; float pixelWidth; float pixelHeight; - + float top = czm_frustumPlanes.x; float bottom = czm_frustumPlanes.y; float left = czm_frustumPlanes.z; float right = czm_frustumPlanes.w; - - if (czm_sceneMode == czm_sceneMode2D) + + if (czm_sceneMode == czm_sceneMode2D || czm_orthographicIn3D == 1.0) { float frustumWidth = right - left; float frustumHeight = top - bottom; diff --git a/Source/Shaders/Builtin/Functions/octDecode.glsl b/Source/Shaders/Builtin/Functions/octDecode.glsl index ce4df362da12..98b2736f706a 100644 --- a/Source/Shaders/Builtin/Functions/octDecode.glsl +++ b/Source/Shaders/Builtin/Functions/octDecode.glsl @@ -28,7 +28,7 @@ * Decodes a unit-length vector in 'oct' encoding to a normalized 3-component Cartesian vector. * The 'oct' encoding is described in "A Survey of Efficient Representations of Independent Unit Vectors", * Cigolle et al 2014: http://jcgt.org/published/0003/02/01/ - * + * * @name czm_octDecode * @param {vec2} encoded The oct-encoded, unit-length vector * @returns {vec3} The decoded and normalized vector @@ -42,7 +42,7 @@ * Decodes a unit-length vector in 'oct' encoding packed into a floating-point number to a normalized 3-component Cartesian vector. * The 'oct' encoding is described in "A Survey of Efficient Representations of Independent Unit Vectors", * Cigolle et al 2014: http://jcgt.org/published/0003/02/01/ - * + * * @name czm_octDecode * @param {float} encoded The oct-encoded, unit-length vector * @returns {vec3} The decoded and normalized vector @@ -54,12 +54,12 @@ float y = (temp - x) * 256.0; return czm_octDecode(vec2(x, y)); } - + /** * Decodes three unit-length vectors in 'oct' encoding packed into two floating-point numbers to normalized 3-component Cartesian vectors. * The 'oct' encoding is described in "A Survey of Efficient Representations of Independent Unit Vectors", * Cigolle et al 2014: http://jcgt.org/published/0003/02/01/ - * + * * @name czm_octDecode * @param {vec2} encoded The packed oct-encoded, unit-length vectors. * @param {vec3} vector1 One decoded and normalized vector. @@ -80,4 +80,4 @@ vector2 = czm_octDecode(encodedFloat2); vector3 = czm_octDecode(vec2(x, y)); } - + diff --git a/Source/Shaders/Builtin/Functions/transformPlane.glsl b/Source/Shaders/Builtin/Functions/transformPlane.glsl new file mode 100644 index 000000000000..3f0412c891cb --- /dev/null +++ b/Source/Shaders/Builtin/Functions/transformPlane.glsl @@ -0,0 +1,8 @@ +vec4 czm_transformPlane(mat4 transform, vec4 clippingPlane) { + vec3 transformedDirection = normalize((transform * vec4(clippingPlane.xyz, 0.0)).xyz); + vec3 transformedPosition = (transform * vec4(clippingPlane.xyz * -clippingPlane.w, 1.0)).xyz; + vec4 transformedPlane; + transformedPlane.xyz = transformedDirection; + transformedPlane.w = -dot(transformedDirection, transformedPosition); + return transformedPlane; +} diff --git a/Source/Shaders/Builtin/Functions/windowToEyeCoordinates.glsl b/Source/Shaders/Builtin/Functions/windowToEyeCoordinates.glsl index f429c26875ec..91bd5e3164f6 100644 --- a/Source/Shaders/Builtin/Functions/windowToEyeCoordinates.glsl +++ b/Source/Shaders/Builtin/Functions/windowToEyeCoordinates.glsl @@ -31,9 +31,12 @@ vec4 czm_windowToEyeCoordinates(vec4 fragmentCoordinate) vec4 q = vec4(x, y, z, 1.0); q /= fragmentCoordinate.w; - if (czm_inverseProjection != mat4(0.0)) { + if (!(czm_inverseProjection == mat4(0.0))) // IE and Edge sometimes do something weird with != between mat4s + { q = czm_inverseProjection * q; - } else { + } + else + { float top = czm_frustumPlanes.x; float bottom = czm_frustumPlanes.y; float left = czm_frustumPlanes.z; diff --git a/Source/Shaders/GlobeFS.glsl b/Source/Shaders/GlobeFS.glsl index 31b3d21689ff..6186d322c9b0 100644 --- a/Source/Shaders/GlobeFS.glsl +++ b/Source/Shaders/GlobeFS.glsl @@ -53,8 +53,8 @@ uniform vec2 u_lightingFadeDistance; #endif #ifdef ENABLE_CLIPPING_PLANES -uniform int u_clippingPlanesLength; -uniform vec4 u_clippingPlanes[czm_maxClippingPlanes]; +uniform sampler2D u_clippingPlanes; +uniform mat4 u_clippingPlanesMatrix; uniform vec4 u_clippingPlanesEdgeStyle; #endif @@ -157,11 +157,7 @@ vec4 computeWaterColor(vec3 positionEyeCoordinates, vec2 textureCoordinates, mat void main() { #ifdef ENABLE_CLIPPING_PLANES - #ifdef UNION_CLIPPING_REGIONS - float clipDistance = czm_discardIfClippedWithUnion(u_clippingPlanes, u_clippingPlanesLength); - #else - float clipDistance = czm_discardIfClippedWithIntersect(u_clippingPlanes, u_clippingPlanesLength); - #endif + float clipDistance = clip(gl_FragCoord, u_clippingPlanes, u_clippingPlanesMatrix); #endif // The clamp below works around an apparent bug in Chrome Canary v23.0.1241.0 diff --git a/Specs/Core/Cartesian4Spec.js b/Specs/Core/Cartesian4Spec.js index 2d5b5c3128d2..69c00533355d 100644 --- a/Specs/Core/Cartesian4Spec.js +++ b/Specs/Core/Cartesian4Spec.js @@ -946,6 +946,21 @@ defineSuite([ }).toThrowDeveloperError(); }); + it('packs and unpacks floating point values for representation as uint8 4-vectors', function() { + var float = 123.456; + var packedFloat = Cartesian4.packFloat(float); + expect(0 <= packedFloat.x && packedFloat.x <= 255).toBe(true); + expect(0 <= packedFloat.y && packedFloat.y <= 255).toBe(true); + expect(0 <= packedFloat.z && packedFloat.z <= 255).toBe(true); + expect(0 <= packedFloat.w && packedFloat.w <= 255).toBe(true); + + var unpackedFloat = Cartesian4.unpackFloat(packedFloat); + expect(CesiumMath.equalsEpsilon(float, unpackedFloat, CesiumMath.EPSILON7)).toBe(true); + + var packedZero = Cartesian4.packFloat(0); + expect(packedZero).toEqual(Cartesian4.ZERO); + }); + createPackableSpecs(Cartesian4, new Cartesian4(1, 2, 3, 4), [1, 2, 3, 4]); createPackableArraySpecs(Cartesian4, [new Cartesian4(1, 2, 3, 4), new Cartesian4(5, 6, 7, 8)], [1, 2, 3, 4, 5, 6, 7, 8]); }); diff --git a/Specs/Core/ClippingPlaneCollectionSpec.js b/Specs/Core/ClippingPlaneCollectionSpec.js index e196db219e9c..9c41233a59ce 100644 --- a/Specs/Core/ClippingPlaneCollectionSpec.js +++ b/Specs/Core/ClippingPlaneCollectionSpec.js @@ -1,32 +1,63 @@ defineSuite([ 'Core/ClippingPlaneCollection', + 'Core/AttributeCompression', 'Core/BoundingSphere', + 'Core/Cartesian2', 'Core/Cartesian3', 'Core/Cartesian4', + 'Core/ClippingPlane', 'Core/Color', + 'Core/Math', + 'Core/PixelFormat', + 'Renderer/PixelDatatype', 'Core/Intersect', 'Core/Matrix4', - 'Core/Plane' + 'Core/Plane', + 'Specs/createScene', + 'Renderer/TextureMagnificationFilter', + 'Renderer/TextureMinificationFilter', + 'Renderer/TextureWrap' ], function( ClippingPlaneCollection, + AttributeCompression, BoundingSphere, + Cartesian2, Cartesian3, Cartesian4, + ClippingPlane, Color, + CesiumMath, + PixelFormat, + PixelDatatype, Intersect, Matrix4, - Plane) { + Plane, + createScene, + TextureMagnificationFilter, + TextureMinificationFilter, + TextureWrap) { 'use strict'; var clippingPlanes; var planes = [ - new Plane(Cartesian3.UNIT_X, 1.0), - new Plane(Cartesian3.UNIT_Y, 2.0) + new ClippingPlane(Cartesian3.UNIT_X, 1.0), + new ClippingPlane(Cartesian3.UNIT_Y, 2.0) ]; var transform = new Matrix4.fromTranslation(new Cartesian3(1.0, 3.0, 2.0)); var boundingVolume = new BoundingSphere(Cartesian3.ZERO, 1.0); + function decodeUint8Plane(pixel1, pixel2) { + // expect pixel1 to be the normal + var xOct16 = pixel1.x * 256 + pixel1.y; + var yOct16 = pixel1.z * 256 + pixel1.w; + var normal = AttributeCompression.octDecodeInRange(xOct16, yOct16, 65535, new Cartesian3()); + + // expect pixel2 to be the distance + var distance = Cartesian4.unpackFloat(pixel2); + return new Plane(normal, distance); + } + it('default constructor', function() { clippingPlanes = new ClippingPlaneCollection(); expect(clippingPlanes._planes).toEqual([]); @@ -47,7 +78,7 @@ defineSuite([ expect(clippingPlanes.length).toBe(2); - clippingPlanes._planes.push(new Plane(Cartesian3.UNIT_Z, -1.0)); + clippingPlanes._planes.push(new ClippingPlane(Cartesian3.UNIT_Z, -1.0)); expect(clippingPlanes.length).toBe(3); @@ -66,15 +97,6 @@ defineSuite([ expect(clippingPlanes._planes[0]).toBe(planes[0]); }); - it('add throws developer error if the added plane exceeds the maximum number of planes', function() { - clippingPlanes = new ClippingPlaneCollection(); - clippingPlanes._planes = new Array(ClippingPlaneCollection.MAX_CLIPPING_PLANES); - - expect(function() { - clippingPlanes.add(new Plane(Cartesian3.UNIT_Z, -1.0)); - }).toThrowDeveloperError(); - }); - it('gets the plane at an index', function() { clippingPlanes = new ClippingPlaneCollection({ planes : planes @@ -96,8 +118,8 @@ defineSuite([ }); expect(clippingPlanes.contains(planes[0])).toBe(true); - expect(clippingPlanes.contains(new Plane(Cartesian3.UNIT_Y, 2.0))).toBe(true); - expect(clippingPlanes.contains(new Plane(Cartesian3.UNIT_Z, 3.0))).toBe(false); + expect(clippingPlanes.contains(new ClippingPlane(Cartesian3.UNIT_Y, 2.0))).toBe(true); + expect(clippingPlanes.contains(new ClippingPlane(Cartesian3.UNIT_Z, 3.0))).toBe(false); }); it('remove removes and the first occurrence of a plane', function() { @@ -118,29 +140,346 @@ defineSuite([ expect(result).toBe(false); }); - it('transforms and packs planes into result parameter', function() { + describe('uint8 texture mode', function() { + beforeEach(function() { + spyOn(ClippingPlaneCollection, 'useFloatTexture').and.returnValue(false); + }); + + it('update creates a RGBA ubyte texture with no filtering or wrapping to house packed clipping planes', function() { + var scene = createScene(); + clippingPlanes = new ClippingPlaneCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + + clippingPlanes.update(scene.frameState); + + var packedTexture = clippingPlanes.texture; + expect(packedTexture).toBeDefined(); + + // Two RGBA uint8 clipping planes consume 4 pixels of texture, allocation to be double that + expect(packedTexture.width).toEqual(8); + expect(packedTexture.height).toEqual(1); + + expect(packedTexture.pixelFormat).toEqual(PixelFormat.RGBA); + expect(packedTexture.pixelDatatype).toEqual(PixelDatatype.UNSIGNED_BYTE); + + var sampler = packedTexture.sampler; + expect(sampler.wrapS).toEqual(TextureWrap.CLAMP_TO_EDGE); + expect(sampler.wrapT).toEqual(TextureWrap.CLAMP_TO_EDGE); + expect(sampler.minificationFilter).toEqual(TextureMinificationFilter.NEAREST); + expect(sampler.magnificationFilter).toEqual(TextureMinificationFilter.NEAREST); + + clippingPlanes.destroy(); + scene.destroyForSpecs(); + }); + + it('update fills the clipping plane texture with packed planes', function() { + var scene = createScene(); + + clippingPlanes = new ClippingPlaneCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + + var rgba; + var gl = scene.frameState.context._gl; + spyOn(gl, 'texSubImage2D').and.callFake(function(target, level, xoffset, yoffset, width, height, format, type, arrayBufferView) { + rgba = arrayBufferView; + }); + + clippingPlanes.update(scene.frameState); + expect(rgba).toBeDefined(); + expect(rgba.length).toEqual(32); + + // Expect two clipping planes to use 4 pixels in the texture, so the first 16 bytes + for (var i = 16; i < rgba.length; i++) { + expect(rgba[i]).toEqual(0); + } + var pixel1 = Cartesian4.fromArray(rgba, 0); + var pixel2 = Cartesian4.fromArray(rgba, 4); + var pixel3 = Cartesian4.fromArray(rgba, 8); + var pixel4 = Cartesian4.fromArray(rgba, 12); + + var plane1 = decodeUint8Plane(pixel1, pixel2); + var plane2 = decodeUint8Plane(pixel3, pixel4); + + expect(Cartesian3.equalsEpsilon(plane1.normal, planes[0].normal, CesiumMath.EPSILON3)).toEqual(true); + expect(Cartesian3.equalsEpsilon(plane2.normal, planes[1].normal, CesiumMath.EPSILON3)).toEqual(true); + expect(CesiumMath.equalsEpsilon(plane1.distance, planes[0].distance, CesiumMath.EPSILON3)).toEqual(true); + expect(CesiumMath.equalsEpsilon(plane2.distance, planes[1].distance, CesiumMath.EPSILON3)).toEqual(true); + + clippingPlanes.destroy(); + scene.destroyForSpecs(); + }); + + it('reallocates textures when above capacity or below 1/4 capacity', function() { + var scene = createScene(); + + clippingPlanes = new ClippingPlaneCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + + clippingPlanes.update(scene.frameState); + + var packedTexture = clippingPlanes.texture; + + // Two RGBA uint8 clipping planes consume 4 pixels of texture, allocation to be double that + expect(packedTexture.width).toEqual(8); + expect(packedTexture.height).toEqual(1); + + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, 1.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, 1.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, 1.0)); + + clippingPlanes.update(scene.frameState); + + expect(packedTexture.isDestroyed()).toBe(true); + packedTexture = clippingPlanes.texture; + + // Five RGBA uint8 clipping planes consume 10 pixels of texture, allocation to be double that + expect(packedTexture.width).toEqual(20); + expect(packedTexture.height).toEqual(1); + + clippingPlanes.removeAll(); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, 1.0)); + + clippingPlanes.update(scene.frameState); + + expect(packedTexture.isDestroyed()).toBe(true); + packedTexture = clippingPlanes.texture; + + // One RGBA uint8 clipping plane consume 2 pixels of texture, allocation to be double that + expect(packedTexture.width).toEqual(4); + expect(packedTexture.height).toEqual(1); + }); + }); + + describe('float texture mode', function() { + it('update creates a float texture with no filtering or wrapping to house packed clipping planes', function() { + var scene = createScene(); + + if (!ClippingPlaneCollection.useFloatTexture(scene._context)) { + // Don't fail just because float textures aren't supported + scene.destroyForSpecs(); + return; + } + + clippingPlanes = new ClippingPlaneCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + + clippingPlanes.update(scene.frameState); + + var packedTexture = clippingPlanes.texture; + expect(packedTexture).toBeDefined(); + expect(packedTexture.width).toEqual(4); + expect(packedTexture.height).toEqual(1); + expect(packedTexture.pixelFormat).toEqual(PixelFormat.RGBA); + expect(packedTexture.pixelDatatype).toEqual(PixelDatatype.FLOAT); + + var sampler = packedTexture.sampler; + expect(sampler.wrapS).toEqual(TextureWrap.CLAMP_TO_EDGE); + expect(sampler.wrapT).toEqual(TextureWrap.CLAMP_TO_EDGE); + expect(sampler.minificationFilter).toEqual(TextureMinificationFilter.NEAREST); + expect(sampler.magnificationFilter).toEqual(TextureMinificationFilter.NEAREST); + + clippingPlanes.destroy(); + scene.destroyForSpecs(); + }); + + it('update fills the clipping plane texture with packed planes', function() { + var scene = createScene(); + + if (!ClippingPlaneCollection.useFloatTexture(scene._context)) { + // Don't fail just because float textures aren't supported + scene.destroyForSpecs(); + return; + } + + clippingPlanes = new ClippingPlaneCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + + var rgba; + var gl = scene.frameState.context._gl; + spyOn(gl, 'texSubImage2D').and.callFake(function(target, level, xoffset, yoffset, width, height, format, type, arrayBufferView) { + rgba = arrayBufferView; + }); + + clippingPlanes.update(scene.frameState); + expect(rgba).toBeDefined(); + expect(rgba.length).toEqual(16); + + // Expect two clipping planes to use 2 pixels in the texture, so the first 8 floats. + for (var i = 8; i < rgba.length; i++) { + expect(rgba[i]).toEqual(0); + } + var plane1 = Plane.fromCartesian4(Cartesian4.fromArray(rgba, 0)); + var plane2 = Plane.fromCartesian4(Cartesian4.fromArray(rgba, 4)); + + expect(Cartesian3.equalsEpsilon(plane1.normal, planes[0].normal, CesiumMath.EPSILON3)).toEqual(true); + expect(Cartesian3.equalsEpsilon(plane2.normal, planes[1].normal, CesiumMath.EPSILON3)).toEqual(true); + expect(CesiumMath.equalsEpsilon(plane1.distance, planes[0].distance, CesiumMath.EPSILON3)).toEqual(true); + expect(CesiumMath.equalsEpsilon(plane2.distance, planes[1].distance, CesiumMath.EPSILON3)).toEqual(true); + + clippingPlanes.destroy(); + scene.destroyForSpecs(); + }); + + it('reallocates textures when above capacity or below 1/4 capacity', function() { + var scene = createScene(); + + if (!ClippingPlaneCollection.useFloatTexture(scene._context)) { + // Don't fail just because float textures aren't supported + scene.destroyForSpecs(); + return; + } + + clippingPlanes = new ClippingPlaneCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + + clippingPlanes.update(scene.frameState); + + var packedTexture = clippingPlanes.texture; + + // Two RGBA float clipping planes consume 2 pixels of texture, allocation to be double that + expect(packedTexture.width).toEqual(4); + expect(packedTexture.height).toEqual(1); + + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, 1.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, 1.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, 1.0)); + + clippingPlanes.update(scene.frameState); + + expect(packedTexture.isDestroyed()).toBe(true); + packedTexture = clippingPlanes.texture; + + // Five RGBA float clipping planes consume 5 pixels of texture, allocation to be double that + expect(packedTexture.width).toEqual(10); + expect(packedTexture.height).toEqual(1); + + clippingPlanes.removeAll(); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, 1.0)); + + clippingPlanes.update(scene.frameState); + + expect(packedTexture.isDestroyed()).toBe(true); + packedTexture = clippingPlanes.texture; + + // One RGBA float clipping plane consume 1 pixels of texture, allocation to be double that + expect(packedTexture.width).toEqual(2); + expect(packedTexture.height).toEqual(1); + }); + }); + + it('does not perform texture updates if the planes are unchanged', function() { + var scene = createScene(); + + var gl = scene.frameState.context._gl; + spyOn(gl, 'texSubImage2D').and.callThrough(); + clippingPlanes = new ClippingPlaneCollection({ - planes : planes + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform }); + expect(gl.texSubImage2D.calls.count()).toEqual(0); + + clippingPlanes.update(scene.frameState); + expect(gl.texSubImage2D.calls.count()).toEqual(1); - var result = clippingPlanes.transformAndPackPlanes(transform); - expect(result.length).toEqual(2); - expect(result[0]).toEqual(new Cartesian4(1.0, 0.0, 0.0, 0.0)); - expect(result[1]).toEqual(new Cartesian4(0.0, 1.0, 0.0, -1.0)); + clippingPlanes.update(scene.frameState); + expect(gl.texSubImage2D.calls.count()).toEqual(1); + + clippingPlanes.destroy(); + scene.destroyForSpecs(); }); - it('transforms and packs planes with no result parameter creates new array', function() { + it('detects if a standard Plane has been added and always performs texture updates', function() { + var scene = createScene(); + + var gl = scene.frameState.context._gl; + spyOn(gl, 'texSubImage2D').and.callThrough(); + clippingPlanes = new ClippingPlaneCollection({ - planes : planes + planes : [new Plane(Cartesian3.UNIT_X, 1.0)], + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + expect(gl.texSubImage2D.calls.count()).toEqual(0); + + clippingPlanes.update(scene.frameState); + expect(gl.texSubImage2D.calls.count()).toEqual(1); + + clippingPlanes.update(scene.frameState); + expect(gl.texSubImage2D.calls.count()).toEqual(2); + + clippingPlanes.destroy(); + scene.destroyForSpecs(); + }); + + it('provides a function for attaching the ClippingPlaneCollection to objects', function() { + var clippedObject1 = { + clippingPlanes : undefined + }; + var clippedObject2 = { + clippingPlanes : undefined + }; + + var clippingPlanes1 = new ClippingPlaneCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform + }); + + ClippingPlaneCollection.setOwnership(clippingPlanes1, clippedObject1, 'clippingPlanes'); + expect(clippedObject1.clippingPlanes).toBe(clippingPlanes1); + expect(clippingPlanes1._owner).toBe(clippedObject1); + + var clippingPlanes2 = new ClippingPlaneCollection({ + planes : planes, + enabled : false, + edgeColor : Color.RED, + modelMatrix : transform }); - var result = clippingPlanes.transformAndPackPlanes(transform); - expect(result.length).toEqual(2); - expect(result[0]).toBeInstanceOf(Cartesian4); - expect(result[1]).toBeInstanceOf(Cartesian4); + // Expect detached clipping planes to be destroyed + ClippingPlaneCollection.setOwnership(clippingPlanes2, clippedObject1, 'clippingPlanes'); + expect(clippingPlanes1.isDestroyed()).toBe(true); + + // Expect setting the same ClippingPlaneCollection again to not destroy the ClippingPlaneCollection + ClippingPlaneCollection.setOwnership(clippingPlanes2, clippedObject1, 'clippingPlanes'); + expect(clippingPlanes2.isDestroyed()).toBe(false); + + // Expect failure when attaching one ClippingPlaneCollection to two objects + expect(function() { + ClippingPlaneCollection.setOwnership(clippingPlanes2, clippedObject2, 'clippingPlanes'); + }).toThrowDeveloperError(); }); - it('clone without a result parameter returns new identical copy', function() { + it('clone without a result parameter returns new copy', function() { clippingPlanes = new ClippingPlaneCollection({ planes : planes, enabled : false, @@ -150,8 +489,10 @@ defineSuite([ var result = clippingPlanes.clone(); expect(result).not.toBe(clippingPlanes); - expect(result._planes[0]).toEqual(planes[0]); - expect(result._planes[1]).toEqual(planes[1]); + expect(Cartesian3.equals(result._planes[0].normal, planes[0].normal)).toBe(true); + expect(result._planes[0].distance).toEqual(planes[0].distance); + expect(Cartesian3.equals(result._planes[1].normal, planes[1].normal)).toBe(true); + expect(result._planes[1].distance).toEqual(planes[1].distance); expect(result.enabled).toEqual(false); expect(result.modelMatrix).toEqual(transform); expect(result.edgeColor).toEqual(Color.RED); @@ -171,8 +512,10 @@ defineSuite([ 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(Cartesian3.equals(result._planes[0].normal, planes[0].normal)).toBe(true); + expect(result._planes[0].distance).toEqual(planes[0].distance); + expect(Cartesian3.equals(result._planes[1].normal, planes[1].normal)).toBe(true); + expect(result._planes[1].distance).toEqual(planes[1].distance); expect(result.enabled).toEqual(false); expect(result.modelMatrix).toEqual(transform); expect(result.edgeColor).toEqual(Color.RED); @@ -203,19 +546,19 @@ defineSuite([ var intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); expect(intersect).toEqual(Intersect.INSIDE); - clippingPlanes.add(new Plane(Cartesian3.UNIT_X, -2.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, -2.0)); intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); expect(intersect).toEqual(Intersect.OUTSIDE); - clippingPlanes.add(new Plane(Cartesian3.UNIT_Y, 0.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_Y, 0.0)); intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); expect(intersect).toEqual(Intersect.INTERSECTING); - clippingPlanes.add(new Plane(Cartesian3.UNIT_Z, 1.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_Z, 1.0)); intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); expect(intersect).toEqual(Intersect.INSIDE); - clippingPlanes.add(new Plane(Cartesian3.UNIT_Z, 0.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_Z, 0.0)); intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); expect(intersect).toEqual(Intersect.INSIDE); }); @@ -228,16 +571,16 @@ defineSuite([ var intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); expect(intersect).toEqual(Intersect.INSIDE); - clippingPlanes.add(new Plane(Cartesian3.UNIT_Z, 1.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_Z, 1.0)); intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); expect(intersect).toEqual(Intersect.INSIDE); - var temp = new Plane(Cartesian3.UNIT_Y, -2.0); + var temp = new ClippingPlane(Cartesian3.UNIT_Y, -2.0); clippingPlanes.add(temp); intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); expect(intersect).toEqual(Intersect.OUTSIDE); - clippingPlanes.add(new Plane(Cartesian3.UNIT_X, 0.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, 0.0)); intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume); expect(intersect).toEqual(Intersect.OUTSIDE); @@ -246,14 +589,31 @@ defineSuite([ expect(intersect).toEqual(Intersect.INTERSECTING); }); - it('computes intersections applies optional transform to planes', function() { + it('compute intersections applies optional transform to planes', function() { clippingPlanes = new ClippingPlaneCollection(); var intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume, transform); expect(intersect).toEqual(Intersect.INSIDE); - clippingPlanes.add(new Plane(Cartesian3.UNIT_X, -1.0)); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, -1.0)); intersect = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume, transform); expect(intersect).not.toEqual(Intersect.INSIDE); }); + + it('computes a description of the current shader for comparison', function() { + clippingPlanes = new ClippingPlaneCollection(); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_X, -1.0)); + + expect(clippingPlanes.clippingPlanesState()).toEqual(-1); + + var holdThisPlane = new ClippingPlane(Cartesian3.UNIT_X, -1.0); + clippingPlanes.add(holdThisPlane); + expect(clippingPlanes.clippingPlanesState()).toEqual(-2); + + clippingPlanes.unionClippingRegions = true; + expect(clippingPlanes.clippingPlanesState()).toEqual(2); + + clippingPlanes.remove(holdThisPlane); + expect(clippingPlanes.clippingPlanesState()).toEqual(1); + }); }); diff --git a/Specs/Core/ClippingPlaneSpec.js b/Specs/Core/ClippingPlaneSpec.js new file mode 100644 index 000000000000..2aa6e569daf9 --- /dev/null +++ b/Specs/Core/ClippingPlaneSpec.js @@ -0,0 +1,97 @@ +defineSuite([ + 'Core/ClippingPlane', + 'Core/Cartesian3', + 'Core/Math', + 'Core/Matrix3', + 'Core/Matrix4', + 'Core/Plane' + ], function( + ClippingPlane, + Cartesian3, + CesiumMath, + Matrix3, + Matrix4, + Plane) { + 'use strict'; + + it('constructs', function() { + var normal = Cartesian3.UNIT_X; + var distance = 1.0; + var clippingPlane = new ClippingPlane(normal, distance); + expect(clippingPlane.normal).toEqual(normal); + expect(clippingPlane.distance).toEqual(distance); + }); + + it('runs onChangeCallback when changed', function() { + var normal = Cartesian3.UNIT_X; + var distance = 1.0; + var changeCount = 0; + + var clippingPlane = new ClippingPlane(normal, distance); + clippingPlane.onChangeCallback = function(index) { + expect(index).toEqual(clippingPlane.index); + changeCount++; + }; + + // Distance change + clippingPlane.distance += 0.1; + expect(changeCount).toEqual(1); + + // Distance non-change + clippingPlane.distance += 0.0; + expect(changeCount).toEqual(1); + + // Normal change + clippingPlane.normal = Cartesian3.UNIT_Z; + expect(changeCount).toEqual(2); + + // Normal non-change + clippingPlane.normal = Cartesian3.UNIT_Z; + expect(changeCount).toEqual(2); + + // Normal member change + clippingPlane.normal.x += 1.0; + expect(changeCount).toEqual(3); + }); + + it('can be instantiated from a Plane', function() { + var plane = new Plane(Cartesian3.UNIT_X, 1.0); + var clippingPlane = ClippingPlane.fromPlane(plane); + expect(Cartesian3.equals(clippingPlane.normal, plane.normal)).toBe(true); + expect(clippingPlane.distance).toEqual(plane.distance); + + var scratchClippingPlane = new ClippingPlane(Cartesian3.UNIT_Y, 0.0); + clippingPlane = ClippingPlane.fromPlane(plane, scratchClippingPlane); + expect(Cartesian3.equals(clippingPlane.normal, plane.normal)).toBe(true); + expect(clippingPlane.distance).toEqual(plane.distance); + expect(clippingPlane).toBe(scratchClippingPlane); + }); + + it('clones', function() { + var clippingPlane = new ClippingPlane(Cartesian3.UNIT_X, 1.0); + var cloneClippingPlane = ClippingPlane.clone(clippingPlane); + expect(Cartesian3.equals(clippingPlane.normal, cloneClippingPlane.normal)).toBe(true); + expect(clippingPlane.distance).toEqual(cloneClippingPlane.distance); + + var scratchClippingPlane = new ClippingPlane(Cartesian3.UNIT_Y, 0.0); + cloneClippingPlane = ClippingPlane.clone(clippingPlane, scratchClippingPlane); + expect(Cartesian3.equals(clippingPlane.normal, cloneClippingPlane.normal)).toBe(true); + expect(clippingPlane.distance).toEqual(cloneClippingPlane.distance); + expect(cloneClippingPlane).toBe(scratchClippingPlane); + }); + + it('works with Plane math', function() { + var normal = new Cartesian3(1.0, 2.0, 3.0); + normal = Cartesian3.normalize(normal, normal); + var clippingPlane = new ClippingPlane(normal, 12.34); + + var transform = Matrix4.fromUniformScale(2.0); + transform = Matrix4.multiplyByMatrix3(transform, Matrix3.fromRotationY(Math.PI), transform); + + var transformedPlane = Plane.transform(clippingPlane, transform); + expect(transformedPlane.distance).toEqual(clippingPlane.distance * 2.0); + expect(transformedPlane.normal.x).toEqualEpsilon(-clippingPlane.normal.x, CesiumMath.EPSILON10); + expect(transformedPlane.normal.y).toEqual(clippingPlane.normal.y); + expect(transformedPlane.normal.z).toEqual(-clippingPlane.normal.z); + }); +}); diff --git a/Specs/DataSources/ModelVisualizerSpec.js b/Specs/DataSources/ModelVisualizerSpec.js index 3cca9e13f21a..1ef89c237d74 100644 --- a/Specs/DataSources/ModelVisualizerSpec.js +++ b/Specs/DataSources/ModelVisualizerSpec.js @@ -2,12 +2,12 @@ defineSuite([ 'DataSources/ModelVisualizer', 'Core/BoundingSphere', 'Core/Cartesian3', + 'Core/ClippingPlane', 'Core/ClippingPlaneCollection', 'Core/defined', 'Core/DistanceDisplayCondition', 'Core/JulianDate', 'Core/Matrix4', - 'Core/Plane', 'Core/Quaternion', 'Core/Resource', 'Core/Transforms', @@ -24,12 +24,12 @@ defineSuite([ ModelVisualizer, BoundingSphere, Cartesian3, + ClippingPlane, ClippingPlaneCollection, defined, DistanceDisplayCondition, JulianDate, Matrix4, - Plane, Quaternion, Resource, Transforms, @@ -144,7 +144,7 @@ defineSuite([ var clippingPlanes = new ClippingPlaneCollection({ planes: [ - new Plane(Cartesian3.UNIT_X, 0.0) + new ClippingPlane(Cartesian3.UNIT_X, 0.0) ] }); model.clippingPlanes = new ConstantProperty(clippingPlanes); @@ -164,7 +164,9 @@ 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); + expect(primitive.clippingPlanes._planes.length).toEqual(clippingPlanes._planes.length); + expect(Cartesian3.equals(primitive.clippingPlanes._planes[0].normal, clippingPlanes._planes[0].normal)).toBe(true); + expect(primitive.clippingPlanes._planes[0].distance).toEqual(clippingPlanes._planes[0].distance); // wait till the model is loaded before we can check node transformations return pollToPromise(function() { diff --git a/Specs/Renderer/AutomaticUniformSpec.js b/Specs/Renderer/AutomaticUniformSpec.js index 239897440a44..72baabf820d2 100644 --- a/Specs/Renderer/AutomaticUniformSpec.js +++ b/Specs/Renderer/AutomaticUniformSpec.js @@ -1280,4 +1280,31 @@ defineSuite([ }).contextToRender(); }); + it('has czm_orthographicIn3D', function() { + var frameState = createFrameState(context, createMockCamera()); + context.uniformState.update(frameState); + var fs = + 'void main() {' + + ' gl_FragColor = vec4(czm_orthographicIn3D == 0.0);' + + '}'; + expect({ + context : context, + fragmentShader : fs + }).contextToRender(); + + var frustum = new OrthographicFrustum(); + frustum.aspectRatio = 1.0; + frustum.width = 1.0; + frameState.camera.frustum = frustum; + context.uniformState.update(frameState); + fs = + 'void main() {' + + ' gl_FragColor = vec4(czm_orthographicIn3D == 1.0);' + + '}'; + expect({ + context : context, + fragmentShader : fs + }).contextToRender(); + }); + }, 'WebGL'); diff --git a/Specs/Renderer/BuiltinFunctionsSpec.js b/Specs/Renderer/BuiltinFunctionsSpec.js index b1e80ffad5a6..623bab8ff0e6 100644 --- a/Specs/Renderer/BuiltinFunctionsSpec.js +++ b/Specs/Renderer/BuiltinFunctionsSpec.js @@ -366,4 +366,20 @@ defineSuite([ fragmentShader : fs }).contextToRender(); }); + + it('has czm_transformPlane', function() { + var fs = + 'void main() { ' + + ' mat4 uniformScale2 = mat4(2.0, 0.0, 0.0, 0.0,' + + ' 0.0, 2.0, 0.0, 0.0,' + + ' 0.0, 0.0, 2.0, 0.0,' + + ' 0.0, 0.0, 0.0, 1.0);' + + ' gl_FragColor = vec4(all(equal(czm_transformPlane(uniformScale2, vec4(1.0, 0.0, 0.0, 10.0)), vec4(1.0, 0.0, 0.0, 20.0))));' + + '}'; + expect({ + context : context, + fragmentShader : fs + }).contextToRender(); + }); + }, 'WebGL'); diff --git a/Specs/Scene/Batched3DModel3DTileContentSpec.js b/Specs/Scene/Batched3DModel3DTileContentSpec.js index 2183b62f39e4..5f2be35499d9 100644 --- a/Specs/Scene/Batched3DModel3DTileContentSpec.js +++ b/Specs/Scene/Batched3DModel3DTileContentSpec.js @@ -1,25 +1,27 @@ defineSuite([ 'Scene/Batched3DModel3DTileContent', 'Core/Cartesian3', + 'Core/ClippingPlane', 'Core/ClippingPlaneCollection', 'Core/Color', 'Core/HeadingPitchRange', 'Core/HeadingPitchRoll', - 'Core/Plane', 'Core/Transforms', 'Specs/Cesium3DTilesTester', - 'Specs/createScene' + 'Specs/createScene', + 'Scene/Model' ], function( Batched3DModel3DTileContent, Cartesian3, + ClippingPlane, ClippingPlaneCollection, Color, HeadingPitchRange, HeadingPitchRoll, - Plane, Transforms, Cesium3DTilesTester, - createScene) { + createScene, + Model) { 'use strict'; var scene; @@ -290,36 +292,49 @@ defineSuite([ }); }); - it('Updates model\'s clipping planes', function() { + it('Links model to tileset clipping planes based on bounding volume clipping', 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.length).toBe(0); - expect(model.clippingPlanes.enabled).toBe(false); + expect(model.clippingPlanes).toBeUndefined(); - tileset.clippingPlanes = new ClippingPlaneCollection({ + var clippingPlaneCollection = new ClippingPlaneCollection({ planes : [ - new Plane(Cartesian3.UNIT_X, 0.0) + new ClippingPlane(Cartesian3.UNIT_X, 0.0) ] }); - content.update(tileset, scene.frameState); + tileset.clippingPlanes = clippingPlaneCollection; + clippingPlaneCollection.update(scene.frameState); + tile.update(tileset, scene.frameState); expect(model.clippingPlanes).toBeDefined(); - expect(model.clippingPlanes.length).toBe(1); - expect(model.clippingPlanes.enabled).toBe(true); + expect(model.clippingPlanes).toBe(tileset.clippingPlanes); tile._isClipped = false; - content.update(tileset, scene.frameState); + tile.update(tileset, scene.frameState); - expect(model.clippingPlanes.enabled).toBe(false); + expect(model.clippingPlanes).toBeUndefined(); + }); + }); - tileset.clippingPlanes = undefined; + it('rebuilds Model shaders when clipping planes change', function() { + spyOn(Model, '_getClippingFunction').and.callThrough(); - expect(model.clippingPlanes).toBeDefined(); - expect(model.clippingPlanes.enabled).toBe(false); + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + var tile = tileset._root; + + var clippingPlaneCollection = new ClippingPlaneCollection({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_X, 0.0) + ] + }); + tileset.clippingPlanes = clippingPlaneCollection; + clippingPlaneCollection.update(scene.frameState); + tile.update(tileset, scene.frameState); + + expect(Model._getClippingFunction.calls.count()).toEqual(2); }); }); diff --git a/Specs/Scene/Cesium3DTilesetSpec.js b/Specs/Scene/Cesium3DTilesetSpec.js index 5554566e08e6..ce63efab7177 100644 --- a/Specs/Scene/Cesium3DTilesetSpec.js +++ b/Specs/Scene/Cesium3DTilesetSpec.js @@ -1,6 +1,7 @@ defineSuite([ 'Scene/Cesium3DTileset', 'Core/Cartesian3', + 'Core/ClippingPlane', 'Core/ClippingPlaneCollection', 'Core/Color', 'Core/CullingVolume', @@ -12,7 +13,6 @@ defineSuite([ 'Core/Math', 'Core/Matrix4', 'Core/PerspectiveFrustum', - 'Core/Plane', 'Core/PrimitiveType', 'Core/RequestScheduler', 'Core/Resource', @@ -31,6 +31,7 @@ defineSuite([ ], function( Cesium3DTileset, Cartesian3, + ClippingPlane, ClippingPlaneCollection, Color, CullingVolume, @@ -42,7 +43,6 @@ defineSuite([ CesiumMath, Matrix4, PerspectiveFrustum, - Plane, PrimitiveType, RequestScheduler, Resource, @@ -2893,13 +2893,56 @@ defineSuite([ }); }); + it('destroys attached ClippingPlaneCollections and ClippingPlaneCollections that have been detached', function() { + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset) { + var clippingPlaneCollection1 = new ClippingPlaneCollection({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_Z, -100000000.0) + ] + }); + expect(clippingPlaneCollection1.owner).not.toBeDefined(); + + tileset.clippingPlanes = clippingPlaneCollection1; + var clippingPlaneCollection2 = new ClippingPlaneCollection({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_Z, -100000000.0) + ] + }); + + tileset.clippingPlanes = clippingPlaneCollection2; + expect(clippingPlaneCollection1.isDestroyed()).toBe(true); + + scene.primitives.remove(tileset); + expect(clippingPlaneCollection2.isDestroyed()).toBe(true); + }); + }); + + it('throws a DeveloperError when given a ClippingPlaneCollection attached to another Tileset', function() { + var clippingPlanes; + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl).then(function(tileset1) { + clippingPlanes = new ClippingPlaneCollection({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_X, 0.0) + ] + }); + tileset1.clippingPlanes = clippingPlanes; + + return Cesium3DTilesTester.loadTileset(scene, tilesetUrl); + }) + .then(function(tileset2) { + expect(function() { + tileset2.clippingPlanes = clippingPlanes; + }).toThrowDeveloperError(); + }); + }); + 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); + var plane = new ClippingPlane(Cartesian3.UNIT_Z, -100000000.0); tileset.clippingPlanes = new ClippingPlaneCollection({ planes : [ plane @@ -2923,7 +2966,7 @@ defineSuite([ expect(visibility).not.toBe(Intersect.OUTSIDE); - var plane = new Plane(Cartesian3.UNIT_Z, -100000000.0); + var plane = new ClippingPlane(Cartesian3.UNIT_Z, -100000000.0); tileset.clippingPlanes = new ClippingPlaneCollection({ planes : [ plane @@ -2952,7 +2995,7 @@ defineSuite([ tileset.update(scene.frameState); - var plane = new Plane(Cartesian3.UNIT_Z, 0.0); + var plane = new ClippingPlane(Cartesian3.UNIT_Z, 0.0); tileset.clippingPlanes = new ClippingPlaneCollection({ planes : [ plane @@ -2994,7 +3037,7 @@ defineSuite([ tileset.update(scene.frameState); - var plane = new Plane(Cartesian3.UNIT_Z, 0.0); + var plane = new ClippingPlane(Cartesian3.UNIT_Z, 0.0); tileset.clippingPlanes = new ClippingPlaneCollection({ planes : [ plane diff --git a/Specs/Scene/GlobeSurfaceTileProviderSpec.js b/Specs/Scene/GlobeSurfaceTileProviderSpec.js index c5a6bda85b21..380e907415ba 100644 --- a/Specs/Scene/GlobeSurfaceTileProviderSpec.js +++ b/Specs/Scene/GlobeSurfaceTileProviderSpec.js @@ -2,6 +2,7 @@ defineSuite([ 'Scene/GlobeSurfaceTileProvider', 'Core/Cartesian3', 'Core/CesiumTerrainProvider', + 'Core/ClippingPlane', 'Core/ClippingPlaneCollection', 'Core/Color', 'Core/Credit', @@ -10,7 +11,6 @@ defineSuite([ 'Core/EllipsoidTerrainProvider', 'Core/GeographicProjection', 'Core/Intersect', - 'Core/Plane', 'Core/Rectangle', 'Core/WebMercatorProjection', 'Renderer/ContextLimits', @@ -21,6 +21,7 @@ defineSuite([ 'Scene/GlobeSurfaceShaderSet', 'Scene/ImageryLayerCollection', 'Scene/ImagerySplitDirection', + 'Scene/Model', 'Scene/QuadtreeTile', 'Scene/QuadtreeTileProvider', 'Scene/SceneMode', @@ -32,6 +33,7 @@ defineSuite([ GlobeSurfaceTileProvider, Cartesian3, CesiumTerrainProvider, + ClippingPlane, ClippingPlaneCollection, Color, Credit, @@ -40,7 +42,6 @@ defineSuite([ EllipsoidTerrainProvider, GeographicProjection, Intersect, - Plane, Rectangle, WebMercatorProjection, ContextLimits, @@ -51,6 +52,7 @@ defineSuite([ GlobeSurfaceShaderSet, ImageryLayerCollection, ImagerySplitDirection, + Model, QuadtreeTile, QuadtreeTileProvider, SceneMode, @@ -759,7 +761,7 @@ defineSuite([ expect(rgba).not.toEqual([0, 0, 0, 255]); }); - var clipPlane = new Plane(Cartesian3.UNIT_Z, -10000.0); + var clipPlane = new ClippingPlane(Cartesian3.UNIT_Z, -10000.0); scene.globe.clippingPlanes = new ClippingPlaneCollection ({ planes : [ clipPlane @@ -790,7 +792,7 @@ defineSuite([ expect(rgba).not.toEqual([0, 0, 0, 255]); }); - var clipPlane = new Plane(Cartesian3.UNIT_Z, -1000.0); + var clipPlane = new ClippingPlane(Cartesian3.UNIT_Z, -1000.0); scene.globe.clippingPlanes = new ClippingPlaneCollection ({ planes : [ clipPlane @@ -825,8 +827,8 @@ defineSuite([ scene.globe.clippingPlanes = new ClippingPlaneCollection ({ planes : [ - new Plane(Cartesian3.UNIT_Z, -10000.0), - new Plane(Cartesian3.UNIT_X, -1000.0) + new ClippingPlane(Cartesian3.UNIT_Z, -10000.0), + new ClippingPlane(Cartesian3.UNIT_X, -1000.0) ], unionClippingRegions: true }); @@ -854,7 +856,7 @@ defineSuite([ var globe = scene.globe; globe.clippingPlanes = new ClippingPlaneCollection ({ planes : [ - new Plane(Cartesian3.UNIT_Z, -1000000.0) + new ClippingPlane(Cartesian3.UNIT_Z, -1000000.0) ] }); @@ -872,7 +874,7 @@ defineSuite([ var globe = scene.globe; globe.clippingPlanes = new ClippingPlaneCollection ({ planes : [ - new Plane(Cartesian3.UNIT_Z, 0.0) + new ClippingPlane(Cartesian3.UNIT_Z, 0.0) ] }); @@ -890,7 +892,7 @@ defineSuite([ var globe = scene.globe; globe.clippingPlanes = new ClippingPlaneCollection ({ planes : [ - new Plane(Cartesian3.UNIT_Z, 10000000.0) + new ClippingPlane(Cartesian3.UNIT_Z, 10000000.0) ] }); @@ -904,4 +906,35 @@ defineSuite([ }); }); + it('destroys attached ClippingPlaneCollections that have been detached', function() { + var clippingPlanes = new ClippingPlaneCollection ({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_Z, 10000000.0) + ] + }); + var globe = scene.globe; + globe.clippingPlanes = clippingPlanes; + expect(clippingPlanes.isDestroyed()).toBe(false); + + globe.clippingPlanes = undefined; + expect(clippingPlanes.isDestroyed()).toBe(true); + }); + + it('throws a DeveloperError when given a ClippingPlaneCollection attached to a Model', function() { + var clippingPlanes = new ClippingPlaneCollection ({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_Z, 10000000.0) + ] + }); + var model = scene.primitives.add(Model.fromGltf({ + url : './Data/Models/Box/CesiumBoxTest.gltf' + })); + model.clippingPlanes = clippingPlanes; + var globe = scene.globe; + + expect(function() { + globe.clippingPlanes = clippingPlanes; + }).toThrowDeveloperError(); + }); + }, 'WebGL'); diff --git a/Specs/Scene/Instanced3DModel3DTileContentSpec.js b/Specs/Scene/Instanced3DModel3DTileContentSpec.js index 7b520df17c43..0da44e7f15e9 100644 --- a/Specs/Scene/Instanced3DModel3DTileContentSpec.js +++ b/Specs/Scene/Instanced3DModel3DTileContentSpec.js @@ -1,25 +1,27 @@ defineSuite([ 'Core/Cartesian3', + 'Core/ClippingPlane', 'Core/ClippingPlaneCollection', 'Core/Color', 'Core/HeadingPitchRange', 'Core/HeadingPitchRoll', - 'Core/Plane', 'Core/Transforms', 'Scene/TileBoundingSphere', 'Specs/Cesium3DTilesTester', - 'Specs/createScene' + 'Specs/createScene', + 'Scene/Model' ], 'Scene/Instanced3DModel3DTileContent', function( Cartesian3, + ClippingPlane, ClippingPlaneCollection, Color, HeadingPitchRange, HeadingPitchRoll, - Plane, Transforms, TileBoundingSphere, Cesium3DTilesTester, - createScene) { + createScene, + Model) { 'use strict'; var scene; @@ -305,7 +307,7 @@ defineSuite([ }); }); - it('Updates model\'s clipping planes', function() { + it('Links model to tileset clipping planes based on bounding volume clipping', function() { return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { var tile = tileset._root; var content = tile.content; @@ -313,26 +315,43 @@ defineSuite([ expect(model.clippingPlanes).toBeUndefined(); - tileset.clippingPlanes = new ClippingPlaneCollection({ + var clippingPlaneCollection = new ClippingPlaneCollection({ planes : [ - new Plane(Cartesian3.UNIT_X, 0.0) + new ClippingPlane(Cartesian3.UNIT_X, 0.0) ] }); - content.update(tileset, scene.frameState); + tileset.clippingPlanes = clippingPlaneCollection; + clippingPlaneCollection.update(scene.frameState); + tile.update(tileset, scene.frameState); expect(model.clippingPlanes).toBeDefined(); - expect(model.clippingPlanes.length).toBe(1); - expect(model.clippingPlanes.enabled).toBe(true); + expect(model.clippingPlanes).toBe(tileset.clippingPlanes); tile._isClipped = false; - content.update(tileset, scene.frameState); + tile.update(tileset, scene.frameState); - expect(model.clippingPlanes.enabled).toBe(false); + expect(model.clippingPlanes).toBeUndefined(); + }); + }); - tileset.clippingPlanes = undefined; + it('rebuilds Model shaders when clipping planes change', function() { + spyOn(Model, '_getClippingFunction').and.callThrough(); - expect(model.clippingPlanes).toBeDefined(); - expect(model.clippingPlanes.enabled).toBe(false); + return Cesium3DTilesTester.loadTileset(scene, withBatchTableUrl).then(function(tileset) { + var tile = tileset._root; + var content = tile.content; + + var clippingPlaneCollection = new ClippingPlaneCollection({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_X, 0.0) + ] + }); + tileset.clippingPlanes = clippingPlaneCollection; + clippingPlaneCollection.update(scene.frameState); + content.clippingPlanesDirty = true; + tile.update(tileset, scene.frameState); + + expect(Model._getClippingFunction.calls.count()).toEqual(2); }); }); diff --git a/Specs/Scene/ModelInstanceCollectionSpec.js b/Specs/Scene/ModelInstanceCollectionSpec.js index bb51e7e8f93e..df0f831a67e6 100644 --- a/Specs/Scene/ModelInstanceCollectionSpec.js +++ b/Specs/Scene/ModelInstanceCollectionSpec.js @@ -550,6 +550,9 @@ defineSuite([ expect(drawCommand.receiveShadows).toBe(true); collection.shadows = ShadowMode.DISABLED; scene.renderForSpecs(); + + // Expect commands to have been recreated + drawCommand = collection._drawCommands[0]; expect(drawCommand.castShadows).toBe(false); expect(drawCommand.receiveShadows).toBe(false); }); diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js index d747edb28d99..a8ee41ed6f18 100644 --- a/Specs/Scene/ModelSpec.js +++ b/Specs/Scene/ModelSpec.js @@ -3,6 +3,7 @@ defineSuite([ 'Core/Cartesian3', 'Core/Cartesian4', 'Core/CesiumTerrainProvider', + 'Core/ClippingPlane', 'Core/ClippingPlaneCollection', 'Core/Color', 'Core/combine', @@ -19,7 +20,6 @@ defineSuite([ 'Core/Matrix3', 'Core/Matrix4', 'Core/PerspectiveFrustum', - 'Core/Plane', 'Core/PrimitiveType', 'Core/Resource', 'Core/Transforms', @@ -40,6 +40,7 @@ defineSuite([ Cartesian3, Cartesian4, CesiumTerrainProvider, + ClippingPlane, ClippingPlaneCollection, Color, combine, @@ -56,7 +57,6 @@ defineSuite([ Matrix3, Matrix4, PerspectiveFrustum, - Plane, PrimitiveType, Resource, Transforms, @@ -769,6 +769,100 @@ defineSuite([ }); }); + it('destroys attached ClippingPlaneCollections', function() { + return loadModel(boxUrl).then(function(model) { + var clippingPlanes = new ClippingPlaneCollection({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_X, 0.0) + ] + }); + model.clippingPlanes = clippingPlanes; + expect(model.isDestroyed()).toEqual(false); + expect(clippingPlanes.isDestroyed()).toEqual(false); + primitives.remove(model); + expect(model.isDestroyed()).toEqual(true); + expect(clippingPlanes.isDestroyed()).toEqual(true); + }); + }); + + it('throws a DeveloperError when given a ClippingPlaneCollection attached to another Model', function() { + var clippingPlanes; + return loadModel(boxUrl).then(function(model1) { + clippingPlanes = new ClippingPlaneCollection({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_X, 0.0) + ] + }); + model1.clippingPlanes = clippingPlanes; + + return loadModel(boxUrl); + }) + .then(function(model2) { + expect(function() { + model2.clippingPlanes = clippingPlanes; + }).toThrowDeveloperError(); + }); + }); + + it('destroys ClippingPlaneCollections that are detached', function() { + var clippingPlanes; + return loadModel(boxUrl).then(function(model1) { + clippingPlanes = new ClippingPlaneCollection({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_X, 0.0) + ] + }); + model1.clippingPlanes = clippingPlanes; + expect(clippingPlanes.isDestroyed()).toBe(false); + + model1.clippingPlanes = undefined; + expect(clippingPlanes.isDestroyed()).toBe(true); + + primitives.remove(model1); + }); + }); + + it('rebuilds shaders when clipping planes change state', function() { + spyOn(Model, '_getClippingFunction').and.callThrough(); + + return loadModel(boxUrl).then(function(model) { + model.show = true; + + var clippingPlanes = new ClippingPlaneCollection({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_X, 0.0) + ], + unionClippingRegions : false + }); + model.clippingPlanes = clippingPlanes; + + scene.renderForSpecs(); + expect(Model._getClippingFunction.calls.count()).toEqual(2); + + clippingPlanes.unionClippingRegions = true; + + scene.renderForSpecs(); + expect(Model._getClippingFunction.calls.count()).toEqual(4); + + primitives.remove(model); + }); + }); + + it('rebuilds shaders when color requires coloring shader code', function() { + spyOn(Model, '_modifyShaderForColor').and.callThrough(); + + return loadModel(boxUrl).then(function(model) { + model.show = true; + + model.color = Color.LIME; + + scene.renderForSpecs(); + expect(Model._modifyShaderForColor.calls.count()).toEqual(1); + + primitives.remove(model); + }); + }); + /////////////////////////////////////////////////////////////////////////// it('Throws because of invalid extension', function() { @@ -2640,23 +2734,23 @@ defineSuite([ model.show = true; model.zoomTo(); + var gl = scene.frameState.context._gl; + spyOn(gl, 'texSubImage2D').and.callThrough(); + scene.renderForSpecs(); + var callsBeforeClipping = gl.texSubImage2D.calls.count(); - expect(model._packedClippingPlanes).toBeDefined(); - expect(model._packedClippingPlanes.length).toBe(0); expect(model._modelViewMatrix).toEqual(Matrix4.IDENTITY); model.clippingPlanes = new ClippingPlaneCollection({ planes : [ - new Plane(Cartesian3.UNIT_X, 0.0) + new ClippingPlane(Cartesian3.UNIT_X, 0.0) ] }); model.update(scene.frameState); scene.renderForSpecs(); - - expect(model._packedClippingPlanes.length).toBe(1); - expect(model._modelViewMatrix).not.toEqual(Matrix4.IDENTITY); + expect(gl.texSubImage2D.calls.count() - callsBeforeClipping * 2).toEqual(1); primitives.remove(model); }); @@ -2673,7 +2767,7 @@ defineSuite([ modelColor = rgba; }); - var plane = new Plane(Cartesian3.UNIT_X, 0.0); + var plane = new ClippingPlane(Cartesian3.UNIT_X, 0.0); model.clippingPlanes = new ClippingPlaneCollection({ planes : [ plane @@ -2706,7 +2800,7 @@ defineSuite([ modelColor = rgba; }); - var plane = new Plane(Cartesian3.UNIT_X, 0.0); + var plane = new ClippingPlane(Cartesian3.UNIT_X, 0.0); model.clippingPlanes = new ClippingPlaneCollection({ planes : [ plane @@ -2743,8 +2837,8 @@ defineSuite([ model.clippingPlanes = new ClippingPlaneCollection({ planes : [ - new Plane(Cartesian3.UNIT_Z, 5.0), - new Plane(Cartesian3.UNIT_X, 0.0) + new ClippingPlane(Cartesian3.UNIT_Z, 5.0), + new ClippingPlane(Cartesian3.UNIT_X, 0.0) ], unionClippingRegions: true }); diff --git a/Specs/Scene/PointCloud3DTileContentSpec.js b/Specs/Scene/PointCloud3DTileContentSpec.js index d7fdad687e6b..2b5b31cc6cd2 100644 --- a/Specs/Scene/PointCloud3DTileContentSpec.js +++ b/Specs/Scene/PointCloud3DTileContentSpec.js @@ -1,5 +1,6 @@ defineSuite([ 'Core/Cartesian3', + 'Core/ClippingPlane', 'Core/ClippingPlaneCollection', 'Core/Color', 'Core/ComponentDatatype', @@ -9,7 +10,6 @@ defineSuite([ 'Core/Math', 'Core/Matrix4', 'Core/PerspectiveFrustum', - 'Core/Plane', 'Core/Transforms', 'Renderer/Pass', 'Scene/Cesium3DTileStyle', @@ -20,6 +20,7 @@ defineSuite([ 'ThirdParty/when' ], 'Scene/PointCloud3DTileContent', function( Cartesian3, + ClippingPlane, ClippingPlaneCollection, Color, ComponentDatatype, @@ -29,7 +30,6 @@ defineSuite([ CesiumMath, Matrix4, PerspectiveFrustum, - Plane, Transforms, Pass, Cesium3DTileStyle, @@ -827,47 +827,47 @@ defineSuite([ }); }); - it('Updates clipping planes when clipping planes are enabled', function () { + it('Rebuilds shaders when clipping planes are enabled, change between union and intersection, or change count', function () { return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { - var content = tileset._root.content; + var tile = tileset._root; + tile._isClipped = true; + var content = tile.content; - expect(content._packedClippingPlanes).toBeDefined(); - expect(content._packedClippingPlanes.length).toBe(0); - expect(content._modelViewMatrix).toEqual(Matrix4.IDENTITY); + var noClipFS = content._drawCommand.shaderProgram._fragmentShaderText; + expect(noClipFS.includes('clip')).toBe(false); - tileset.clippingPlanes = new ClippingPlaneCollection({ + var clippingPlanes = new ClippingPlaneCollection({ planes : [ - new Plane(Cartesian3.UNIT_X, 0.0) - ] + new ClippingPlane(Cartesian3.UNIT_X, 0.0) + ], + unionClippingRegions : false }); + tileset.clippingPlanes = clippingPlanes; - 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; + clippingPlanes.update(scene.frameState); + tile.update(tileset, scene.frameState); + var clipOneIntersectFS = content._drawCommand.shaderProgram._fragmentShaderText; + expect(clipOneIntersectFS.includes('= clip(')).toBe(true); + expect(clipOneIntersectFS.includes('float clip')).toBe(true); - expect(content._packedClippingPlanes).toBeDefined(); - expect(content._packedClippingPlanes.length).toBe(0); - expect(content._modelViewMatrix).toEqual(Matrix4.IDENTITY); + clippingPlanes.unionClippingRegions = true; - tileset.clippingPlanes = new ClippingPlaneCollection({ - planes : [ - new Plane(Cartesian3.UNIT_X, 0.0) - ] - }); + clippingPlanes.update(scene.frameState); + tile.update(tileset, scene.frameState); + var clipOneUnionFS = content._drawCommand.shaderProgram._fragmentShaderText; + expect(clipOneUnionFS.includes('= clip(')).toBe(true); + expect(clipOneUnionFS.includes('float clip')).toBe(true); + expect(clipOneUnionFS).not.toEqual(clipOneIntersectFS); - content.update(tileset, scene.frameState); + clippingPlanes.add(new ClippingPlane(Cartesian3.UNIT_Y, 1.0)); - expect(content._packedClippingPlanes.length).toBe(0); - expect(content._modelViewMatrix).toEqual(Matrix4.IDENTITY); + clippingPlanes.update(scene.frameState); + tile.update(tileset, scene.frameState); + var clipTwoUnionFS = content._drawCommand.shaderProgram._fragmentShaderText; + expect(clipTwoUnionFS.includes('= clip(')).toBe(true); + expect(clipTwoUnionFS.includes('float clip')).toBe(true); + expect(clipTwoUnionFS).not.toEqual(clipOneIntersectFS); + expect(clipTwoUnionFS).not.toEqual(clipOneUnionFS); }); }); @@ -878,7 +878,7 @@ defineSuite([ color = rgba; }); - var clipPlane = new Plane(Cartesian3.UNIT_Z, -10.0); + var clipPlane = new ClippingPlane(Cartesian3.UNIT_Z, -10.0); tileset.clippingPlanes = new ClippingPlaneCollection({ planes : [ clipPlane @@ -901,7 +901,7 @@ defineSuite([ color = rgba; }); - var clipPlane = new Plane(Cartesian3.UNIT_Z, -10.0); + var clipPlane = new ClippingPlane(Cartesian3.UNIT_Z, -10.0); tileset.clippingPlanes = new ClippingPlaneCollection ({ planes : [ clipPlane @@ -915,7 +915,38 @@ defineSuite([ }); }); - it('clipping planes union regions', function() { + it('clipping planes union regions (Uint8)', function() { + // Force uint8 mode - there's a slight rendering difference between + // float and packed uint8 clipping planes for this test due to the small context + spyOn(ClippingPlaneCollection, 'useFloatTexture').and.returnValue(false); + return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { + var color; + expect(scene).toRenderAndCall(function(rgba) { + color = rgba; + }); + + tileset.clippingPlanes = new ClippingPlaneCollection ({ + planes : [ + new ClippingPlane(Cartesian3.UNIT_Z, 0.0), + new ClippingPlane(Cartesian3.UNIT_X, 0.0) + ], + modelMatrix : Transforms.eastNorthUpToFixedFrame(tileset.boundingSphere.center), + unionClippingRegions: true + }); + + expect(scene).notToRender(color); + + tileset.clippingPlanes.unionClippingRegions = false; + + expect(scene).toRender(color); + }); + }); + + it('clipping planes union regions (Float)', function() { + if (!ClippingPlaneCollection.useFloatTexture(scene._context)) { + // This configuration for the test fails in uint8 mode due to the small context + return; + } return Cesium3DTilesTester.loadTileset(scene, pointCloudRGBUrl).then(function(tileset) { var color; expect(scene).toRenderAndCall(function(rgba) { @@ -924,8 +955,8 @@ defineSuite([ tileset.clippingPlanes = new ClippingPlaneCollection ({ planes : [ - new Plane(Cartesian3.UNIT_Z, -10.0), - new Plane(Cartesian3.UNIT_X, 0.0) + new ClippingPlane(Cartesian3.UNIT_Z, -10.0), + new ClippingPlane(Cartesian3.UNIT_X, 0.0) ], modelMatrix : Transforms.eastNorthUpToFixedFrame(tileset.boundingSphere.center), unionClippingRegions: true