Source: render/DrawContext.js

/*
 * Copyright 2015-2017 WorldWind Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * @exports DrawContext
 */
define([
        '../error/ArgumentError',
        '../util/Color',
        '../util/FrameStatistics',
        '../render/FramebufferTexture',
        '../render/FramebufferTileController',
        '../geom/Frustum',
        '../globe/Globe',
        '../shaders/GpuProgram',
        '../cache/GpuResourceCache',
        '../layer/Layer',
        '../util/Logger',
        '../geom/Matrix',
        '../navigate/NavigatorState',
        '../pick/PickedObjectList',
        '../geom/Plane',
        '../geom/Position',
        '../geom/Rectangle',
        '../render/ScreenCreditController',
        '../geom/Sector',
        '../shapes/SurfaceShape',
        '../shapes/SurfaceShapeTileBuilder',
        '../render/SurfaceTileRenderer',
        '../render/TextSupport',
        '../geom/Vec2',
        '../geom/Vec3',
        '../util/WWMath'
    ],
    function (ArgumentError,
              Color,
              FrameStatistics,
              FramebufferTexture,
              FramebufferTileController,
              Frustum,
              Globe,
              GpuProgram,
              GpuResourceCache,
              Layer,
              Logger,
              Matrix,
              NavigatorState,
              PickedObjectList,
              Plane,
              Position,
              Rectangle,
              ScreenCreditController,
              Sector,
              SurfaceShape,
              SurfaceShapeTileBuilder,
              SurfaceTileRenderer,
              TextSupport,
              Vec2,
              Vec3,
              WWMath) {
        "use strict";

        /**
         * Constructs a DrawContext. Applications do not call this constructor. A draw context is created by a
         * {@link WorldWindow} during its construction.
         * @alias DrawContext
         * @constructor
         * @classdesc Provides current state during rendering. The current draw context is passed to most rendering
         * methods in order to make those methods aware of current state.
         * @param {WebGLRenderingContext} gl The WebGL rendering context this draw context is associated with.
         * @throws {ArgumentError} If the specified WebGL rendering context is null or undefined.
         */
        var DrawContext = function (gl) {
            if (!gl) {
                throw new ArgumentError(Logger.logMessage(Logger.LEVEL_SEVERE, "Texture", "constructor",
                    "missingGlContext"));
            }

            /**
             * The current WebGL rendering context.
             * @type {WebGLRenderingContext}
             */
            this.currentGlContext = gl;

            /**
             * A 2D canvas for creating texture maps.
             * @type {HTMLElement}
             */
            this.canvas2D = document.createElement("canvas");

            /**
             * A 2D context for this draw context's [canvas property]{@link DrawContext#canvas}.
             */
            this.ctx2D = this.canvas2D.getContext("2d");

            /**
             * The current clear color.
             * @type {Color}
             * @default Color.TRANSPARENT (red = 0, green = 0, blue = 0, alpha = 0)
             */
            this.clearColor = Color.TRANSPARENT;

            /**
             * The GPU resource cache, which tracks WebGL resources.
             * @type {GpuResourceCache}
             */
            this.gpuResourceCache = new GpuResourceCache(WorldWind.configuration.gpuCacheSize,
                0.8 * WorldWind.configuration.gpuCacheSize);

            /**
             * The surface-tile-renderer to use for drawing surface tiles.
             * @type {SurfaceTileRenderer}
             */
            this.surfaceTileRenderer = new SurfaceTileRenderer();

            /**
             * The surface shape tile builder used to create and draw surface shapes.
             * @type {SurfaceShapeTileBuilder}
             */
            this.surfaceShapeTileBuilder = new SurfaceShapeTileBuilder();

            /**
             * Provides access to a multi-resolution WebGL framebuffer arranged as adjacent tiles in a pyramid. Surface
             * shapes use these tiles internally to draw on the terrain surface.
             * @type {FramebufferTileController}
             */
            this.surfaceShapeTileController = new FramebufferTileController();

            /**
             * The screen credit controller responsible for collecting and drawing screen credits.
             * @type {ScreenCreditController}
             */
            this.screenCreditController = new ScreenCreditController();

            /**
             * A shared TextSupport instance.
             * @type {TextSupport}
             */
            this.textSupport = new TextSupport();

            /**
             * The current WebGL framebuffer. Null indicates that the default WebGL framebuffer is active.
             * @type {FramebufferTexture}
             */
            this.currentFramebuffer = null;

            /**
             * The current WebGL program. Null indicates that no WebGL program is active.
             * @type {GpuProgram}
             */
            this.currentProgram = null;

            /**
             * The list of surface renderables.
             * @type {Array}
             */
            this.surfaceRenderables = [];

            /**
             * Indicates whether this draw context is in ordered rendering mode.
             * @type {Boolean}
             */
            this.orderedRenderingMode = false;

            /**
             * The list of ordered renderables.
             * @type {Array}
             */
            this.orderedRenderables = [];

            /**
             * The list of screen renderables.
             * @type {Array}
             */
            this.screeRenderables = [];

            // Internal. Intentionally not documented. Provides ordinal IDs to ordered renderables.
            this.orderedRenderablesCounter = 0; // Number

            /**
             * The starting time of the current frame, in milliseconds. The frame timestamp is updated immediately
             * before the WorldWindow associated with this draw context is rendered, either as a result of redrawing or
             * as a result of a picking operation.
             * @type {Number}
             * @readonly
             */
            this.timestamp = Date.now();

            /**
             * The [time stamp]{@link DrawContext#timestamp} of the last visible frame, in milliseconds. This indicates
             * the time stamp that was current during the WorldWindow's last frame, ignoring frames associated with a
             * picking operation. The difference between the previous redraw time stamp and the current time stamp
             * indicates the duration between visible frames, e.g. <code style='white-space:nowrap'>timeStamp - previousRedrawTimestamp</code>.
             * @type {Number}
             * @readonly
             */
            this.previousRedrawTimestamp = this.timestamp;

            /**
             * Indicates whether a redraw has been requested during the current frame. When true, this causes the World
             * Window associated with this draw context to redraw after the current frame.
             * @type {Boolean}
             */
            this.redrawRequested = false;

            /**
             * The globe being rendered.
             * @type {Globe}
             */
            this.globe = null;

            /**
             * A copy of the current globe's state key. Provided here to avoid having to recompute it every time
             * it's needed.
             * @type {String}
             */
            this.globeStateKey = null;

            /**
             * The layers being rendered.
             * @type {Layer[]}
             */
            this.layers = null;

            /**
             * The layer being rendered.
             * @type {Layer}
             */
            this.currentLayer = null;

            /**
             * The current state of the associated navigator.
             * @type {NavigatorState}
             */
            this.navigatorState = null;

            /**
             * The current eye position.
             * @type {Position}
             */
            this.eyePosition = new Position(0, 0, 0);

            /**
             * The current screen projection matrix.
             * @type {Matrix}
             */
            this.screenProjection = Matrix.fromIdentity();

            /**
             * The terrain for the current frame.
             * @type {Terrain}
             */
            this.terrain = null;

            /**
             * The current vertical exaggeration.
             * @type {Number}
             */
            this.verticalExaggeration = 1;

            /**
             * The number of milliseconds over which to fade shapes that support fading. Fading is most typically
             * used during decluttering.
             * @type {Number}
             * @default 500
             */
            this.fadeTime = 500;

            /**
             * The opacity to apply to terrain and surface shapes. Should be a number between 0 and 1.
             * @type {Number}
             * @default 1
             */
            this.surfaceOpacity = 1;

            /**
             * Frame statistics.
             * @type {FrameStatistics}
             */
            this.frameStatistics = null;

            /**
             * Indicates whether the frame is being drawn for picking.
             * @type {Boolean}
             */
            this.pickingMode = false;

            /**
             * Indicates that picking will return only the terrain object, if the pick point is over the terrain.
             * @type {Boolean}
             * @default false
             */
            this.pickTerrainOnly = false;

            /**
             * Indicates that picking will return all objects at the pick point, if any. The top-most object will have
             * its isOnTop flag set to true. If [deep picking]{@link WorldWindow#deepPicking} is false, the default,
             * only the top-most object is returned, plus the picked-terrain object if the pick point is over the
             * terrain.
             * @type {Boolean}
             * @default false
             */
            this.deepPicking = false;

            /**
             * Indicates that picking will return all objects that intersect the pick region, if any. Visible objects
             * will have the isOnTop flag set to true.
             * @type {Boolean}
             * @default false
             */
            this.regionPicking = false;

            /**
             * The current pick point, in screen coordinates.
             * @type {Vec2}
             */
            this.pickPoint = null;

            /**
             * The current pick rectangle, in WebGL (lower-left origin) screen coordinates.
             * @type {Rectangle}
             */
            this.pickRectangle = null;

            /**
             * The off-screen WebGL framebuffer used during picking.
             * @type {FramebufferTexture}
             * @readonly
             */
            this.pickFramebuffer = null;

            /**
             * The current pick frustum, created anew each picking frame.
             * @type {Frustum}
             * @readonly
             */
            this.pickFrustum = null;

            // Internal. Keeps track of the current pick color.
            this.pickColor = new Color(0, 0, 0, 1);

            /**
             * The objects at the current pick point.
             * @type {PickedObjectList}
             * @readonly
             */
            this.objectsAtPickPoint = new PickedObjectList();

            // Intentionally not documented.
            this.pixelScale = 1;
        };

        // Internal use. Intentionally not documented.
        DrawContext.unitCubeKey = "DrawContextUnitCubeKey";
        DrawContext.unitCubeElementsKey = "DrawContextUnitCubeElementsKey";
        DrawContext.unitQuadKey = "DrawContextUnitQuadKey";
        DrawContext.unitQuadKey3 = "DrawContextUnitQuadKey3";

        /**
         * Prepare this draw context for the drawing of a new frame.
         */
        DrawContext.prototype.reset = function () {
            // Reset the draw context's internal properties.
            this.screenCreditController.clear();
            this.surfaceRenderables = []; // clears the surface renderables array
            this.orderedRenderingMode = false;
            this.orderedRenderables = []; // clears the ordered renderables array
            this.screenRenderables = [];
            this.orderedRenderablesCounter = 0;

            // Advance the per-frame timestamp.
            var previousTimestamp = this.timestamp;
            this.timestamp = Date.now();
            if (this.timestamp === previousTimestamp)
                ++this.timestamp;

            // Reset properties set by the WorldWindow every frame.
            this.redrawRequested = false;
            this.globe = null;
            this.globeStateKey = null;
            this.layers = null;
            this.currentLayer = null;
            this.navigatorState = null;
            this.terrain = null;
            this.verticalExaggeration = 1;
            this.frameStatistics = null;
            this.accumulateOrderedRenderables = true;

            // Reset picking properties that may be set by the WorldWindow.
            this.pickingMode = false;
            this.pickTerrainOnly = false;
            this.deepPicking = false;
            this.regionPicking = false;
            this.pickPoint = null;
            this.pickRectangle = null;
            this.pickFrustum = null;
            this.pickColor = new Color(0, 0, 0, 1);
            this.objectsAtPickPoint.clear();
        };

        /**
         * Computes any values necessary to render the upcoming frame. Called after all draw context state for the
         * frame has been set.
         */
        DrawContext.prototype.update = function () {
            var gl = this.currentGlContext,
                eyePoint = this.navigatorState.eyePoint;

            this.globeStateKey = this.globe.stateKey;
            this.globe.computePositionFromPoint(eyePoint[0], eyePoint[1], eyePoint[2], this.eyePosition);
            this.screenProjection.setToScreenProjection(gl.drawingBufferWidth, gl.drawingBufferHeight);
        };

        /**
         * Notifies this draw context that the current WebGL rendering context has been lost. This function removes all
         * cached WebGL resources and resets all properties tracking the current WebGL state.
         */
        DrawContext.prototype.contextLost = function () {
            // Remove all cached WebGL resources, which are now invalid.
            this.gpuResourceCache.clear();
            this.pickFramebuffer = null;
            // Reset properties tracking the current WebGL state, which are now invalid.
            this.currentFramebuffer = null;
            this.currentProgram = null;
        };

        /**
         * Notifies this draw context that the current WebGL rendering context has been restored. This function prepares
         * this draw context to resume rendering.
         */
        DrawContext.prototype.contextRestored = function () {
            // Remove all cached WebGL resources. This cache is already cleared when the context is lost, but
            // asynchronous load operations that complete between context lost and context restored populate the cache
            // with invalid entries.
            this.gpuResourceCache.clear();
        };

        /**
         * Binds a specified WebGL framebuffer. This function also makes the framebuffer the active framebuffer.
         * @param {FramebufferTexture} framebuffer The framebuffer to bind. May be null or undefined, in which case the
         * default WebGL framebuffer is made active.
         */
        DrawContext.prototype.bindFramebuffer = function (framebuffer) {
            if (this.currentFramebuffer != framebuffer) {
                this.currentGlContext.bindFramebuffer(this.currentGlContext.FRAMEBUFFER,
                    framebuffer ? framebuffer.framebufferId : null);
                this.currentFramebuffer = framebuffer;
            }
        };

        /**
         * Binds a specified WebGL program. This function also makes the program the current program.
         * @param {GpuProgram} program The program to bind. May be null or undefined, in which case the currently
         * bound program is unbound.
         */
        DrawContext.prototype.bindProgram = function (program) {
            if (this.currentProgram != program) {
                this.currentGlContext.useProgram(program ? program.programId : null);
                this.currentProgram = program;
            }
        };

        /**
         * Binds a potentially cached WebGL program, creating and caching it if it isn't already cached.
         * This function also makes the program the current program.
         * @param {function} programConstructor The constructor to use to create the program.
         * @returns {GpuProgram} The bound program.
         * @throws {ArgumentError} If the specified constructor is null or undefined.
         */
        DrawContext.prototype.findAndBindProgram = function (programConstructor) {
            if (!programConstructor) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "findAndBindProgram",
                        "The specified program constructor is null or undefined."));
            }

            var program = this.gpuResourceCache.resourceForKey(programConstructor.key);
            if (program) {
                this.bindProgram(program);
            } else {
                try {
                    program = new programConstructor(this.currentGlContext);
                    this.bindProgram(program);
                    this.gpuResourceCache.putResource(programConstructor.key, program, program.size);
                } catch (e) {
                    Logger.log(Logger.LEVEL_SEVERE, "Error attempting to create GPU program.")
                }
            }

            return program;
        };

        /**
         * Adds a surface renderable to this draw context's surface renderable list.
         * @param {SurfaceRenderable} surfaceRenderable The surface renderable to add. May be null, in which case the
         * current surface renderable list remains unchanged.
         */
        DrawContext.prototype.addSurfaceRenderable = function (surfaceRenderable) {
            if (surfaceRenderable) {
                this.surfaceRenderables.push(surfaceRenderable);
            }
        };

        /**
         * Returns the surface renderable at the head of the surface renderable list without removing it from the list.
         * @returns {SurfaceRenderable} The first surface renderable in this draw context's surface renderable list, or
         * null if the surface renderable list is empty.
         */
        DrawContext.prototype.peekSurfaceRenderable = function () {
            if (this.surfaceRenderables.length > 0) {
                return this.surfaceRenderables[this.surfaceRenderables.length - 1];
            } else {
                return null;
            }
        };

        /**
         * Returns the surface renderable at the head of the surface renderable list and removes it from the list.
         * @returns {SurfaceRenderable} The first surface renderable in this draw context's surface renderable list, or
         * null if the surface renderable list is empty.
         */
        DrawContext.prototype.popSurfaceRenderable = function () {
            if (this.surfaceRenderables.length > 0) {
                return this.surfaceRenderables.pop();
            } else {
                return null;
            }
        };

        /**
         * Reverses the surface renderable list in place. After this function completes, the functions
         * peekSurfaceRenderable and popSurfaceRenderable return renderables in the order in which they were added to
         * the surface renderable list.
         */
        DrawContext.prototype.reverseSurfaceRenderables = function () {
            this.surfaceRenderables.reverse();
        };

        /**
         * Adds an ordered renderable to this draw context's ordered renderable list.
         * @param {OrderedRenderable} orderedRenderable The ordered renderable to add. May be null, in which case the
         * current ordered renderable list remains unchanged.
         * @param {Number} eyeDistance An optional argument indicating the ordered renderable's eye distance.
         * If this parameter is not specified then the ordered renderable must have an eyeDistance property.
         */
        DrawContext.prototype.addOrderedRenderable = function (orderedRenderable, eyeDistance) {
            if (orderedRenderable) {
                var ore = {
                    orderedRenderable: orderedRenderable,
                    insertionOrder: this.orderedRenderablesCounter++,
                    eyeDistance: eyeDistance || orderedRenderable.eyeDistance,
                    globeStateKey: this.globeStateKey
                };

                if (this.globe.continuous) {
                    ore.globeOffset = this.globe.offset;
                }

                if (ore.eyeDistance === 0) {
                    this.screenRenderables.push(ore);
                } else {
                    this.orderedRenderables.push(ore);
                }
            }
        };

        /**
         * Adds an ordered renderable to the end of this draw context's ordered renderable list, denoting it as the
         * most distant from the eye point.
         * @param {OrderedRenderable} orderedRenderable The ordered renderable to add. May be null, in which case the
         * current ordered renderable list remains unchanged.
         */
        DrawContext.prototype.addOrderedRenderableToBack = function (orderedRenderable) {
            if (orderedRenderable) {
                var ore = {
                    orderedRenderable: orderedRenderable,
                    insertionOrder: this.orderedRenderablesCounter++,
                    eyeDistance: Number.MAX_VALUE,
                    globeStateKey: this.globeStateKey
                };

                if (this.globe.continuous) {
                    ore.globeOffset = this.globe.offset;
                }

                this.orderedRenderables.push(ore);
            }
        };

        /**
         * Returns the ordered renderable at the head of the ordered renderable list without removing it from the list.
         * @returns {OrderedRenderable} The first ordered renderable in this draw context's ordered renderable list, or
         * null if the ordered renderable list is empty.
         */
        DrawContext.prototype.peekOrderedRenderable = function () {
            if (this.orderedRenderables.length > 0) {
                return this.orderedRenderables[this.orderedRenderables.length - 1].orderedRenderable;
            } else {
                return null;
            }
        };

        /**
         * Returns the ordered renderable at the head of the ordered renderable list and removes it from the list.
         * @returns {OrderedRenderable} The first ordered renderable in this draw context's ordered renderable list, or
         * null if the ordered renderable list is empty.
         */
        DrawContext.prototype.popOrderedRenderable = function () {
            if (this.orderedRenderables.length > 0) {
                var ore = this.orderedRenderables.pop();
                this.globeStateKey = ore.globeStateKey;

                if (this.globe.continuous) {
                    // Restore the globe state to that when the ordered renderable was created.
                    this.globe.offset = ore.globeOffset;
                }

                return ore.orderedRenderable;
            } else {
                return null;
            }
        };

        /**
         * Returns the ordered renderable at the head of the ordered renderable list and removes it from the list.
         * @returns {OrderedRenderable} The first ordered renderable in this draw context's ordered renderable list, or
         * null if the ordered renderable list is empty.
         */
        DrawContext.prototype.nextScreenRenderable = function () {
            if (this.screenRenderables.length > 0) {
                var ore = this.screenRenderables.shift();
                this.globeStateKey = ore.globeStateKey;

                if (this.globe.continuous) {
                    // Restore the globe state to that when the ordered renderable was created.
                    this.globe.offset = ore.globeOffset;
                }

                return ore.orderedRenderable;
            } else {
                return null;
            }
        };

        /**
         * Sorts the ordered renderable list from nearest to the eye point to farthest from the eye point.
         */
        DrawContext.prototype.sortOrderedRenderables = function () {
            // Sort the ordered renderables by eye distance from front to back and then by insertion time. The ordered
            // renderable peek and pop access the back of the ordered renderable list, thereby causing ordered renderables to
            // be processed from back to front.

            this.orderedRenderables.sort(function (oreA, oreB) {
                var eA = oreA.eyeDistance,
                    eB = oreB.eyeDistance;

                if (eA < eB) { // orA is closer to the eye than orB; sort orA before orB
                    return -1;
                } else if (eA > eB) { // orA is farther from the eye than orB; sort orB before orA
                    return 1;
                } else { // orA and orB are the same distance from the eye; sort them based on insertion time
                    var tA = oreA.insertionOrder,
                        tB = oreB.insertionOrder;

                    if (tA > tB) {
                        return -1;
                    } else if (tA < tB) {
                        return 1;
                    } else {
                        return 0;
                    }
                }
            });
        };

        /**
         * Reads the color from the current render buffer at a specified point. Used during picking to identify the item most
         * recently affecting the pixel at the specified point.
         * @param {Vec2} pickPoint The current pick point.
         * @returns {Color} The color at the pick point.
         */
        DrawContext.prototype.readPickColor = function (pickPoint) {
            var glPickPoint = this.navigatorState.convertPointToViewport(pickPoint, new Vec2(0, 0)),
                colorBytes = new Uint8Array(4);

            this.currentGlContext.readPixels(glPickPoint[0], glPickPoint[1], 1, 1, this.currentGlContext.RGBA,
                this.currentGlContext.UNSIGNED_BYTE, colorBytes);

            if (this.clearColor.equalsBytes(colorBytes)) {
                return null;
            }

            return Color.colorFromByteArray(colorBytes);
        };

        /**
         * Reads the current pick buffer colors in a specified rectangle. Used during region picking to identify
         * the items not occluded.
         * @param {Rectangle} pickRectangle The rectangle for which to read the colors.
         * @returns {{}} An object containing the unique colors in the specified rectangle, excluding the current
         * clear color. The colors are referenced by their byte string
         * (see [Color.toByteString]{@link Color#toByteString}.
         */
        DrawContext.prototype.readPickColors = function (pickRectangle) {
            var gl = this.currentGlContext,
                colorBytes = new Uint8Array(pickRectangle.width * pickRectangle.height * 4),
                uniqueColors = {},
                color,
                blankColor = new Color(0, 0, 0, 0),
                packAlignment = gl.getParameter(gl.PACK_ALIGNMENT);

            gl.pixelStorei(gl.PACK_ALIGNMENT, 1); // read byte aligned
            this.currentGlContext.readPixels(pickRectangle.x, pickRectangle.y,
                pickRectangle.width, pickRectangle.height,
                gl.RGBA, gl.UNSIGNED_BYTE, colorBytes);
            gl.pixelStorei(gl.PACK_ALIGNMENT, packAlignment); // restore the pack alignment

            for (var i = 0, len = pickRectangle.width * pickRectangle.height; i < len; i++) {
                var k = i * 4;
                color = Color.colorFromBytes(colorBytes[k], colorBytes[k + 1], colorBytes[k + 2], colorBytes[k + 3]);
                if (color.equals(this.clearColor) || color.equals(blankColor))
                    continue;
                uniqueColors[color.toByteString()] = color;
            }

            return uniqueColors;
        };

        /**
         * Determines whether a specified picked object is under the pick point, and if it is adds it to this draw
         * context's list of picked objects. This method should be called by shapes during ordered rendering
         * after the shape is drawn. If this draw context is in single-picking mode, the specified pickable object
         * is added to the list of picked objects whether or not it is under the pick point.
         * @param pickableObject
         * @returns {null}
         */
        DrawContext.prototype.resolvePick = function (pickableObject) {
            if (!(pickableObject.userObject instanceof SurfaceShape) && this.deepPicking && !this.regionPicking) {
                var color = this.readPickColor(this.pickPoint);
                if (!color) { // getPickColor returns null if the pick point selects the clear color
                    return null;
                }

                if (pickableObject.color.equals(color)) {
                    this.addPickedObject(pickableObject);
                }
            } else {
                // Don't resolve. Just add the object to the pick list. It will be resolved later.
                this.addPickedObject(pickableObject);
            }
        };

        /**
         * Adds an object to the current picked-object list. The list identifies objects that are at the pick point
         * but not necessarily the top-most object.
         * @param  {PickedObject} pickedObject The object to add.
         */
        DrawContext.prototype.addPickedObject = function (pickedObject) {
            if (pickedObject) {
                this.objectsAtPickPoint.add(pickedObject);
            }
        };

        /**
         * Computes a unique color to use as a pick color.
         * @returns {Color} A unique color.
         */
        DrawContext.prototype.uniquePickColor = function () {
            var color = this.pickColor.nextColor().clone();

            return color.equals(this.clearColor) ? color.nextColor() : color;
        };

        /**
         * Creates an off-screen WebGL framebuffer for use during picking and stores it in this draw context. The
         * framebuffer width and height match the WebGL rendering context's drawingBufferWidth and drawingBufferHeight.
         */
        DrawContext.prototype.makePickFramebuffer = function () {
            var gl = this.currentGlContext,
                width = gl.drawingBufferWidth,
                height = gl.drawingBufferHeight;

            if (!this.pickFramebuffer ||
                this.pickFramebuffer.width != width ||
                this.pickFramebuffer.height != height) {

                this.pickFramebuffer = new FramebufferTexture(gl, width, height, true); // enable depth buffering
            }

            return this.pickFramebuffer;
        };

        /**
         * Creates a pick frustum for the current pick point and stores it in this draw context. If this context's
         * pick rectangle is null or undefined then a pick rectangle is also computed and assigned to this context.
         * If the existing pick rectangle extends beyond the viewport then it is truncated by this method to fit
         * within the viewport.
         * This method assumes that this draw context's pick point or pick rectangle has been set. It returns
         * false if neither one of these exists.
         *
         * @returns {Boolean} <code>true</code> if the pick frustum could be created, otherwise <code>false</code>.
         */
        DrawContext.prototype.makePickFrustum = function () {
            if (!this.pickPoint && !this.pickRectangle) {
                return false;
            }

            var lln, llf, lrn, lrf, uln, ulf, urn, urf, // corner points of frustum
                nl, nr, nt, nb, nn, nf, // normal vectors of frustum planes
                l, r, t, b, n, f, // frustum planes
                va, vb = new Vec3(0, 0, 0), // vectors formed by the corner points
                apertureRadius = 2, // radius of pick window in screen coordinates
                screenPoint = new Vec3(0, 0, 0),
                pickPoint,
                pickRectangle = this.pickRectangle,
                viewport = this.navigatorState.viewport;

            // Compute the pick rectangle if necessary.
            if (!pickRectangle) {
                pickPoint = this.navigatorState.convertPointToViewport(this.pickPoint, new Vec2(0, 0));
                pickRectangle = new Rectangle(
                    pickPoint[0] - apertureRadius,
                    pickPoint[1] - apertureRadius,
                    2 * apertureRadius,
                    2 * apertureRadius);
            }

            // Clamp the pick rectangle to the viewport.

            var xl = pickRectangle.x,
                xr = pickRectangle.x + pickRectangle.width,
                yb = pickRectangle.y,
                yt = pickRectangle.y + pickRectangle.height;

            if (xr < 0 || yt < 0 || xl > viewport.x + viewport.width || yb > viewport.y + viewport.height) {
                return false; // pick rectangle is outside the viewport.
            }

            pickRectangle.x = WWMath.clamp(xl, viewport.x, viewport.x + viewport.width);
            pickRectangle.y = WWMath.clamp(yb, viewport.y, viewport.y + viewport.height);
            pickRectangle.width = WWMath.clamp(xr, viewport.x, viewport.x + viewport.width) - pickRectangle.x;
            pickRectangle.height = WWMath.clamp(yt, viewport.y, viewport.y + viewport.height) - pickRectangle.y;
            this.pickRectangle = pickRectangle;

            // Compute the pick frustum.

            screenPoint[0] = pickRectangle.x;
            screenPoint[1] = pickRectangle.y;
            screenPoint[2] = 0;
            this.navigatorState.unProject(screenPoint, lln = new Vec3(0, 0, 0));

            screenPoint[0] = pickRectangle.x;
            screenPoint[1] = pickRectangle.y;
            screenPoint[2] = 1;
            this.navigatorState.unProject(screenPoint, llf = new Vec3(0, 0, 0));

            screenPoint[0] = pickRectangle.x + pickRectangle.width;
            screenPoint[1] = pickRectangle.y;
            screenPoint[2] = 0;
            this.navigatorState.unProject(screenPoint, lrn = new Vec3(0, 0, 0));

            screenPoint[0] = pickRectangle.x + pickRectangle.width;
            screenPoint[1] = pickRectangle.y;
            screenPoint[2] = 1;
            this.navigatorState.unProject(screenPoint, lrf = new Vec3(0, 0, 0));

            screenPoint[0] = pickRectangle.x;
            screenPoint[1] = pickRectangle.y + pickRectangle.height;
            screenPoint[2] = 0;
            this.navigatorState.unProject(screenPoint, uln = new Vec3(0, 0, 0));

            screenPoint[0] = pickRectangle.x;
            screenPoint[1] = pickRectangle.y + pickRectangle.height;
            screenPoint[2] = 1;
            this.navigatorState.unProject(screenPoint, ulf = new Vec3(0, 0, 0));

            screenPoint[0] = pickRectangle.x + pickRectangle.width;
            screenPoint[1] = pickRectangle.y + pickRectangle.height;
            screenPoint[2] = 0;
            this.navigatorState.unProject(screenPoint, urn = new Vec3(0, 0, 0));

            screenPoint[0] = pickRectangle.x + pickRectangle.width;
            screenPoint[1] = pickRectangle.y + pickRectangle.height;
            screenPoint[2] = 1;
            this.navigatorState.unProject(screenPoint, urf = new Vec3(0, 0, 0));

            va = new Vec3(ulf[0] - lln[0], ulf[1] - lln[1], ulf[2] - lln[2]);
            vb.set(uln[0] - llf[0], uln[1] - llf[1], uln[2] - llf[2]);
            nl = va.cross(vb);
            l = new Plane(nl[0], nl[1], nl[2], -nl.dot(lln));
            l.normalize();

            va = new Vec3(urn[0] - lrf[0], urn[1] - lrf[1], urn[2] - lrf[2]);
            vb.set(urf[0] - lrn[0], urf[1] - lrn[1], urf[2] - lrn[2]);
            nr = va.cross(vb);
            r = new Plane(nr[0], nr[1], nr[2], -nr.dot(lrn));
            r.normalize();

            va = new Vec3(ulf[0] - urn[0], ulf[1] - urn[1], ulf[2] - urn[2]);
            vb.set(urf[0] - uln[0], urf[1] - uln[1], urf[2] - uln[2]);
            nt = va.cross(vb);
            t = new Plane(nt[0], nt[1], nt[2], -nt.dot(uln));
            t.normalize();

            va = new Vec3(lrf[0] - lln[0], lrf[1] - lln[1], lrf[2] - lln[2]);
            vb.set(llf[0] - lrn[0], llf[1] - lrn[1], llf[2] - lrn[2]);
            nb = va.cross(vb);
            b = new Plane(nb[0], nb[1], nb[2], -nb.dot(lrn));
            b.normalize();

            va = new Vec3(uln[0] - lrn[0], uln[1] - lrn[1], uln[2] - lrn[2]);
            vb.set(urn[0] - lln[0], urn[1] - lln[1], urn[2] - lln[2]);
            nn = va.cross(vb);
            n = new Plane(nn[0], nn[1], nn[2], -nn.dot(lln));
            n.normalize();

            va = new Vec3(urf[0] - llf[0], urf[1] - llf[1], urf[2] - llf[2]);
            vb.set(ulf[0] - lrf[0], ulf[1] - lrf[1], ulf[2] - lrf[2]);
            nf = va.cross(vb);
            f = new Plane(nf[0], nf[1], nf[2], -nf.dot(llf));
            f.normalize();

            this.pickFrustum = new Frustum(l, r, b, t, n, f);

            return true;
        };

        /**
         * Indicates whether an extent is smaller than a specified number of pixels.
         * @param {BoundingBox} extent The extent to test.
         * @param {Number} numPixels The number of pixels below which the extent is considered small.
         * @returns {Boolean} True if the extent is smaller than the specified number of pixels, otherwise false.
         * Returns false if the extent is null or undefined.
         */
        DrawContext.prototype.isSmall = function (extent, numPixels) {
            if (!extent) {
                return false;
            }

            var distance = this.navigatorState.eyePoint.distanceTo(extent.center),
                pixelSize = this.navigatorState.pixelSizeAtDistance(distance);

            return (2 * extent.radius) < (numPixels * pixelSize); // extent diameter less than size of num pixels
        };

        /**
         * Returns the VBO ID of an array buffer containing a unit cube expressed as eight 3D vertices at (0, 1, 0),
         * (0, 0, 0), (1, 1, 0), (1, 0, 0), (0, 1, 1), (0, 0, 1), (1, 1, 1) and (1, 0, 1). The buffer is created on
         * first use and cached. Subsequent calls to this method return the cached buffer.
         * @returns {Object} The VBO ID identifying the array buffer.
         */
        DrawContext.prototype.unitCubeBuffer = function () {
            var vboId = this.gpuResourceCache.resourceForKey(DrawContext.unitCubeKey);

            if (!vboId) {
                var gl = this.currentGlContext,
                    points = new Float32Array(24),
                    i = 0;

                points[i++] = 0; // upper left corner, z = 0
                points[i++] = 1;
                points[i++] = 0;
                points[i++] = 0; // lower left corner, z = 0
                points[i++] = 0;
                points[i++] = 0;
                points[i++] = 1; // upper right corner, z = 0
                points[i++] = 1;
                points[i++] = 0;
                points[i++] = 1; // lower right corner, z = 0
                points[i++] = 0;
                points[i++] = 0;

                points[i++] = 0; // upper left corner, z = 1
                points[i++] = 1;
                points[i++] = 1;
                points[i++] = 0; // lower left corner, z = 1
                points[i++] = 0;
                points[i++] = 1;
                points[i++] = 1; // upper right corner, z = 1
                points[i++] = 1;
                points[i++] = 1;
                points[i++] = 1; // lower right corner, z = 1
                points[i++] = 0;
                points[i] = 1;

                vboId = gl.createBuffer();
                gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
                gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
                gl.bindBuffer(gl.ARRAY_BUFFER, null);
                this.frameStatistics.incrementVboLoadCount(1);

                this.gpuResourceCache.putResource(DrawContext.unitCubeKey, vboId, points.length * 4);
            }

            return vboId;
        };

        /**
         * Returns the VBO ID of a element array buffer containing the tessellation of a unit cube expressed as
         * a single buffer containing both triangle indices and line indices. This is intended for use in conjunction
         * with <code>unitCubeBuffer</code>. The unit cube's interior and outline may be rasterized as shown in the
         * following WebGL pseudocode:
         * <code><pre>
         * // Assumes that the VBO returned by unitCubeBuffer is used as the source of vertex positions.
         * bindBuffer(ELEMENT_ARRAY_BUFFER, drawContext.unitCubeElements());
         * drawElements(TRIANGLES, 36, UNSIGNED_SHORT, 0); // draw the unit cube interior
         * drawElements(LINES, 24, UNSIGNED_SHORT, 72); // draw the unit cube outline
         * </pre></code>
         * The buffer is created on first use
         * and cached. Subsequent calls to this method return the cached buffer.
         * @returns {Object} The VBO ID identifying the element array buffer.
         */
        DrawContext.prototype.unitCubeElements = function () {
            var vboId = this.gpuResourceCache.resourceForKey(DrawContext.unitCubeElementsKey);

            if (!vboId) {
                var gl = this.currentGlContext,
                    elems = new Int16Array(60),
                    i = 0;

                // interior

                elems[i++] = 1; // -z face
                elems[i++] = 0;
                elems[i++] = 3;
                elems[i++] = 3;
                elems[i++] = 0;
                elems[i++] = 2;

                elems[i++] = 4; // +z face
                elems[i++] = 5;
                elems[i++] = 6;
                elems[i++] = 6;
                elems[i++] = 5;
                elems[i++] = 7;

                elems[i++] = 5; // -y face
                elems[i++] = 1;
                elems[i++] = 7;
                elems[i++] = 7;
                elems[i++] = 1;
                elems[i++] = 3;

                elems[i++] = 6; // +y face
                elems[i++] = 2;
                elems[i++] = 4;
                elems[i++] = 4;
                elems[i++] = 2;
                elems[i++] = 0;

                elems[i++] = 4; // -x face
                elems[i++] = 0;
                elems[i++] = 5;
                elems[i++] = 5;
                elems[i++] = 0;
                elems[i++] = 1;

                elems[i++] = 7; // +x face
                elems[i++] = 3;
                elems[i++] = 6;
                elems[i++] = 6;
                elems[i++] = 3;
                elems[i++] = 2;

                // outline

                elems[i++] = 0; // left, -z
                elems[i++] = 1;
                elems[i++] = 1; // bottom, -z
                elems[i++] = 3;
                elems[i++] = 3; // right, -z
                elems[i++] = 2;
                elems[i++] = 2; // top, -z
                elems[i++] = 0;

                elems[i++] = 4; // left, +z
                elems[i++] = 5;
                elems[i++] = 5; // bottom, +z
                elems[i++] = 7;
                elems[i++] = 7; // right, +z
                elems[i++] = 6;
                elems[i++] = 6; // top, +z
                elems[i++] = 4;

                elems[i++] = 0; // upper left
                elems[i++] = 4;
                elems[i++] = 5; // lower left
                elems[i++] = 1;
                elems[i++] = 2; // upper right
                elems[i++] = 6;
                elems[i++] = 7; // lower right
                elems[i] = 3;

                vboId = gl.createBuffer();
                gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vboId);
                gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, elems, gl.STATIC_DRAW);
                gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
                this.frameStatistics.incrementVboLoadCount(1);

                this.gpuResourceCache.putResource(DrawContext.unitCubeElementsKey, vboId, elems.length * 2);
            }

            return vboId;
        };

        /**
         * Returns the VBO ID of a buffer containing a unit quadrilateral expressed as four 2D vertices at (0, 1),
         * (0, 0), (1, 1) and (1, 0). The four vertices are in the order required by a triangle strip. The buffer is
         * created on first use and cached. Subsequent calls to this method return the cached buffer.
         * @returns {Object} The VBO ID identifying the vertex buffer.
         */
        DrawContext.prototype.unitQuadBuffer = function () {
            var vboId = this.gpuResourceCache.resourceForKey(DrawContext.unitQuadKey);

            if (!vboId) {
                var gl = this.currentGlContext,
                    points = new Float32Array(8);

                points[0] = 0; // upper left corner
                points[1] = 1;
                points[2] = 0; // lower left corner
                points[3] = 0;
                points[4] = 1; // upper right corner
                points[5] = 1;
                points[6] = 1; // lower right corner
                points[7] = 0;

                vboId = gl.createBuffer();
                gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
                gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
                gl.bindBuffer(gl.ARRAY_BUFFER, null);
                this.frameStatistics.incrementVboLoadCount(1);

                this.gpuResourceCache.putResource(DrawContext.unitQuadKey, vboId, points.length * 4);
            }

            return vboId;
        };

        /**
         * Returns the VBO ID of a buffer containing a unit quadrilateral expressed as four 3D vertices at (0, 1, 0),
         * (0, 0, 0), (1, 1, 0) and (1, 0, 0).
         * The four vertices are in the order required by a triangle strip. The buffer is created
         * on first use and cached. Subsequent calls to this method return the cached buffer.
         * @returns {Object} The VBO ID identifying the vertex buffer.
         */
        DrawContext.prototype.unitQuadBuffer3 = function () {
            var vboId = this.gpuResourceCache.resourceForKey(DrawContext.unitQuadKey3);

            if (!vboId) {
                var gl = this.currentGlContext,
                    points = new Float32Array(12);

                points[0] = 0; // upper left corner
                points[1] = 1;
                points[2] = 0;
                points[3] = 0; // lower left corner
                points[4] = 0;
                points[5] = 0;
                points[6] = 1; // upper right corner
                points[7] = 1;
                points[8] = 0;
                points[9] = 1; // lower right corner
                points[10] = 0;
                points[11] = 0;

                vboId = gl.createBuffer();
                gl.bindBuffer(gl.ARRAY_BUFFER, vboId);
                gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
                gl.bindBuffer(gl.ARRAY_BUFFER, null);
                this.frameStatistics.incrementVboLoadCount(1);

                this.gpuResourceCache.putResource(DrawContext.unitQuadKey3, vboId, points.length * 4);
            }

            return vboId;
        };

        /**
         * Computes a Cartesian point at a location on the surface of this terrain according to a specified
         * altitude mode. If there is no current terrain, this function approximates the returned point by assuming
         * the terrain is the globe's ellipsoid.
         * @param {Number} latitude The location's latitude.
         * @param {Number} longitude The location's longitude.
         * @param {Number} offset Distance above the terrain, in meters relative to the specified altitude mode, at
         * which to compute the point.
         * @param {String} altitudeMode The altitude mode to use to compute the point. Recognized values are
         * WorldWind.ABSOLUTE, WorldWind.CLAMP_TO_GROUND and
         * WorldWind.RELATIVE_TO_GROUND. The mode WorldWind.ABSOLUTE is used if the
         * specified mode is null, undefined or unrecognized, or if the specified location is outside this terrain.
         * @param {Vec3} result A pre-allocated Vec3 in which to return the computed point.
         * @returns {Vec3} The specified result parameter, set to the coordinates of the computed point.
         * @throws {ArgumentError} If the specified result argument is null or undefined.
         */
        DrawContext.prototype.surfacePointForMode = function (latitude, longitude, offset, altitudeMode, result) {
            if (!result) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "DrawContext", "surfacePointForMode", "missingResult"));
            }

            if (this.terrain) {
                this.terrain.surfacePointForMode(latitude, longitude, offset, altitudeMode, result);
            } else {
                var h = offset + this.globe.elevationAtLocation(latitude, longitude) * this.verticalExaggeration;
                this.globe.computePointFromPosition(latitude, longitude, h, result);
            }

            return result;
        };

        return DrawContext;
    });