/*global define*/
define([
'../Core/BoundingSphere',
'../Core/buildModuleUrl',
'../Core/Cartesian3',
'../Core/Cartographic',
'../Core/defaultValue',
'../Core/defined',
'../Core/defineProperties',
'../Core/destroyObject',
'../Core/DeveloperError',
'../Core/Ellipsoid',
'../Core/EllipsoidTerrainProvider',
'../Core/Event',
'../Core/GeographicProjection',
'../Core/IntersectionTests',
'../Core/loadImage',
'../Core/Ray',
'../Core/Rectangle',
'../Renderer/ShaderSource',
'../Renderer/Texture',
'../Shaders/GlobeFS',
'../Shaders/GlobeVS',
'../Shaders/GroundAtmosphere',
'../ThirdParty/when',
'./GlobeSurfaceShaderSet',
'./GlobeSurfaceTileProvider',
'./ImageryLayerCollection',
'./QuadtreePrimitive',
'./SceneMode',
'./ShadowMode'
], function(
BoundingSphere,
buildModuleUrl,
Cartesian3,
Cartographic,
defaultValue,
defined,
defineProperties,
destroyObject,
DeveloperError,
Ellipsoid,
EllipsoidTerrainProvider,
Event,
GeographicProjection,
IntersectionTests,
loadImage,
Ray,
Rectangle,
ShaderSource,
Texture,
GlobeFS,
GlobeVS,
GroundAtmosphere,
when,
GlobeSurfaceShaderSet,
GlobeSurfaceTileProvider,
ImageryLayerCollection,
QuadtreePrimitive,
SceneMode,
ShadowMode) {
'use strict';
/**
* The globe rendered in the scene, including its terrain ({@link Globe#terrainProvider})
* and imagery layers ({@link Globe#imageryLayers}). Access the globe using {@link Scene#globe}.
*
* @alias Globe
* @constructor
*
* @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] Determines the size and shape of the
* globe.
*/
function Globe(ellipsoid) {
ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84);
var terrainProvider = new EllipsoidTerrainProvider({
ellipsoid : ellipsoid
});
var imageryLayerCollection = new ImageryLayerCollection();
this._ellipsoid = ellipsoid;
this._imageryLayerCollection = imageryLayerCollection;
this._surfaceShaderSet = new GlobeSurfaceShaderSet();
this._surfaceShaderSet.baseVertexShaderSource = new ShaderSource({
sources : [GroundAtmosphere, GlobeVS]
});
this._surfaceShaderSet.baseFragmentShaderSource = new ShaderSource({
sources : [GlobeFS]
});
this._surface = new QuadtreePrimitive({
tileProvider : new GlobeSurfaceTileProvider({
terrainProvider : terrainProvider,
imageryLayers : imageryLayerCollection,
surfaceShaderSet : this._surfaceShaderSet
})
});
this._terrainProvider = terrainProvider;
this._terrainProviderChanged = new Event();
/**
* Determines if the globe will be shown.
*
* @type {Boolean}
* @default true
*/
this.show = true;
/**
* The normal map to use for rendering waves in the ocean. Setting this property will
* only have an effect if the configured terrain provider includes a water mask.
*
* @type {String}
* @default buildModuleUrl('Assets/Textures/waterNormalsSmall.jpg')
*/
this.oceanNormalMapUrl = buildModuleUrl('Assets/Textures/waterNormalsSmall.jpg');
this._oceanNormalMapUrl = undefined;
/**
* The maximum screen-space error used to drive level-of-detail refinement. Higher
* values will provide better performance but lower visual quality.
*
* @type {Number}
* @default 2
*/
this.maximumScreenSpaceError = 2;
/**
* The size of the terrain tile cache, expressed as a number of tiles. Any additional
* tiles beyond this number will be freed, as long as they aren't needed for rendering
* this frame. A larger number will consume more memory but will show detail faster
* when, for example, zooming out and then back in.
*
* @type {Number}
* @default 100
*/
this.tileCacheSize = 100;
/**
* Enable lighting the globe with the sun as a light source.
*
* @type {Boolean}
* @default false
*/
this.enableLighting = false;
/**
* The distance where everything becomes lit. This only takes effect
* when <code>enableLighting</code> is <code>true</code>.
*
* @type {Number}
* @default 6500000.0
*/
this.lightingFadeOutDistance = 6500000.0;
/**
* The distance where lighting resumes. This only takes effect
* when <code>enableLighting</code> is <code>true</code>.
*
* @type {Number}
* @default 9000000.0
*/
this.lightingFadeInDistance = 9000000.0;
/**
* True if an animated wave effect should be shown in areas of the globe
* covered by water; otherwise, false. This property is ignored if the
* <code>terrainProvider</code> does not provide a water mask.
*
* @type {Boolean}
* @default true
*/
this.showWaterEffect = true;
/**
* True if primitives such as billboards, polylines, labels, etc. should be depth-tested
* against the terrain surface, or false if such primitives should always be drawn on top
* of terrain unless they're on the opposite side of the globe. The disadvantage of depth
* testing primitives against terrain is that slight numerical noise or terrain level-of-detail
* switched can sometimes make a primitive that should be on the surface disappear underneath it.
*
* @type {Boolean}
* @default false
*
*/
this.depthTestAgainstTerrain = false;
/**
* Determines whether the globe casts or receives shadows from each light source. Setting the globe
* to cast shadows may impact performance since the terrain is rendered again from the light's perspective.
* Currently only terrain that is in view casts shadows. By default the globe does not cast shadows.
*
* @type {ShadowMode}
* @default ShadowMode.RECEIVE_ONLY
*/
this.shadows = ShadowMode.RECEIVE_ONLY;
this._oceanNormalMap = undefined;
this._zoomedOutOceanSpecularIntensity = 0.5;
}
defineProperties(Globe.prototype, {
/**
* Gets an ellipsoid describing the shape of this globe.
* @memberof Globe.prototype
* @type {Ellipsoid}
*/
ellipsoid : {
get : function() {
return this._ellipsoid;
}
},
/**
* Gets the collection of image layers that will be rendered on this globe.
* @memberof Globe.prototype
* @type {ImageryLayerCollection}
*/
imageryLayers : {
get : function() {
return this._imageryLayerCollection;
}
},
/**
* Gets or sets the color of the globe when no imagery is available.
* @memberof Globe.prototype
* @type {Color}
*/
baseColor : {
get : function() {
return this._surface.tileProvider.baseColor;
},
set : function(value) {
this._surface.tileProvider.baseColor = value;
}
},
/**
* The terrain provider providing surface geometry for this globe.
* @type {TerrainProvider}
*
* @memberof Globe.prototype
* @type {TerrainProvider}
*
*/
terrainProvider : {
get : function() {
return this._terrainProvider;
},
set : function(value) {
if (value !== this._terrainProvider) {
this._terrainProvider = value;
this._terrainProviderChanged.raiseEvent(value);
}
}
},
/**
* Gets an event that's raised when the terrain provider is changed
*
* @memberof Globe.prototype
* @type {Event}
* @readonly
*/
terrainProviderChanged : {
get: function() {
return this._terrainProviderChanged;
}
},
/**
* Gets an event that's raised when the length of the tile load queue has changed since the last render frame. When the load queue is empty,
* all terrain and imagery for the current view have been loaded. The event passes the new length of the tile load queue.
*
* @memberof Globe.prototype
* @type {Event}
*/
tileLoadProgressEvent : {
get: function() {
return this._surface.tileLoadProgressEvent;
}
}
});
function createComparePickTileFunction(rayOrigin) {
return function(a, b) {
var aDist = BoundingSphere.distanceSquaredTo(a.pickBoundingSphere, rayOrigin);
var bDist = BoundingSphere.distanceSquaredTo(b.pickBoundingSphere, rayOrigin);
return aDist - bDist;
};
}
var scratchArray = [];
var scratchSphereIntersectionResult = {
start : 0.0,
stop : 0.0
};
/**
* Find an intersection between a ray and the globe surface that was rendered. The ray must be given in world coordinates.
*
* @param {Ray} ray The ray to test for intersection.
* @param {Scene} scene The scene.
* @param {Cartesian3} [result] The object onto which to store the result.
* @returns {Cartesian3|undefined} The intersection or <code>undefined</code> if none was found.
*
* @example
* // find intersection of ray through a pixel and the globe
* var ray = viewer.camera.getPickRay(windowCoordinates);
* var intersection = globe.pick(ray, scene);
*/
Globe.prototype.pick = function(ray, scene, result) {
//>>includeStart('debug', pragmas.debug);
if (!defined(ray)) {
throw new DeveloperError('ray is required');
}
if (!defined(scene)) {
throw new DeveloperError('scene is required');
}
//>>includeEnd('debug');
var mode = scene.mode;
var projection = scene.mapProjection;
var sphereIntersections = scratchArray;
sphereIntersections.length = 0;
var tilesToRender = this._surface._tilesToRender;
var length = tilesToRender.length;
var tile;
var i;
for (i = 0; i < length; ++i) {
tile = tilesToRender[i];
var tileData = tile.data;
if (!defined(tileData)) {
continue;
}
var boundingVolume = tileData.pickBoundingSphere;
if (mode !== SceneMode.SCENE3D) {
BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, projection, tileData.minimumHeight, tileData.maximumHeight, boundingVolume);
Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center);
} else {
BoundingSphere.clone(tileData.boundingSphere3D, boundingVolume);
}
var boundingSphereIntersection = IntersectionTests.raySphere(ray, boundingVolume, scratchSphereIntersectionResult);
if (defined(boundingSphereIntersection)) {
sphereIntersections.push(tileData);
}
}
sphereIntersections.sort(createComparePickTileFunction(ray.origin));
var intersection;
length = sphereIntersections.length;
for (i = 0; i < length; ++i) {
intersection = sphereIntersections[i].pick(ray, scene.mode, scene.mapProjection, true, result);
if (defined(intersection)) {
break;
}
}
return intersection;
};
var scratchGetHeightCartesian = new Cartesian3();
var scratchGetHeightIntersection = new Cartesian3();
var scratchGetHeightCartographic = new Cartographic();
var scratchGetHeightRay = new Ray();
/**
* Get the height of the surface at a given cartographic.
*
* @param {Cartographic} cartographic The cartographic for which to find the height.
* @returns {Number|undefined} The height of the cartographic or undefined if it could not be found.
*/
Globe.prototype.getHeight = function(cartographic) {
//>>includeStart('debug', pragmas.debug);
if (!defined(cartographic)) {
throw new DeveloperError('cartographic is required');
}
//>>includeEnd('debug');
var levelZeroTiles = this._surface._levelZeroTiles;
if (!defined(levelZeroTiles)) {
return;
}
var tile;
var i;
var length = levelZeroTiles.length;
for (i = 0; i < length; ++i) {
tile = levelZeroTiles[i];
if (Rectangle.contains(tile.rectangle, cartographic)) {
break;
}
}
if (!defined(tile) || !Rectangle.contains(tile.rectangle, cartographic)) {
return undefined;
}
while (tile.renderable) {
var children = tile.children;
length = children.length;
for (i = 0; i < length; ++i) {
tile = children[i];
if (Rectangle.contains(tile.rectangle, cartographic)) {
break;
}
}
}
while (defined(tile) && (!defined(tile.data) || !defined(tile.data.pickTerrain))) {
tile = tile.parent;
}
if (!defined(tile)) {
return undefined;
}
var ellipsoid = this._surface._tileProvider.tilingScheme.ellipsoid;
var cartesian = ellipsoid.cartographicToCartesian(cartographic, scratchGetHeightCartesian);
var ray = scratchGetHeightRay;
Cartesian3.normalize(cartesian, ray.direction);
var intersection = tile.data.pick(ray, undefined, undefined, false, scratchGetHeightIntersection);
if (!defined(intersection)) {
return undefined;
}
return ellipsoid.cartesianToCartographic(intersection, scratchGetHeightCartographic).height;
};
/**
* @private
*/
Globe.prototype.beginFrame = function(frameState) {
if (!this.show) {
return;
}
var surface = this._surface;
var tileProvider = surface.tileProvider;
var terrainProvider = this.terrainProvider;
var hasWaterMask = this.showWaterEffect && terrainProvider.ready && terrainProvider.hasWaterMask;
if (hasWaterMask && this.oceanNormalMapUrl !== this._oceanNormalMapUrl) {
// url changed, load new normal map asynchronously
var oceanNormalMapUrl = this.oceanNormalMapUrl;
this._oceanNormalMapUrl = oceanNormalMapUrl;
if (defined(oceanNormalMapUrl)) {
var that = this;
when(loadImage(oceanNormalMapUrl), function(image) {
if (oceanNormalMapUrl !== that.oceanNormalMapUrl) {
// url changed while we were loading
return;
}
that._oceanNormalMap = that._oceanNormalMap && that._oceanNormalMap.destroy();
that._oceanNormalMap = new Texture({
context : frameState.context,
source : image
});
});
} else {
this._oceanNormalMap = this._oceanNormalMap && this._oceanNormalMap.destroy();
}
}
var mode = frameState.mode;
var pass = frameState.passes;
if (pass.render) {
// Don't show the ocean specular highlights when zoomed out in 2D and Columbus View.
if (mode === SceneMode.SCENE3D) {
this._zoomedOutOceanSpecularIntensity = 0.5;
} else {
this._zoomedOutOceanSpecularIntensity = 0.0;
}
surface.maximumScreenSpaceError = this.maximumScreenSpaceError;
surface.tileCacheSize = this.tileCacheSize;
tileProvider.terrainProvider = this.terrainProvider;
tileProvider.lightingFadeOutDistance = this.lightingFadeOutDistance;
tileProvider.lightingFadeInDistance = this.lightingFadeInDistance;
tileProvider.zoomedOutOceanSpecularIntensity = this._zoomedOutOceanSpecularIntensity;
tileProvider.hasWaterMask = hasWaterMask;
tileProvider.oceanNormalMap = this._oceanNormalMap;
tileProvider.enableLighting = this.enableLighting;
tileProvider.shadows = this.shadows;
surface.beginFrame(frameState);
}
};
/**
* @private
*/
Globe.prototype.update = function(frameState) {
if (!this.show) {
return;
}
var surface = this._surface;
var pass = frameState.passes;
if (pass.render) {
surface.update(frameState);
}
if (pass.pick) {
surface.update(frameState);
}
};
/**
* @private
*/
Globe.prototype.endFrame = function(frameState) {
if (!this.show) {
return;
}
if (frameState.passes.render) {
this._surface.endFrame(frameState);
}
};
/**
* Returns true if this object was destroyed; otherwise, false.
* <br /><br />
* If this object was destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
*
* @returns {Boolean} True if this object was destroyed; otherwise, false.
*
* @see Globe#destroy
*/
Globe.prototype.isDestroyed = function() {
return false;
};
/**
* Destroys the WebGL resources held by this object. Destroying an object allows for deterministic
* release of WebGL resources, instead of relying on the garbage collector to destroy this object.
* <br /><br />
* Once an object is destroyed, it should not be used; calling any function other than
* <code>isDestroyed</code> will result in a {@link DeveloperError} exception. Therefore,
* assign the return value (<code>undefined</code>) to the object as done in the example.
*
* @returns {undefined}
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*
*
* @example
* globe = globe && globe.destroy();
*
* @see Globe#isDestroyed
*/
Globe.prototype.destroy = function() {
this._surfaceShaderSet = this._surfaceShaderSet && this._surfaceShaderSet.destroy();
this._surface = this._surface && this._surface.destroy();
this._oceanNormalMap = this._oceanNormalMap && this._oceanNormalMap.destroy();
return destroyObject(this);
};
return Globe;
});