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