define([
        '../Core/BoundingSphere',
        '../Core/BoxOutlineGeometry',
        '../Core/Cartesian2',
        '../Core/Cartesian3',
        '../Core/Cartesian4',
        '../Core/Cartographic',
        '../Core/Color',
        '../Core/ColorGeometryInstanceAttribute',
        '../Core/combine',
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/destroyObject',
        '../Core/DeveloperError',
        '../Core/Event',
        '../Core/GeometryInstance',
        '../Core/GeometryPipeline',
        '../Core/IndexDatatype',
        '../Core/Intersect',
        '../Core/Math',
        '../Core/Matrix4',
        '../Core/OrientedBoundingBox',
        '../Core/OrthographicFrustum',
        '../Core/PrimitiveType',
        '../Core/Rectangle',
        '../Core/SphereOutlineGeometry',
        '../Core/TerrainMesh',
        '../Core/TerrainQuantization',
        '../Core/Visibility',
        '../Core/WebMercatorProjection',
        '../Renderer/Buffer',
        '../Renderer/BufferUsage',
        '../Renderer/ContextLimits',
        '../Renderer/DrawCommand',
        '../Renderer/Pass',
        '../Renderer/RenderState',
        '../Renderer/VertexArray',
        './BlendingState',
        './ImageryState',
        './TileBoundingRegion',
        './TileSelectionResult',
        './ClippingPlaneCollection',
        './DepthFunction',
        './GlobeSurfaceTile',
        './ImageryLayer',
        './PerInstanceColorAppearance',
        './Primitive',
        './QuadtreeTileLoadState',
        './SceneMode',
        './ShadowMode',
        './TerrainFillMesh',
        './TerrainState'
    ], function(
        BoundingSphere,
        BoxOutlineGeometry,
        Cartesian2,
        Cartesian3,
        Cartesian4,
        Cartographic,
        Color,
        ColorGeometryInstanceAttribute,
        combine,
        defaultValue,
        defined,
        defineProperties,
        destroyObject,
        DeveloperError,
        Event,
        GeometryInstance,
        GeometryPipeline,
        IndexDatatype,
        Intersect,
        CesiumMath,
        Matrix4,
        OrientedBoundingBox,
        OrthographicFrustum,
        PrimitiveType,
        Rectangle,
        SphereOutlineGeometry,
        TerrainMesh,
        TerrainQuantization,
        Visibility,
        WebMercatorProjection,
        Buffer,
        BufferUsage,
        ContextLimits,
        DrawCommand,
        Pass,
        RenderState,
        VertexArray,
        BlendingState,
        ImageryState,
        TileBoundingRegion,
        TileSelectionResult,
        ClippingPlaneCollection,
        DepthFunction,
        GlobeSurfaceTile,
        ImageryLayer,
        PerInstanceColorAppearance,
        Primitive,
        QuadtreeTileLoadState,
        SceneMode,
        ShadowMode,
        TerrainFillMesh,
        TerrainState) {
    'use strict';

    /**
     * Provides quadtree tiles representing the surface of the globe.  This type is intended to be used
     * with {@link QuadtreePrimitive}.
     *
     * @alias GlobeSurfaceTileProvider
     * @constructor
     *
     * @param {TerrainProvider} options.terrainProvider The terrain provider that describes the surface geometry.
     * @param {ImageryLayerCollection} option.imageryLayers The collection of imagery layers describing the shading of the surface.
     * @param {GlobeSurfaceShaderSet} options.surfaceShaderSet The set of shaders used to render the surface.
     *
     * @private
     */
    function GlobeSurfaceTileProvider(options) {
        //>>includeStart('debug', pragmas.debug);
        if (!defined(options)) {
            throw new DeveloperError('options is required.');
        }
        if (!defined(options.terrainProvider)) {
            throw new DeveloperError('options.terrainProvider is required.');
        } else if (!defined(options.imageryLayers)) {
            throw new DeveloperError('options.imageryLayers is required.');
        } else if (!defined(options.surfaceShaderSet)) {
            throw new DeveloperError('options.surfaceShaderSet is required.');
        }
        //>>includeEnd('debug');

        this.lightingFadeOutDistance = 6500000.0;
        this.lightingFadeInDistance = 9000000.0;
        this.hasWaterMask = false;
        this.oceanNormalMap = undefined;
        this.zoomedOutOceanSpecularIntensity = 0.5;
        this.enableLighting = false;
        this.showGroundAtmosphere = false;
        this.shadows = ShadowMode.RECEIVE_ONLY;

        /**
         * The color to use to highlight terrain fill tiles. If undefined, fill tiles are not
         * highlighted at all. The alpha value is used to alpha blend with the tile's
         * actual color. Because terrain fill tiles do not represent the actual terrain surface,
         * it may be useful in some applications to indicate visually that they are not to be trusted.
         * @type {Color}
         * @default undefined
         */
        this.fillHighlightColor = undefined;

        this.hueShift = 0.0;
        this.saturationShift = 0.0;
        this.brightnessShift = 0.0;

        this._quadtree = undefined;
        this._terrainProvider = options.terrainProvider;
        this._imageryLayers = options.imageryLayers;
        this._surfaceShaderSet = options.surfaceShaderSet;

        this._renderState = undefined;
        this._blendRenderState = undefined;

        this._errorEvent = new Event();

        this._imageryLayers.layerAdded.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerAdded, this);
        this._imageryLayers.layerRemoved.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerRemoved, this);
        this._imageryLayers.layerMoved.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerMoved, this);
        this._imageryLayers.layerShownOrHidden.addEventListener(GlobeSurfaceTileProvider.prototype._onLayerShownOrHidden, this);
        this._imageryLayersUpdatedEvent = new Event();

        this._layerOrderChanged = false;

        this._tilesToRenderByTextureCount = [];
        this._drawCommands = [];
        this._uniformMaps = [];
        this._usedDrawCommands = 0;

        this._vertexArraysToDestroy = [];

        this._debug = {
            wireframe : false,
            boundingSphereTile : undefined
        };

        this._baseColor = undefined;
        this._firstPassInitialColor = undefined;
        this.baseColor = new Color(0.0, 0.0, 0.5, 1.0);

        /**
         * A property specifying a {@link ClippingPlaneCollection} used to selectively disable rendering on the outside of each plane.
         * @type {ClippingPlaneCollection}
         * @private
         */
        this._clippingPlanes = undefined;

        /**
         * A property specifying a {@link Rectangle} used to selectively limit terrain and imagery rendering.
         * @type {Rectangle}
         */
        this.cartographicLimitRectangle = Rectangle.clone(Rectangle.MAX_VALUE);

        this._hasLoadedTilesThisFrame = false;
        this._hasFillTilesThisFrame = false;
    }

    defineProperties(GlobeSurfaceTileProvider.prototype, {
        /**
         * Gets or sets the color of the globe when no imagery is available.
         * @memberof GlobeSurfaceTileProvider.prototype
         * @type {Color}
         */
        baseColor : {
            get : function() {
                return this._baseColor;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                this._baseColor = value;
                this._firstPassInitialColor = Cartesian4.fromColor(value, this._firstPassInitialColor);
            }
        },
        /**
         * Gets or sets the {@link QuadtreePrimitive} for which this provider is
         * providing tiles.  This property may be undefined if the provider is not yet associated
         * with a {@link QuadtreePrimitive}.
         * @memberof GlobeSurfaceTileProvider.prototype
         * @type {QuadtreePrimitive}
         */
        quadtree : {
            get : function() {
                return this._quadtree;
            },
            set : function(value) {
                //>>includeStart('debug', pragmas.debug);
                if (!defined(value)) {
                    throw new DeveloperError('value is required.');
                }
                //>>includeEnd('debug');

                this._quadtree = value;
            }
        },

        /**
         * Gets a value indicating whether or not the provider is ready for use.
         * @memberof GlobeSurfaceTileProvider.prototype
         * @type {Boolean}
         */
        ready : {
            get : function() {
                return this._terrainProvider.ready && (this._imageryLayers.length === 0 || this._imageryLayers.get(0).imageryProvider.ready);
            }
        },

        /**
         * Gets the tiling scheme used by the provider.  This property should
         * not be accessed before {@link GlobeSurfaceTileProvider#ready} returns true.
         * @memberof GlobeSurfaceTileProvider.prototype
         * @type {TilingScheme}
         */
        tilingScheme : {
            get : function() {
                return this._terrainProvider.tilingScheme;
            }
        },

        /**
         * Gets an event that is raised when the geometry provider encounters an asynchronous error.  By subscribing
         * to the event, you will be notified of the error and can potentially recover from it.  Event listeners
         * are passed an instance of {@link TileProviderError}.
         * @memberof GlobeSurfaceTileProvider.prototype
         * @type {Event}
         */
        errorEvent : {
            get : function() {
                return this._errorEvent;
            }
        },

        /**
         * Gets an event that is raised when an imagery layer is added, shown, hidden, moved, or removed.
         * @memberof GlobeSurfaceTileProvider.prototype
         * @type {Event}
         */
        imageryLayersUpdatedEvent : {
            get : function() {
                return this._imageryLayersUpdatedEvent;
            }
        },

        /**
         * Gets or sets the terrain provider that describes the surface geometry.
         * @memberof GlobeSurfaceTileProvider.prototype
         * @type {TerrainProvider}
         */
        terrainProvider : {
            get : function() {
                return this._terrainProvider;
            },
            set : function(terrainProvider) {
                if (this._terrainProvider === terrainProvider) {
                    return;
                }

                //>>includeStart('debug', pragmas.debug);
                if (!defined(terrainProvider)) {
                    throw new DeveloperError('terrainProvider is required.');
                }
                //>>includeEnd('debug');

                this._terrainProvider = terrainProvider;

                if (defined(this._quadtree)) {
                    this._quadtree.invalidateAllTiles();
                }
            }
        },
        /**
         * The {@link ClippingPlaneCollection} used to selectively disable rendering the tileset.
         *
         * @type {ClippingPlaneCollection}
         *
         * @private
         */
        clippingPlanes : {
            get : function() {
                return this._clippingPlanes;
            },
            set : function(value) {
                ClippingPlaneCollection.setOwner(value, this, '_clippingPlanes');
            }
        }
    });

    function sortTileImageryByLayerIndex(a, b) {
        var aImagery = a.loadingImagery;
        if (!defined(aImagery)) {
            aImagery = a.readyImagery;
        }

        var bImagery = b.loadingImagery;
        if (!defined(bImagery)) {
            bImagery = b.readyImagery;
        }

        return aImagery.imageryLayer._layerIndex - bImagery.imageryLayer._layerIndex;
    }

     /**
     * Make updates to the tile provider that are not involved in rendering. Called before the render update cycle.
     */
    GlobeSurfaceTileProvider.prototype.update = function(frameState) {
        // update collection: imagery indices, base layers, raise layer show/hide event
        this._imageryLayers._update();
    };

    function updateCredits(surface, frameState) {
        var creditDisplay = frameState.creditDisplay;
        if (surface._terrainProvider.ready && defined(surface._terrainProvider.credit)) {
            creditDisplay.addCredit(surface._terrainProvider.credit);
        }

        var imageryLayers = surface._imageryLayers;
        for (var i = 0, len = imageryLayers.length; i < len; ++i) {
            var imageryProvider = imageryLayers.get(i).imageryProvider;
            if (imageryProvider.ready && defined(imageryProvider.credit)) {
                creditDisplay.addCredit(imageryProvider.credit);
            }
        }
    }

    /**
     * Called at the beginning of each render frame, before {@link QuadtreeTileProvider#showTileThisFrame}
     * @param {FrameState} frameState The frame state.
     */
    GlobeSurfaceTileProvider.prototype.initialize = function(frameState) {
        // update each layer for texture reprojection.
        this._imageryLayers.queueReprojectionCommands(frameState);

        if (this._layerOrderChanged) {
            this._layerOrderChanged = false;

            // Sort the TileImagery instances in each tile by the layer index.
            this._quadtree.forEachLoadedTile(function(tile) {
                tile.data.imagery.sort(sortTileImageryByLayerIndex);
            });
        }

        // Add credits for terrain and imagery providers.
        updateCredits(this, frameState);

        var vertexArraysToDestroy = this._vertexArraysToDestroy;
        var length = vertexArraysToDestroy.length;
        for (var j = 0; j < length; ++j) {
            GlobeSurfaceTile._freeVertexArray(vertexArraysToDestroy[j]);
        }
        vertexArraysToDestroy.length = 0;
    };

    /**
     * Called at the beginning of the update cycle for each render frame, before {@link QuadtreeTileProvider#showTileThisFrame}
     * or any other functions.
     *
     * @param {FrameState} frameState The frame state.
     */
    GlobeSurfaceTileProvider.prototype.beginUpdate = function(frameState) {
        var tilesToRenderByTextureCount = this._tilesToRenderByTextureCount;
        for (var i = 0, len = tilesToRenderByTextureCount.length; i < len; ++i) {
            var tiles = tilesToRenderByTextureCount[i];
            if (defined(tiles)) {
                tiles.length = 0;
            }
        }
        // update clipping planes
        var clippingPlanes = this._clippingPlanes;
        if (defined(clippingPlanes) && clippingPlanes.enabled) {
            clippingPlanes.update(frameState);
        }
        this._usedDrawCommands = 0;

        this._hasLoadedTilesThisFrame = false;
        this._hasFillTilesThisFrame = false;
    };

    /**
     * Called at the end of the update cycle for each render frame, after {@link QuadtreeTileProvider#showTileThisFrame}
     * and any other functions.
     *
     * @param {FrameState} frameState The frame state.
     */
    GlobeSurfaceTileProvider.prototype.endUpdate = function(frameState) {
        if (!defined(this._renderState)) {
            this._renderState = RenderState.fromCache({ // Write color and depth
                cull : {
                    enabled : true
                },
                depthTest : {
                    enabled : true,
                    func : DepthFunction.LESS
                }
            });

            this._blendRenderState = RenderState.fromCache({ // Write color and depth
                cull : {
                    enabled : true
                },
                depthTest : {
                    enabled : true,
                    func : DepthFunction.LESS_OR_EQUAL
                },
                blending : BlendingState.ALPHA_BLEND
            });
        }

        // If this frame has a mix of loaded and fill tiles, we need to propagate
        // loaded heights to the fill tiles.
        if (this._hasFillTilesThisFrame && this._hasLoadedTilesThisFrame) {
            TerrainFillMesh.updateFillTiles(this, this._quadtree._tilesToRender, frameState, this._vertexArraysToDestroy);
        }

        // Add the tile render commands to the command list, sorted by texture count.
        var tilesToRenderByTextureCount = this._tilesToRenderByTextureCount;
        for (var textureCountIndex = 0, textureCountLength = tilesToRenderByTextureCount.length; textureCountIndex < textureCountLength; ++textureCountIndex) {
            var tilesToRender = tilesToRenderByTextureCount[textureCountIndex];
            if (!defined(tilesToRender)) {
                continue;
            }

            for (var tileIndex = 0, tileLength = tilesToRender.length; tileIndex < tileLength; ++tileIndex) {
                addDrawCommandsForTile(this, tilesToRender[tileIndex], frameState);
            }
        }
    };

    /**
     * Adds draw commands for tiles rendered in the previous frame for a pick pass.
     *
     * @param {FrameState} frameState The frame state.
     */
    GlobeSurfaceTileProvider.prototype.updateForPick = function(frameState) {
        // Add the tile pick commands from the tiles drawn last frame.
        var drawCommands = this._drawCommands;
        for (var i = 0, length = this._usedDrawCommands; i < length; ++i) {
            frameState.commandList.push(drawCommands[i]);
        }
    };

    /**
     * Cancels any imagery re-projections in the queue.
     */
    GlobeSurfaceTileProvider.prototype.cancelReprojections = function() {
        this._imageryLayers.cancelReprojections();
    };

    /**
     * Gets the maximum geometric error allowed in a tile at a given level, in meters.  This function should not be
     * called before {@link GlobeSurfaceTileProvider#ready} returns true.
     *
     * @param {Number} level The tile level for which to get the maximum geometric error.
     * @returns {Number} The maximum geometric error in meters.
     */
    GlobeSurfaceTileProvider.prototype.getLevelMaximumGeometricError = function(level) {
        return this._terrainProvider.getLevelMaximumGeometricError(level);
    };

    /**
     * Loads, or continues loading, a given tile.  This function will continue to be called
     * until {@link QuadtreeTile#state} is no longer {@link QuadtreeTileLoadState#LOADING}.  This function should
     * not be called before {@link GlobeSurfaceTileProvider#ready} returns true.
     *
     * @param {FrameState} frameState The frame state.
     * @param {QuadtreeTile} tile The tile to load.
     *
     * @exception {DeveloperError} <code>loadTile</code> must not be called before the tile provider is ready.
     */
    GlobeSurfaceTileProvider.prototype.loadTile = function(frameState, tile) {
        // We don't want to load imagery until we're certain that the terrain tiles are actually visible.
        // So if our bounding volume isn't accurate because it came from another tile, load terrain only
        // initially. If we load some terrain and suddenly have a more accurate bounding volume and the
        // tile is _still_ visible, give the tile a chance to load imagery immediately rather than
        // waiting for next frame.

        var surfaceTile = tile.data;
        var terrainOnly = true;
        var terrainStateBefore;
        if (defined(surfaceTile)) {
            terrainOnly = surfaceTile.boundingVolumeSourceTile !== tile || tile._lastSelectionResult === TileSelectionResult.CULLED_BUT_NEEDED;
            terrainStateBefore = surfaceTile.terrainState;
        }

        GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly);

        surfaceTile = tile.data;
        if (terrainOnly && terrainStateBefore !== tile.data.terrainState) {
            // Terrain state changed. If:
            // a) The tile is visible, and
            // b) The bounding volume is accurate (updated as a side effect of computing visibility)
            // Then we'll load imagery, too.
            if (this.computeTileVisibility(tile, frameState, this.quadtree.occluders) && surfaceTile.boundingVolumeSourceTile === tile) {
                terrainOnly = false;
                GlobeSurfaceTile.processStateMachine(tile, frameState, this.terrainProvider, this._imageryLayers, this._vertexArraysToDestroy, terrainOnly);
            }
        }
    };

    var boundingSphereScratch = new BoundingSphere();
    var rectangleIntersectionScratch = new Rectangle();
    var splitCartographicLimitRectangleScratch = new Rectangle();
    var rectangleCenterScratch = new Cartographic();

    // cartographicLimitRectangle may span the IDL, but tiles never will.
    function clipRectangleAntimeridian(tileRectangle, cartographicLimitRectangle) {
        if (cartographicLimitRectangle.west < cartographicLimitRectangle.east) {
            return cartographicLimitRectangle;
        }
        var splitRectangle = Rectangle.clone(cartographicLimitRectangle, splitCartographicLimitRectangleScratch);
        var tileCenter = Rectangle.center(tileRectangle, rectangleCenterScratch);
        if (tileCenter.longitude > 0.0) {
            splitRectangle.east = CesiumMath.PI;
        } else {
            splitRectangle.west = -CesiumMath.PI;
        }
        return splitRectangle;
    }

    /**
     * Determines the visibility of a given tile.  The tile may be fully visible, partially visible, or not
     * visible at all.  Tiles that are renderable and are at least partially visible will be shown by a call
     * to {@link GlobeSurfaceTileProvider#showTileThisFrame}.
     *
     * @param {QuadtreeTile} tile The tile instance.
     * @param {FrameState} frameState The state information about the current frame.
     * @param {QuadtreeOccluders} occluders The objects that may occlude this tile.
     *
     * @returns {Visibility} The visibility of the tile.
     */
    GlobeSurfaceTileProvider.prototype.computeTileVisibility = function(tile, frameState, occluders) {
        var distance = this.computeDistanceToTile(tile, frameState);
        tile._distance = distance;

        if (frameState.fog.enabled) {
            if (CesiumMath.fog(distance, frameState.fog.density) >= 1.0) {
                // Tile is completely in fog so return that it is not visible.
                return Visibility.NONE;
            }
        }

        var surfaceTile = tile.data;
        var tileBoundingRegion = surfaceTile.tileBoundingRegion;

        if (surfaceTile.boundingVolumeSourceTile === undefined) {
            // We have no idea where this tile is, so let's just call it partially visible.
            return Visibility.PARTIAL;
        }

        var cullingVolume = frameState.cullingVolume;
        var boundingVolume = surfaceTile.orientedBoundingBox;

        // Check if the tile is outside the limit area in cartographic space
        surfaceTile.clippedByBoundaries = false;
        var clippedCartographicLimitRectangle = clipRectangleAntimeridian(tile.rectangle, this.cartographicLimitRectangle);
        var areaLimitIntersection = Rectangle.simpleIntersection(clippedCartographicLimitRectangle, tile.rectangle, rectangleIntersectionScratch);
        if (!defined(areaLimitIntersection)) {
            return Visibility.NONE;
        }
        if (!Rectangle.equals(areaLimitIntersection, tile.rectangle)) {
            surfaceTile.clippedByBoundaries = true;
        }

        if (frameState.mode !== SceneMode.SCENE3D) {
            boundingVolume = boundingSphereScratch;
            BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, boundingVolume);
            Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center);

            if (frameState.mode === SceneMode.MORPHING && defined(surfaceTile.renderedMesh)) {
                boundingVolume = BoundingSphere.union(surfaceTile.renderedMesh.boundingSphere3D, boundingVolume, boundingVolume);
            }
        }

        var clippingPlanes = this._clippingPlanes;
        if (defined(clippingPlanes) && clippingPlanes.enabled) {
            var planeIntersection = clippingPlanes.computeIntersectionWithBoundingVolume(boundingVolume);
            tile.isClipped = (planeIntersection !== Intersect.INSIDE);
            if (planeIntersection === Intersect.OUTSIDE) {
                return Visibility.NONE;
            }
        }

        var intersection = cullingVolume.computeVisibility(boundingVolume);
        if (intersection === Intersect.OUTSIDE) {
            return Visibility.NONE;
        }

        var ortho3D = frameState.mode === SceneMode.SCENE3D && frameState.camera.frustum instanceof OrthographicFrustum;
        if (frameState.mode === SceneMode.SCENE3D && !ortho3D && defined(occluders)) {
            var occludeePointInScaledSpace = surfaceTile.occludeePointInScaledSpace;
            if (!defined(occludeePointInScaledSpace)) {
                return intersection;
            }

            if (occluders.ellipsoid.isScaledSpacePointVisible(occludeePointInScaledSpace)) {
                return intersection;
            }

            return Visibility.NONE;
        }

        return intersection;
    };

    /**
     * Determines if the given tile can be refined
     * @param {QuadtreeTile} tile The tile to check.
     * @returns {boolean} True if the tile can be refined, false if it cannot.
     */
    GlobeSurfaceTileProvider.prototype.canRefine = function(tile) {
        // Only allow refinement it we know whether or not the children of this tile exist.
        // For a tileset with `availability`, we'll always be able to refine.
        // We can ask for availability of _any_ child tile because we only need to confirm
        // that we get a yes or no answer, it doesn't matter what the answer is.
        if (defined(tile.data.terrainData)) {
            return true;
        }
        var childAvailable = this.terrainProvider.getTileDataAvailable(tile.x * 2, tile.y * 2, tile.level + 1);
        return childAvailable !== undefined;
    };

    var readyImageryScratch = [];
    var canRenderTraversalStack = [];

    /**
     * Determines if the given not-fully-loaded tile can be rendered without losing detail that
     * was present last frame as a result of rendering descendant tiles. This method will only be
     * called if this tile's descendants were rendered last frame. If the tile is fully loaded,
     * it is assumed that this method will return true and it will not be called.
     * @param {QuadtreeTile} tile The tile to check.
     * @returns {boolean} True if the tile can be rendered without losing detail.
     */
    GlobeSurfaceTileProvider.prototype.canRenderWithoutLosingDetail = function(tile, frameState) {
        var surfaceTile = tile.data;

        var readyImagery = readyImageryScratch;
        readyImagery.length = this._imageryLayers.length;

        var terrainReady = false;
        var initialImageryState = false;
        var imagery;

        if (defined(surfaceTile)) {
            // We can render even with non-ready terrain as long as all our rendered descendants
            // are missing terrain geometry too. i.e. if we rendered fills for more detailed tiles
            // last frame, it's ok to render a fill for this tile this frame.
            terrainReady = surfaceTile.terrainState === TerrainState.READY;

            // Initially assume all imagery layers are ready, unless imagery hasn't been initialized at all.
            initialImageryState = true;

            imagery = surfaceTile.imagery;
        }

        var i;
        var len;

        for (i = 0, len = readyImagery.length; i < len; ++i) {
            readyImagery[i] = initialImageryState;
        }

        if (defined(imagery)) {
            for (i = 0, len = imagery.length; i < len; ++i) {
                var tileImagery = imagery[i];
                var loadingImagery = tileImagery.loadingImagery;
                var isReady = !defined(loadingImagery) || loadingImagery.state === ImageryState.FAILED || loadingImagery.state === ImageryState.INVALID;
                var layerIndex = (tileImagery.loadingImagery || tileImagery.readyImagery).imageryLayer._layerIndex;

                // For a layer to be ready, all tiles belonging to that layer must be ready.
                readyImagery[layerIndex] = isReady && readyImagery[layerIndex];
            }
        }

        var lastFrame = this.quadtree._lastSelectionFrameNumber;

        // Traverse the descendants looking for one with terrain or imagery that is not loaded on this tile.
        var stack = canRenderTraversalStack;
        stack.length = 0;
        stack.push(tile.southwestChild, tile.southeastChild, tile.northwestChild, tile.northeastChild);

        while (stack.length > 0) {
            var descendant = stack.pop();
            var lastFrameSelectionResult = descendant._lastSelectionResultFrame === lastFrame ? descendant._lastSelectionResult : TileSelectionResult.NONE;

            if (lastFrameSelectionResult === TileSelectionResult.RENDERED) {
                var descendantSurface = descendant.data;

                if (!defined(descendantSurface)) {
                    // Descendant has no data, so it can't block rendering.
                    continue;
                }

                if (!terrainReady && descendant.data.terrainState === TerrainState.READY) {
                    // Rendered descendant has real terrain, but we don't. Rendering is blocked.
                    return false;
                }

                var descendantImagery = descendant.data.imagery;
                for (i = 0, len = descendantImagery.length; i < len; ++i) {
                    var descendantTileImagery = descendantImagery[i];
                    var descendantLoadingImagery = descendantTileImagery.loadingImagery;
                    var descendantIsReady = !defined(descendantLoadingImagery) || descendantLoadingImagery.state === ImageryState.FAILED || descendantLoadingImagery.state === ImageryState.INVALID;
                    var descendantLayerIndex = (descendantTileImagery.loadingImagery || descendantTileImagery.readyImagery).imageryLayer._layerIndex;

                    // If this imagery tile of a descendant is ready but the layer isn't ready in this tile,
                    // then rendering is blocked.
                    if (descendantIsReady && !readyImagery[descendantLayerIndex]) {
                        return false;
                    }
                }
            } else if (lastFrameSelectionResult === TileSelectionResult.REFINED) {
                stack.push(descendant.southwestChild, descendant.southeastChild, descendant.northwestChild, descendant.northeastChild);
            }
        }

        return true;
    };

    var tileDirectionScratch = new Cartesian3();

    /**
     * Determines the priority for loading this tile. Lower priority values load sooner.
     * @param {QuatreeTile} tile The tile.
     * @param {FrameState} frameState The frame state.
     * @returns {Number} The load priority value.
     */
    GlobeSurfaceTileProvider.prototype.computeTileLoadPriority = function(tile, frameState) {
        var surfaceTile = tile.data;
        if (surfaceTile === undefined) {
            return 0.0;
        }

        var obb = surfaceTile.orientedBoundingBox;
        if (obb === undefined) {
            return 0.0;
        }

        var cameraPosition = frameState.camera.positionWC;
        var cameraDirection = frameState.camera.directionWC;
        var tileDirection = Cartesian3.subtract(obb.center, cameraPosition, tileDirectionScratch);
        var magnitude = Cartesian3.magnitude(tileDirection);
        if (magnitude < CesiumMath.EPSILON5) {
            return 0.0;
        }
        Cartesian3.divideByScalar(tileDirection, magnitude, tileDirection);
        return (1.0 - Cartesian3.dot(tileDirection, cameraDirection)) * tile._distance;
    };

    var modifiedModelViewScratch = new Matrix4();
    var modifiedModelViewProjectionScratch = new Matrix4();
    var tileRectangleScratch = new Cartesian4();
    var localizedCartographicLimitRectangleScratch = new Cartesian4();
    var rtcScratch = new Cartesian3();
    var centerEyeScratch = new Cartesian3();
    var southwestScratch = new Cartesian3();
    var northeastScratch = new Cartesian3();

    /**
     * Shows a specified tile in this frame.  The provider can cause the tile to be shown by adding
     * render commands to the commandList, or use any other method as appropriate.  The tile is not
     * expected to be visible next frame as well, unless this method is called next frame, too.
     *
     * @param {QuadtreeTile} tile The tile instance.
     * @param {FrameState} frameState The state information of the current rendering frame.
     */
    GlobeSurfaceTileProvider.prototype.showTileThisFrame = function(tile, frameState) {
        var readyTextureCount = 0;
        var tileImageryCollection = tile.data.imagery;
        for (var i = 0, len = tileImageryCollection.length; i < len; ++i) {
            var tileImagery = tileImageryCollection[i];
            if (defined(tileImagery.readyImagery) && tileImagery.readyImagery.imageryLayer.alpha !== 0.0) {
                ++readyTextureCount;
            }
        }

        var tileSet = this._tilesToRenderByTextureCount[readyTextureCount];
        if (!defined(tileSet)) {
            tileSet = [];
            this._tilesToRenderByTextureCount[readyTextureCount] = tileSet;
        }

        tileSet.push(tile);

        var surfaceTile = tile.data;
        if (!defined(surfaceTile.vertexArray)) {
            this._hasFillTilesThisFrame = true;
        } else {
            this._hasLoadedTilesThisFrame = true;
        }

        var debug = this._debug;
        ++debug.tilesRendered;
        debug.texturesRendered += readyTextureCount;
    };

    var cornerPositionsScratch = [new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3()];

    function computeOccludeePoint(tileProvider, center, rectangle, height, result) {
        var ellipsoidalOccluder = tileProvider.quadtree._occluders.ellipsoid;
        var ellipsoid = ellipsoidalOccluder.ellipsoid;

        var cornerPositions = cornerPositionsScratch;
        Cartesian3.fromRadians(rectangle.west, rectangle.south, height, ellipsoid, cornerPositions[0]);
        Cartesian3.fromRadians(rectangle.east, rectangle.south, height, ellipsoid, cornerPositions[1]);
        Cartesian3.fromRadians(rectangle.west, rectangle.north, height, ellipsoid, cornerPositions[2]);
        Cartesian3.fromRadians(rectangle.east, rectangle.north, height, ellipsoid, cornerPositions[3]);

        return ellipsoidalOccluder.computeHorizonCullingPoint(center, cornerPositions, result);
    }

    /**
     * Gets the distance from the camera to the closest point on the tile.  This is used for level-of-detail selection.
     *
     * @param {QuadtreeTile} tile The tile instance.
     * @param {FrameState} frameState The state information of the current rendering frame.
     *
     * @returns {Number} The distance from the camera to the closest point on the tile, in meters.
     */
    GlobeSurfaceTileProvider.prototype.computeDistanceToTile = function(tile, frameState) {
        // The distance should be:
        // 1. the actual distance to the tight-fitting bounding volume, or
        // 2. a distance that is equal to or greater than the actual distance to the tight-fitting bounding volume.
        //
        // When we don't know the min/max heights for a tile, but we do know the min/max of an ancestor tile, we can
        // build a tight-fitting bounding volume horizontally, but not vertically. The min/max heights from the
        // ancestor will likely form a volume that is much bigger than it needs to be. This means that the volume may
        // be deemed to be much closer to the camera than it really is, causing us to select tiles that are too detailed.
        // Loading too-detailed tiles is super expensive, so we don't want to do that. We don't know where the child
        // tile really lies within the parent range of heights, but we _do_ know the child tile can't be any closer than
        // the ancestor height surface (min or max) that is _farthest away_ from the camera. So if we compute distance
        // based that conservative metric, we may end up loading tiles that are not detailed enough, but that's much
        // better (faster) than loading tiles that are too detailed.

        var heightSource = updateTileBoundingRegion(tile, this.terrainProvider, frameState);
        var surfaceTile = tile.data;
        var tileBoundingRegion = surfaceTile.tileBoundingRegion;

        if (heightSource === undefined) {
            // Can't find any min/max heights anywhere? Ok, let's just say the
            // tile is really far away so we'll load and render it rather than
            // refining.
            return 9999999999.0;
        } else if (surfaceTile.boundingVolumeSourceTile !== heightSource) {
            // Heights are from a new source tile, so update the bounding volume.
            surfaceTile.boundingVolumeSourceTile = heightSource;
            surfaceTile.orientedBoundingBox = OrientedBoundingBox.fromRectangle(
                tile.rectangle,
                tileBoundingRegion.minimumHeight,
                tileBoundingRegion.maximumHeight,
                tile.tilingScheme.ellipsoid,
                surfaceTile.orientedBoundingBox);

            surfaceTile.occludeePointInScaledSpace = computeOccludeePoint(this, surfaceTile.orientedBoundingBox.center, tile.rectangle, tileBoundingRegion.maximumHeight, surfaceTile.occludeePointInScaledSpace);
        }

        var min = tileBoundingRegion.minimumHeight;
        var max = tileBoundingRegion.maximumHeight;

        if (surfaceTile.boundingVolumeSourceTile !== tile) {
            var cameraHeight = frameState.camera.positionCartographic.height;
            var distanceToMin = Math.abs(cameraHeight - min);
            var distanceToMax = Math.abs(cameraHeight - max);
            if (distanceToMin > distanceToMax) {
                tileBoundingRegion.minimumHeight = min;
                tileBoundingRegion.maximumHeight = min;
            } else {
                tileBoundingRegion.minimumHeight = max;
                tileBoundingRegion.maximumHeight = max;
            }
        }

        var result = tileBoundingRegion.distanceToCamera(frameState);

        tileBoundingRegion.minimumHeight = min;
        tileBoundingRegion.maximumHeight = max;

        return result;
    };

    function updateTileBoundingRegion(tile, terrainProvider, frameState) {
        var surfaceTile = tile.data;
        if (surfaceTile === undefined) {
            surfaceTile = tile.data = new GlobeSurfaceTile();
        }

        if (surfaceTile.tileBoundingRegion === undefined) {
            surfaceTile.tileBoundingRegion = new TileBoundingRegion({
                computeBoundingVolumes : false,
                rectangle : tile.rectangle,
                ellipsoid : tile.tilingScheme.ellipsoid,
                minimumHeight : 0.0,
                maximumHeight : 0.0
            });
        }

        var terrainData = surfaceTile.terrainData;
        var mesh = surfaceTile.mesh;
        var tileBoundingRegion = surfaceTile.tileBoundingRegion;

        if (mesh !== undefined && mesh.minimumHeight !== undefined && mesh.maximumHeight !== undefined) {
            // We have tight-fitting min/max heights from the mesh.
            tileBoundingRegion.minimumHeight = mesh.minimumHeight;
            tileBoundingRegion.maximumHeight = mesh.maximumHeight;
            return tile;
        }

        if (terrainData !== undefined && terrainData._minimumHeight !== undefined && terrainData._maximumHeight !== undefined) {
            // We have tight-fitting min/max heights from the terrain data.
            tileBoundingRegion.minimumHeight = terrainData._minimumHeight * frameState.terrainExaggeration;
            tileBoundingRegion.maximumHeight = terrainData._maximumHeight * frameState.terrainExaggeration;
            return tile;
        }

        // No accurate min/max heights available, so we're stuck with min/max heights from an ancestor tile.
        tileBoundingRegion.minimumHeight = Number.NaN;
        tileBoundingRegion.maximumHeight = Number.NaN;

        var ancestor = tile.parent;
        while (ancestor !== undefined) {
            var ancestorSurfaceTile = ancestor.data;
            if (ancestorSurfaceTile !== undefined) {
                var ancestorMesh = ancestorSurfaceTile.mesh;
                if (ancestorMesh !== undefined && ancestorMesh.minimumHeight !== undefined && ancestorMesh.maximumHeight !== undefined) {
                    tileBoundingRegion.minimumHeight = ancestorMesh.minimumHeight;
                    tileBoundingRegion.maximumHeight = ancestorMesh.maximumHeight;
                    return ancestor;
                }

                var ancestorTerrainData = ancestorSurfaceTile.terrainData;
                if (ancestorTerrainData !== undefined && ancestorTerrainData._minimumHeight !== undefined && ancestorTerrainData._maximumHeight !== undefined) {
                    tileBoundingRegion.minimumHeight = ancestorTerrainData._minimumHeight * frameState.terrainExaggeration;
                    tileBoundingRegion.maximumHeight = ancestorTerrainData._maximumHeight * frameState.terrainExaggeration;
                    return ancestor;
                }
            }
            ancestor = ancestor.parent;
        }

        return undefined;
    }

    /**
     * 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 GlobeSurfaceTileProvider#destroy
     */
    GlobeSurfaceTileProvider.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.
     *
     * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
     *
     *
     * @example
     * provider = provider && provider();
     *
     * @see GlobeSurfaceTileProvider#isDestroyed
     */
    GlobeSurfaceTileProvider.prototype.destroy = function() {
        this._tileProvider = this._tileProvider && this._tileProvider.destroy();
        this._clippingPlanes = this._clippingPlanes && this._clippingPlanes.destroy();

        return destroyObject(this);
    };

    function getTileReadyCallback(tileImageriesToFree, layer, terrainProvider) {
        return function(tile) {
            var tileImagery;
            var imagery;
            var startIndex = -1;
            var tileImageryCollection = tile.data.imagery;
            var length = tileImageryCollection.length;
            var i;
            for (i = 0; i < length; ++i) {
                tileImagery = tileImageryCollection[i];
                imagery = defaultValue(tileImagery.readyImagery, tileImagery.loadingImagery);
                if (imagery.imageryLayer === layer) {
                    startIndex = i;
                    break;
                }
            }

            if (startIndex !== -1) {
                var endIndex = startIndex + tileImageriesToFree;
                tileImagery = tileImageryCollection[endIndex];
                imagery = defined(tileImagery) ? defaultValue(tileImagery.readyImagery, tileImagery.loadingImagery) : undefined;
                if (!defined(imagery) || imagery.imageryLayer !== layer) {
                    // Return false to keep the callback if we have to wait on the skeletons
                    // Return true to remove the callback if something went wrong
                    return !(layer._createTileImagerySkeletons(tile, terrainProvider, endIndex));
                }

                for (i = startIndex; i < endIndex; ++i) {
                    tileImageryCollection[i].freeResources();
                }

                tileImageryCollection.splice(startIndex, tileImageriesToFree);
            }

            return true; // Everything is done, so remove the callback
        };
    }

    GlobeSurfaceTileProvider.prototype._onLayerAdded = function(layer, index) {
        if (layer.show) {
            var terrainProvider = this._terrainProvider;

            var that = this;
            var imageryProvider = layer.imageryProvider;
            var tileImageryUpdatedEvent = this._imageryLayersUpdatedEvent;
            imageryProvider._reload = function() {
                // Clear the layer's cache
                layer._imageryCache = {};

                that._quadtree.forEachLoadedTile(function(tile) {
                    // If this layer is still waiting to for the loaded callback, just return
                    if (defined(tile._loadedCallbacks[layer._layerIndex])) {
                        return;
                    }

                    var i;

                    // Figure out how many TileImageries we will need to remove and where to insert new ones
                    var tileImageryCollection = tile.data.imagery;
                    var length = tileImageryCollection.length;
                    var startIndex = -1;
                    var tileImageriesToFree = 0;
                    for (i = 0; i < length; ++i) {
                        var tileImagery = tileImageryCollection[i];
                        var imagery = defaultValue(tileImagery.readyImagery, tileImagery.loadingImagery);
                        if (imagery.imageryLayer === layer) {
                            if (startIndex === -1) {
                                startIndex = i;
                            }

                            ++tileImageriesToFree;
                        } else if (startIndex !== -1) {
                            // iterated past the section of TileImageries belonging to this layer, no need to continue.
                            break;
                        }
                    }

                    if (startIndex === -1) {
                        return;
                    }

                    // Insert immediately after existing TileImageries
                    var insertionPoint = startIndex + tileImageriesToFree;

                    // Create new TileImageries for all loaded tiles
                    if (layer._createTileImagerySkeletons(tile, terrainProvider, insertionPoint)) {
                        // Add callback to remove old TileImageries when the new TileImageries are ready
                        tile._loadedCallbacks[layer._layerIndex] = getTileReadyCallback(tileImageriesToFree, layer, terrainProvider);

                        tile.state = QuadtreeTileLoadState.LOADING;
                    }
                });
            };

            // create TileImageries for this layer for all previously loaded tiles
            this._quadtree.forEachLoadedTile(function(tile) {
                if (layer._createTileImagerySkeletons(tile, terrainProvider)) {
                    tile.state = QuadtreeTileLoadState.LOADING;

                    // Tiles that are not currently being rendered need to load the new layer before they're renderable.
                    // We don't mark the rendered tiles non-renderable, though, because that would make the globe disappear.
                    if (tile.level !== 0 && (tile._lastSelectionResultFrame !== that.quadtree._lastSelectionFrameNumber || tile._lastSelectionResult !== TileSelectionResult.RENDERED)) {
                        tile.renderable = false;
                    }
                }
            });

            this._layerOrderChanged = true;
            tileImageryUpdatedEvent.raiseEvent();
        }
    };

    GlobeSurfaceTileProvider.prototype._onLayerRemoved = function(layer, index) {
        // destroy TileImagerys for this layer for all previously loaded tiles
        this._quadtree.forEachLoadedTile(function(tile) {
            var tileImageryCollection = tile.data.imagery;

            var startIndex = -1;
            var numDestroyed = 0;
            for (var i = 0, len = tileImageryCollection.length; i < len; ++i) {
                var tileImagery = tileImageryCollection[i];
                var imagery = tileImagery.loadingImagery;
                if (!defined(imagery)) {
                    imagery = tileImagery.readyImagery;
                }
                if (imagery.imageryLayer === layer) {
                    if (startIndex === -1) {
                        startIndex = i;
                    }

                    tileImagery.freeResources();
                    ++numDestroyed;
                } else if (startIndex !== -1) {
                    // iterated past the section of TileImagerys belonging to this layer, no need to continue.
                    break;
                }
            }

            if (startIndex !== -1) {
                tileImageryCollection.splice(startIndex, numDestroyed);
            }
        });

        if (defined(layer.imageryProvider)) {
            layer.imageryProvider._reload = undefined;
        }

        this._imageryLayersUpdatedEvent.raiseEvent();
    };

    GlobeSurfaceTileProvider.prototype._onLayerMoved = function(layer, newIndex, oldIndex) {
        this._layerOrderChanged = true;
        this._imageryLayersUpdatedEvent.raiseEvent();
    };

    GlobeSurfaceTileProvider.prototype._onLayerShownOrHidden = function(layer, index, show) {
        if (show) {
            this._onLayerAdded(layer, index);
        } else {
            this._onLayerRemoved(layer, index);
        }
    };

    var scratchClippingPlaneMatrix = new Matrix4();
    function createTileUniformMap(frameState, globeSurfaceTileProvider) {
        var uniformMap = {
            u_initialColor : function() {
                return this.properties.initialColor;
            },
            u_fillHighlightColor : function() {
                return this.properties.fillHighlightColor;
            },
            u_zoomedOutOceanSpecularIntensity : function() {
                return this.properties.zoomedOutOceanSpecularIntensity;
            },
            u_oceanNormalMap : function() {
                return this.properties.oceanNormalMap;
            },
            u_lightingFadeDistance : function() {
                return this.properties.lightingFadeDistance;
            },
            u_nightFadeDistance : function() {
                return this.properties.nightFadeDistance;
            },
            u_center3D : function() {
                return this.properties.center3D;
            },
            u_tileRectangle : function() {
                return this.properties.tileRectangle;
            },
            u_modifiedModelView : function() {
                var viewMatrix = frameState.context.uniformState.view;
                var centerEye = Matrix4.multiplyByPoint(viewMatrix, this.properties.rtc, centerEyeScratch);
                Matrix4.setTranslation(viewMatrix, centerEye, modifiedModelViewScratch);
                return modifiedModelViewScratch;
            },
            u_modifiedModelViewProjection : function() {
                var viewMatrix = frameState.context.uniformState.view;
                var projectionMatrix = frameState.context.uniformState.projection;
                var centerEye = Matrix4.multiplyByPoint(viewMatrix, this.properties.rtc, centerEyeScratch);
                Matrix4.setTranslation(viewMatrix, centerEye, modifiedModelViewProjectionScratch);
                Matrix4.multiply(projectionMatrix, modifiedModelViewProjectionScratch, modifiedModelViewProjectionScratch);
                return modifiedModelViewProjectionScratch;
            },
            u_dayTextures : function() {
                return this.properties.dayTextures;
            },
            u_dayTextureTranslationAndScale : function() {
                return this.properties.dayTextureTranslationAndScale;
            },
            u_dayTextureTexCoordsRectangle : function() {
                return this.properties.dayTextureTexCoordsRectangle;
            },
            u_dayTextureUseWebMercatorT : function() {
                return this.properties.dayTextureUseWebMercatorT;
            },
            u_dayTextureAlpha : function() {
                return this.properties.dayTextureAlpha;
            },
            u_dayTextureBrightness : function() {
                return this.properties.dayTextureBrightness;
            },
            u_dayTextureContrast : function() {
                return this.properties.dayTextureContrast;
            },
            u_dayTextureHue : function() {
                return this.properties.dayTextureHue;
            },
            u_dayTextureSaturation : function() {
                return this.properties.dayTextureSaturation;
            },
            u_dayTextureOneOverGamma : function() {
                return this.properties.dayTextureOneOverGamma;
            },
            u_dayIntensity : function() {
                return this.properties.dayIntensity;
            },
            u_southAndNorthLatitude : function() {
                return this.properties.southAndNorthLatitude;
            },
            u_southMercatorYAndOneOverHeight : function() {
                return this.properties.southMercatorYAndOneOverHeight;
            },
            u_waterMask : function() {
                return this.properties.waterMask;
            },
            u_waterMaskTranslationAndScale : function() {
                return this.properties.waterMaskTranslationAndScale;
            },
            u_minMaxHeight : function() {
                return this.properties.minMaxHeight;
            },
            u_scaleAndBias : function() {
                return this.properties.scaleAndBias;
            },
            u_dayTextureSplit : function() {
                return this.properties.dayTextureSplit;
            },
            u_dayTextureCutoutRectangles : function() {
                return this.properties.dayTextureCutoutRectangles;
            },
            u_clippingPlanes : function() {
                var clippingPlanes = globeSurfaceTileProvider._clippingPlanes;
                if (defined(clippingPlanes) && defined(clippingPlanes.texture)) {
                    // Check in case clippingPlanes hasn't been updated yet.
                    return clippingPlanes.texture;
                }
                return frameState.context.defaultTexture;
            },
            u_cartographicLimitRectangle : function() {
                return this.properties.localizedCartographicLimitRectangle;
            },
            u_clippingPlanesMatrix : function() {
                var clippingPlanes = globeSurfaceTileProvider._clippingPlanes;
                return defined(clippingPlanes) ? Matrix4.multiply(frameState.context.uniformState.view, clippingPlanes.modelMatrix, scratchClippingPlaneMatrix) : Matrix4.IDENTITY;
            },
            u_clippingPlanesEdgeStyle : function() {
                var style = this.properties.clippingPlanesEdgeColor;
                style.alpha = this.properties.clippingPlanesEdgeWidth;
                return style;
            },
            u_minimumBrightness : function() {
                return frameState.fog.minimumBrightness;
            },
            u_hsbShift : function() {
                return this.properties.hsbShift;
            },

            // make a separate object so that changes to the properties are seen on
            // derived commands that combine another uniform map with this one.
            properties : {
                initialColor : new Cartesian4(0.0, 0.0, 0.5, 1.0),
                fillHighlightColor : new Color(0.0, 0.0, 0.0, 0.0),
                zoomedOutOceanSpecularIntensity : 0.5,
                oceanNormalMap : undefined,
                lightingFadeDistance : new Cartesian2(6500000.0, 9000000.0),
                nightFadeDistance : new Cartesian2(10000000.0, 40000000.0),
                hsbShift : new Cartesian3(),

                center3D : undefined,
                rtc : new Cartesian3(),
                modifiedModelView : new Matrix4(),
                tileRectangle : new Cartesian4(),

                dayTextures : [],
                dayTextureTranslationAndScale : [],
                dayTextureTexCoordsRectangle : [],
                dayTextureUseWebMercatorT : [],
                dayTextureAlpha : [],
                dayTextureBrightness : [],
                dayTextureContrast : [],
                dayTextureHue : [],
                dayTextureSaturation : [],
                dayTextureOneOverGamma : [],
                dayTextureSplit : [],
                dayTextureCutoutRectangles : [],
                dayIntensity : 0.0,

                southAndNorthLatitude : new Cartesian2(),
                southMercatorYAndOneOverHeight : new Cartesian2(),

                waterMask : undefined,
                waterMaskTranslationAndScale : new Cartesian4(),

                minMaxHeight : new Cartesian2(),
                scaleAndBias : new Matrix4(),
                clippingPlanesEdgeColor : Color.clone(Color.WHITE),
                clippingPlanesEdgeWidth : 0.0,

                localizedCartographicLimitRectangle : new Cartesian4()
            }
        };

        return uniformMap;
    }

    function createWireframeVertexArrayIfNecessary(context, provider, tile) {
        var surfaceTile = tile.data;

        var mesh;
        var vertexArray;

        if (defined(surfaceTile.vertexArray)) {
            mesh = surfaceTile.mesh;
            vertexArray = surfaceTile.vertexArray;
        } else if (defined(surfaceTile.fill) && defined(surfaceTile.fill.vertexArray)) {
            mesh = surfaceTile.fill.mesh;
            vertexArray = surfaceTile.fill.vertexArray;
        }

        if (!defined(mesh) || !defined(vertexArray)) {
            return;
        }

        if (defined(surfaceTile.wireframeVertexArray)) {
            if (surfaceTile.wireframeVertexArray.mesh === mesh) {
                return;
            }

            surfaceTile.wireframeVertexArray.destroy();
            surfaceTile.wireframeVertexArray = undefined;
        }

        surfaceTile.wireframeVertexArray = createWireframeVertexArray(context, vertexArray, mesh);
        surfaceTile.wireframeVertexArray.mesh = mesh;
    }

    /**
     * Creates a vertex array for wireframe rendering of a terrain tile.
     *
     * @private
     *
     * @param {Context} context The context in which to create the vertex array.
     * @param {VertexArray} vertexArray The existing, non-wireframe vertex array.  The new vertex array
     *                      will share vertex buffers with this existing one.
     * @param {TerrainMesh} terrainMesh The terrain mesh containing non-wireframe indices.
     * @returns {VertexArray} The vertex array for wireframe rendering.
     */
    function createWireframeVertexArray(context, vertexArray, terrainMesh) {
        var indices = terrainMesh.indices;

        var geometry = {
            indices : indices,
            primitiveType : PrimitiveType.TRIANGLES
        };

        GeometryPipeline.toWireframe(geometry);

        var wireframeIndices = geometry.indices;
        var wireframeIndexBuffer = Buffer.createIndexBuffer({
            context : context,
            typedArray : wireframeIndices,
            usage : BufferUsage.STATIC_DRAW,
            indexDatatype : IndexDatatype.fromSizeInBytes(wireframeIndices.BYTES_PER_ELEMENT)
        });
        return new VertexArray({
            context : context,
            attributes : vertexArray._attributes,
            indexBuffer : wireframeIndexBuffer
        });
    }

    var getDebugOrientedBoundingBox;
    var getDebugBoundingSphere;
    var debugDestroyPrimitive;

    (function() {
        var instanceOBB = new GeometryInstance({
            geometry : BoxOutlineGeometry.fromDimensions({dimensions : new Cartesian3(2.0, 2.0, 2.0)})
        });
        var instanceSphere = new GeometryInstance({
            geometry : new SphereOutlineGeometry({radius : 1.0})
        });
        var modelMatrix = new Matrix4();
        var previousVolume;
        var primitive;

        function createDebugPrimitive(instance) {
            return new Primitive({
                geometryInstances : instance,
                appearance : new PerInstanceColorAppearance({
                    translucent : false,
                    flat : true
                }),
                asynchronous : false
            });
        }

        getDebugOrientedBoundingBox = function(obb, color) {
            if (obb === previousVolume) {
                return primitive;
            }
            debugDestroyPrimitive();

            previousVolume = obb;
            modelMatrix = Matrix4.fromRotationTranslation(obb.halfAxes, obb.center, modelMatrix);

            instanceOBB.modelMatrix = modelMatrix;
            instanceOBB.attributes.color = ColorGeometryInstanceAttribute.fromColor(color);

            primitive = createDebugPrimitive(instanceOBB);
            return primitive;
        };

        getDebugBoundingSphere = function(sphere, color) {
            if (sphere === previousVolume) {
                return primitive;
            }
            debugDestroyPrimitive();

            previousVolume = sphere;
            modelMatrix = Matrix4.fromTranslation(sphere.center, modelMatrix);
            modelMatrix = Matrix4.multiplyByUniformScale(modelMatrix, sphere.radius, modelMatrix);

            instanceSphere.modelMatrix = modelMatrix;
            instanceSphere.attributes.color = ColorGeometryInstanceAttribute.fromColor(color);

            primitive = createDebugPrimitive(instanceSphere);
            return primitive;
        };

        debugDestroyPrimitive = function() {
            if (defined(primitive)) {
                primitive.destroy();
                primitive = undefined;
                previousVolume = undefined;
            }
        };
    })();

    var otherPassesInitialColor = new Cartesian4(0.0, 0.0, 0.0, 0.0);
    var surfaceShaderSetOptionsScratch = {
        frameState : undefined,
        surfaceTile : undefined,
        numberOfDayTextures : undefined,
        applyBrightness : undefined,
        applyContrast : undefined,
        applyHue : undefined,
        applySaturation : undefined,
        applyGamma : undefined,
        applyAlpha : undefined,
        applySplit : undefined,
        showReflectiveOcean : undefined,
        showOceanWaves : undefined,
        enableLighting : undefined,
        showGroundAtmosphere : undefined,
        perFragmentGroundAtmosphere : undefined,
        hasVertexNormals : undefined,
        useWebMercatorProjection : undefined,
        enableFog : undefined,
        enableClippingPlanes : undefined,
        clippingPlanes : undefined,
        clippedByBoundaries : undefined,
        hasImageryLayerCutout : undefined,
        colorCorrect : undefined
    };

    function addDrawCommandsForTile(tileProvider, tile, frameState) {
        var surfaceTile = tile.data;

        if (!defined(surfaceTile.vertexArray)) {
            if (surfaceTile.fill === undefined) {
                // No fill was created for this tile, probably because this tile is not connected to
                // any renderable tiles. So create a simple tile in the middle of the tile's possible
                // height range.
                surfaceTile.fill = new TerrainFillMesh(tile);
            }
            surfaceTile.fill.update(tileProvider, frameState);
        }

        var creditDisplay = frameState.creditDisplay;

        var terrainData = surfaceTile.terrainData;
        if (defined(terrainData) && defined(terrainData.credits)) {
            var tileCredits = terrainData.credits;
            for (var tileCreditIndex = 0,
                     tileCreditLength = tileCredits.length; tileCreditIndex < tileCreditLength; ++tileCreditIndex) {
                creditDisplay.addCredit(tileCredits[tileCreditIndex]);
            }
        }

        var maxTextures = ContextLimits.maximumTextureImageUnits;

        var waterMaskTexture = surfaceTile.waterMaskTexture;
        var waterMaskTranslationAndScale = surfaceTile.waterMaskTranslationAndScale;
        if (!defined(waterMaskTexture) && defined(surfaceTile.fill)) {
            waterMaskTexture = surfaceTile.fill.waterMaskTexture;
            waterMaskTranslationAndScale = surfaceTile.fill.waterMaskTranslationAndScale;
        }

        var showReflectiveOcean = tileProvider.hasWaterMask && defined(waterMaskTexture);
        var oceanNormalMap = tileProvider.oceanNormalMap;
        var showOceanWaves = showReflectiveOcean && defined(oceanNormalMap);
        var hasVertexNormals = tileProvider.terrainProvider.ready && tileProvider.terrainProvider.hasVertexNormals;
        var enableFog = frameState.fog.enabled;
        var showGroundAtmosphere = tileProvider.showGroundAtmosphere;
        var castShadows = ShadowMode.castShadows(tileProvider.shadows);
        var receiveShadows = ShadowMode.receiveShadows(tileProvider.shadows);

        var hueShift = tileProvider.hueShift;
        var saturationShift = tileProvider.saturationShift;
        var brightnessShift = tileProvider.brightnessShift;

        var colorCorrect = !(CesiumMath.equalsEpsilon(hueShift, 0.0, CesiumMath.EPSILON7) &&
                             CesiumMath.equalsEpsilon(saturationShift, 0.0, CesiumMath.EPSILON7) &&
                             CesiumMath.equalsEpsilon(brightnessShift, 0.0, CesiumMath.EPSILON7));

        var perFragmentGroundAtmosphere = false;
        if (showGroundAtmosphere) {
            var mode = frameState.mode;
            var camera = frameState.camera;
            var cameraDistance;
            if (mode === SceneMode.SCENE2D || mode === SceneMode.COLUMBUS_VIEW) {
                cameraDistance = camera.positionCartographic.height;
            } else {
                cameraDistance = Cartesian3.magnitude(camera.positionWC);
            }
            var fadeOutDistance = tileProvider.nightFadeOutDistance;
            if (mode !== SceneMode.SCENE3D) {
                fadeOutDistance -= frameState.mapProjection.ellipsoid.maximumRadius;
            }
            perFragmentGroundAtmosphere = cameraDistance > fadeOutDistance;
        }

        if (showReflectiveOcean) {
            --maxTextures;
        }
        if (showOceanWaves) {
            --maxTextures;
        }

        var mesh = surfaceTile.renderedMesh;
        var rtc = mesh.center;
        var encoding = mesh.encoding;

        // Not used in 3D.
        var tileRectangle = tileRectangleScratch;

        // Only used for Mercator projections.
        var southLatitude = 0.0;
        var northLatitude = 0.0;
        var southMercatorY = 0.0;
        var oneOverMercatorHeight = 0.0;

        var useWebMercatorProjection = false;

        if (frameState.mode !== SceneMode.SCENE3D) {
            var projection = frameState.mapProjection;
            var southwest = projection.project(Rectangle.southwest(tile.rectangle), southwestScratch);
            var northeast = projection.project(Rectangle.northeast(tile.rectangle), northeastScratch);

            tileRectangle.x = southwest.x;
            tileRectangle.y = southwest.y;
            tileRectangle.z = northeast.x;
            tileRectangle.w = northeast.y;

            // In 2D and Columbus View, use the center of the tile for RTC rendering.
            if (frameState.mode !== SceneMode.MORPHING) {
                rtc = rtcScratch;
                rtc.x = 0.0;
                rtc.y = (tileRectangle.z + tileRectangle.x) * 0.5;
                rtc.z = (tileRectangle.w + tileRectangle.y) * 0.5;
                tileRectangle.x -= rtc.y;
                tileRectangle.y -= rtc.z;
                tileRectangle.z -= rtc.y;
                tileRectangle.w -= rtc.z;
            }

            if (frameState.mode === SceneMode.SCENE2D && encoding.quantization === TerrainQuantization.BITS12) {
                // In 2D, the texture coordinates of the tile are interpolated over the rectangle to get the position in the vertex shader.
                // When the texture coordinates are quantized, error is introduced. This can be seen through the 1px wide cracking
                // between the quantized tiles in 2D. To compensate for the error, move the expand the rectangle in each direction by
                // half the error amount.
                var epsilon = (1.0 / (Math.pow(2.0, 12.0) - 1.0)) * 0.5;
                var widthEpsilon = (tileRectangle.z - tileRectangle.x) * epsilon;
                var heightEpsilon = (tileRectangle.w - tileRectangle.y) * epsilon;
                tileRectangle.x -= widthEpsilon;
                tileRectangle.y -= heightEpsilon;
                tileRectangle.z += widthEpsilon;
                tileRectangle.w += heightEpsilon;
            }

            if (projection instanceof WebMercatorProjection) {
                southLatitude = tile.rectangle.south;
                northLatitude = tile.rectangle.north;

                southMercatorY = WebMercatorProjection.geodeticLatitudeToMercatorAngle(southLatitude);

                oneOverMercatorHeight = 1.0 / (WebMercatorProjection.geodeticLatitudeToMercatorAngle(northLatitude) - southMercatorY);

                useWebMercatorProjection = true;
            }
        }

        var surfaceShaderSetOptions = surfaceShaderSetOptionsScratch;
        surfaceShaderSetOptions.frameState = frameState;
        surfaceShaderSetOptions.surfaceTile = surfaceTile;
        surfaceShaderSetOptions.showReflectiveOcean = showReflectiveOcean;
        surfaceShaderSetOptions.showOceanWaves = showOceanWaves;
        surfaceShaderSetOptions.enableLighting = tileProvider.enableLighting;
        surfaceShaderSetOptions.showGroundAtmosphere = showGroundAtmosphere;
        surfaceShaderSetOptions.perFragmentGroundAtmosphere = perFragmentGroundAtmosphere;
        surfaceShaderSetOptions.hasVertexNormals = hasVertexNormals;
        surfaceShaderSetOptions.useWebMercatorProjection = useWebMercatorProjection;
        surfaceShaderSetOptions.clippedByBoundaries = surfaceTile.clippedByBoundaries;

        var tileImageryCollection = surfaceTile.imagery;
        var imageryIndex = 0;
        var imageryLen = tileImageryCollection.length;

        var firstPassRenderState = tileProvider._renderState;
        var otherPassesRenderState = tileProvider._blendRenderState;
        var renderState = firstPassRenderState;

        var initialColor = tileProvider._firstPassInitialColor;

        var context = frameState.context;

        if (!defined(tileProvider._debug.boundingSphereTile)) {
            debugDestroyPrimitive();
        }

        do {
            var numberOfDayTextures = 0;

            var command;
            var uniformMap;

            if (tileProvider._drawCommands.length <= tileProvider._usedDrawCommands) {
                command = new DrawCommand();
                command.owner = tile;
                command.cull = false;
                command.boundingVolume = new BoundingSphere();
                command.orientedBoundingBox = undefined;

                uniformMap = createTileUniformMap(frameState, tileProvider);

                tileProvider._drawCommands.push(command);
                tileProvider._uniformMaps.push(uniformMap);
            } else {
                command = tileProvider._drawCommands[tileProvider._usedDrawCommands];
                uniformMap = tileProvider._uniformMaps[tileProvider._usedDrawCommands];
            }

            command.owner = tile;

            ++tileProvider._usedDrawCommands;

            if (tile === tileProvider._debug.boundingSphereTile) {
                var obb = surfaceTile.orientedBoundingBox;
                // If a debug primitive already exists for this tile, it will not be
                // re-created, to avoid allocation every frame. If it were possible
                // to have more than one selected tile, this would have to change.
                if (defined(obb)) {
                    getDebugOrientedBoundingBox(obb, Color.RED).update(frameState);
                } else if (defined(mesh) && defined(mesh.boundingSphere3D)) {
                    getDebugBoundingSphere(mesh.boundingSphere3D, Color.RED).update(frameState);
                }
            }

            var uniformMapProperties = uniformMap.properties;
            Cartesian4.clone(initialColor, uniformMapProperties.initialColor);
            uniformMapProperties.oceanNormalMap = oceanNormalMap;
            uniformMapProperties.lightingFadeDistance.x = tileProvider.lightingFadeOutDistance;
            uniformMapProperties.lightingFadeDistance.y = tileProvider.lightingFadeInDistance;
            uniformMapProperties.nightFadeDistance.x = tileProvider.nightFadeOutDistance;
            uniformMapProperties.nightFadeDistance.y = tileProvider.nightFadeInDistance;
            uniformMapProperties.zoomedOutOceanSpecularIntensity = tileProvider.zoomedOutOceanSpecularIntensity;

            var highlightFillTile = !defined(surfaceTile.vertexArray) && defined(tileProvider.fillHighlightColor) && tileProvider.fillHighlightColor.alpha > 0.0;
            if (highlightFillTile) {
                Color.clone(tileProvider.fillHighlightColor, uniformMapProperties.fillHighlightColor);
            }

            uniformMapProperties.center3D = mesh.center;
            Cartesian3.clone(rtc, uniformMapProperties.rtc);

            Cartesian4.clone(tileRectangle, uniformMapProperties.tileRectangle);
            uniformMapProperties.southAndNorthLatitude.x = southLatitude;
            uniformMapProperties.southAndNorthLatitude.y = northLatitude;
            uniformMapProperties.southMercatorYAndOneOverHeight.x = southMercatorY;
            uniformMapProperties.southMercatorYAndOneOverHeight.y = oneOverMercatorHeight;

            // Convert tile limiter rectangle from cartographic to texture space using the tileRectangle.
            var localizedCartographicLimitRectangle = localizedCartographicLimitRectangleScratch;
            var cartographicLimitRectangle = clipRectangleAntimeridian(tile.rectangle, tileProvider.cartographicLimitRectangle);

            Cartesian3.fromElements(hueShift, saturationShift, brightnessShift, uniformMapProperties.hsbShift);

            var cartographicTileRectangle = tile.rectangle;
            var inverseTileWidth = 1.0 / cartographicTileRectangle.width;
            var inverseTileHeight = 1.0 / cartographicTileRectangle.height;
            localizedCartographicLimitRectangle.x = (cartographicLimitRectangle.west - cartographicTileRectangle.west) * inverseTileWidth;
            localizedCartographicLimitRectangle.y = (cartographicLimitRectangle.south - cartographicTileRectangle.south) * inverseTileHeight;
            localizedCartographicLimitRectangle.z = (cartographicLimitRectangle.east - cartographicTileRectangle.west) * inverseTileWidth;
            localizedCartographicLimitRectangle.w = (cartographicLimitRectangle.north - cartographicTileRectangle.south) * inverseTileHeight;

            Cartesian4.clone(localizedCartographicLimitRectangle, uniformMapProperties.localizedCartographicLimitRectangle);

            // For performance, use fog in the shader only when the tile is in fog.
            var applyFog = enableFog && CesiumMath.fog(tile._distance, frameState.fog.density) > CesiumMath.EPSILON3;
            colorCorrect = colorCorrect && (applyFog || showGroundAtmosphere);

            var applyBrightness = false;
            var applyContrast = false;
            var applyHue = false;
            var applySaturation = false;
            var applyGamma = false;
            var applyAlpha = false;
            var applySplit = false;
            var applyCutout = false;

            while (numberOfDayTextures < maxTextures && imageryIndex < imageryLen) {
                var tileImagery = tileImageryCollection[imageryIndex];
                var imagery = tileImagery.readyImagery;
                ++imageryIndex;

                if (!defined(imagery) || imagery.imageryLayer.alpha === 0.0) {
                    continue;
                }

                var texture = tileImagery.useWebMercatorT ? imagery.textureWebMercator : imagery.texture;

                //>>includeStart('debug', pragmas.debug);
                if (!defined(texture)) {
                    // Our "ready" texture isn't actually ready.  This should never happen.
                    //
                    // Side note: It IS possible for it to not be in the READY ImageryState, though.
                    // This can happen when a single imagery tile is shared by two terrain tiles (common)
                    // and one of them (A) needs a geographic version of the tile because it is near the poles,
                    // and the other (B) does not.  B can and will transition the imagery tile to the READY state
                    // without reprojecting to geographic.  Then, later, A will deem that same tile not-ready-yet
                    // because it only has the Web Mercator texture, and flip it back to the TRANSITIONING state.
                    // The imagery tile won't be in the READY state anymore, but it's still READY enough for B's
                    // purposes.
                    throw new DeveloperError('readyImagery is not actually ready!');
                }
                //>>includeEnd('debug');

                var imageryLayer = imagery.imageryLayer;

                if (!defined(tileImagery.textureTranslationAndScale)) {
                    tileImagery.textureTranslationAndScale = imageryLayer._calculateTextureTranslationAndScale(tile, tileImagery);
                }

                uniformMapProperties.dayTextures[numberOfDayTextures] = texture;
                uniformMapProperties.dayTextureTranslationAndScale[numberOfDayTextures] = tileImagery.textureTranslationAndScale;
                uniformMapProperties.dayTextureTexCoordsRectangle[numberOfDayTextures] = tileImagery.textureCoordinateRectangle;
                uniformMapProperties.dayTextureUseWebMercatorT[numberOfDayTextures] = tileImagery.useWebMercatorT;

                uniformMapProperties.dayTextureAlpha[numberOfDayTextures] = imageryLayer.alpha;
                applyAlpha = applyAlpha || uniformMapProperties.dayTextureAlpha[numberOfDayTextures] !== 1.0;

                uniformMapProperties.dayTextureBrightness[numberOfDayTextures] = imageryLayer.brightness;
                applyBrightness = applyBrightness || uniformMapProperties.dayTextureBrightness[numberOfDayTextures] !== ImageryLayer.DEFAULT_BRIGHTNESS;

                uniformMapProperties.dayTextureContrast[numberOfDayTextures] = imageryLayer.contrast;
                applyContrast = applyContrast || uniformMapProperties.dayTextureContrast[numberOfDayTextures] !== ImageryLayer.DEFAULT_CONTRAST;

                uniformMapProperties.dayTextureHue[numberOfDayTextures] = imageryLayer.hue;
                applyHue = applyHue || uniformMapProperties.dayTextureHue[numberOfDayTextures] !== ImageryLayer.DEFAULT_HUE;

                uniformMapProperties.dayTextureSaturation[numberOfDayTextures] = imageryLayer.saturation;
                applySaturation = applySaturation || uniformMapProperties.dayTextureSaturation[numberOfDayTextures] !== ImageryLayer.DEFAULT_SATURATION;

                uniformMapProperties.dayTextureOneOverGamma[numberOfDayTextures] = 1.0 / imageryLayer.gamma;
                applyGamma = applyGamma || uniformMapProperties.dayTextureOneOverGamma[numberOfDayTextures] !== 1.0 / ImageryLayer.DEFAULT_GAMMA;

                uniformMapProperties.dayTextureSplit[numberOfDayTextures] = imageryLayer.splitDirection;
                applySplit = applySplit || uniformMapProperties.dayTextureSplit[numberOfDayTextures] !== 0.0;

                // Update cutout rectangle
                var dayTextureCutoutRectangle = uniformMapProperties.dayTextureCutoutRectangles[numberOfDayTextures];
                if (!defined(dayTextureCutoutRectangle)) {
                    dayTextureCutoutRectangle = uniformMapProperties.dayTextureCutoutRectangles[numberOfDayTextures] = new Cartesian4();
                }

                Cartesian4.clone(Cartesian4.ZERO, dayTextureCutoutRectangle);
                if (defined(imageryLayer.cutoutRectangle)) {
                    var cutoutRectangle = clipRectangleAntimeridian(cartographicTileRectangle, imageryLayer.cutoutRectangle);
                    var intersection = Rectangle.simpleIntersection(cutoutRectangle, cartographicTileRectangle, rectangleIntersectionScratch);
                    applyCutout = defined(intersection) || applyCutout;

                    dayTextureCutoutRectangle.x = (cutoutRectangle.west - cartographicTileRectangle.west) * inverseTileWidth;
                    dayTextureCutoutRectangle.y = (cutoutRectangle.south - cartographicTileRectangle.south) * inverseTileHeight;
                    dayTextureCutoutRectangle.z = (cutoutRectangle.east - cartographicTileRectangle.west) * inverseTileWidth;
                    dayTextureCutoutRectangle.w = (cutoutRectangle.north - cartographicTileRectangle.south) * inverseTileHeight;
                }

                if (defined(imagery.credits)) {
                    var credits = imagery.credits;
                    for (var creditIndex = 0, creditLength = credits.length; creditIndex < creditLength; ++creditIndex) {
                        creditDisplay.addCredit(credits[creditIndex]);
                    }
                }

                ++numberOfDayTextures;
            }

            // trim texture array to the used length so we don't end up using old textures
            // which might get destroyed eventually
            uniformMapProperties.dayTextures.length = numberOfDayTextures;
            uniformMapProperties.waterMask = waterMaskTexture;
            Cartesian4.clone(waterMaskTranslationAndScale, uniformMapProperties.waterMaskTranslationAndScale);

            uniformMapProperties.minMaxHeight.x = encoding.minimumHeight;
            uniformMapProperties.minMaxHeight.y = encoding.maximumHeight;
            Matrix4.clone(encoding.matrix, uniformMapProperties.scaleAndBias);

            // update clipping planes
            var clippingPlanes = tileProvider._clippingPlanes;
            var clippingPlanesEnabled = defined(clippingPlanes) && clippingPlanes.enabled && tile.isClipped;
            if (clippingPlanesEnabled) {
                uniformMapProperties.clippingPlanesEdgeColor = Color.clone(clippingPlanes.edgeColor, uniformMapProperties.clippingPlanesEdgeColor);
                uniformMapProperties.clippingPlanesEdgeWidth = clippingPlanes.edgeWidth;
            }

            if (defined(tileProvider.uniformMap)) {
                uniformMap = combine(uniformMap, tileProvider.uniformMap);
            }

            surfaceShaderSetOptions.numberOfDayTextures = numberOfDayTextures;
            surfaceShaderSetOptions.applyBrightness = applyBrightness;
            surfaceShaderSetOptions.applyContrast = applyContrast;
            surfaceShaderSetOptions.applyHue = applyHue;
            surfaceShaderSetOptions.applySaturation = applySaturation;
            surfaceShaderSetOptions.applyGamma = applyGamma;
            surfaceShaderSetOptions.applyAlpha = applyAlpha;
            surfaceShaderSetOptions.applySplit = applySplit;
            surfaceShaderSetOptions.enableFog = applyFog;
            surfaceShaderSetOptions.enableClippingPlanes = clippingPlanesEnabled;
            surfaceShaderSetOptions.clippingPlanes = clippingPlanes;
            surfaceShaderSetOptions.hasImageryLayerCutout = applyCutout;
            surfaceShaderSetOptions.colorCorrect = colorCorrect;
            surfaceShaderSetOptions.highlightFillTile = highlightFillTile;

            command.shaderProgram = tileProvider._surfaceShaderSet.getShaderProgram(surfaceShaderSetOptions);
            command.castShadows = castShadows;
            command.receiveShadows = receiveShadows;
            command.renderState = renderState;
            command.primitiveType = PrimitiveType.TRIANGLES;
            command.vertexArray = surfaceTile.vertexArray || surfaceTile.fill.vertexArray;
            command.uniformMap = uniformMap;
            command.pass = Pass.GLOBE;

            if (tileProvider._debug.wireframe) {
                createWireframeVertexArrayIfNecessary(context, tileProvider, tile);
                if (defined(surfaceTile.wireframeVertexArray)) {
                    command.vertexArray = surfaceTile.wireframeVertexArray;
                    command.primitiveType = PrimitiveType.LINES;
                }
            }

            var boundingVolume = command.boundingVolume;
            var orientedBoundingBox = command.orientedBoundingBox;

            if (frameState.mode !== SceneMode.SCENE3D) {
                var tileBoundingRegion = surfaceTile.tileBoundingRegion;
                BoundingSphere.fromRectangleWithHeights2D(tile.rectangle, frameState.mapProjection, tileBoundingRegion.minimumHeight, tileBoundingRegion.maximumHeight, boundingVolume);
                Cartesian3.fromElements(boundingVolume.center.z, boundingVolume.center.x, boundingVolume.center.y, boundingVolume.center);

                if (frameState.mode === SceneMode.MORPHING) {
                    boundingVolume = BoundingSphere.union(mesh.boundingSphere3D, boundingVolume, boundingVolume);
                }
            } else {
                command.boundingVolume = BoundingSphere.clone(mesh.boundingSphere3D, boundingVolume);
                command.orientedBoundingBox = OrientedBoundingBox.clone(surfaceTile.orientedBoundingBox, orientedBoundingBox);
            }

            command.dirty = true;
            frameState.commandList.push(command);

            renderState = otherPassesRenderState;
            initialColor = otherPassesInitialColor;
        } while (imageryIndex < imageryLen);
    }

    return GlobeSurfaceTileProvider;
});
