define([
        '../Core/Cartographic',
        '../Core/Cartesian2',
        '../Core/Cartesian3',
        '../Core/Math',
        '../Core/Check',
        '../Core/ComponentDatatype',
        '../Core/defaultValue',
        '../Core/defined',
        '../Core/defineProperties',
        '../Core/EncodedCartesian3',
        '../Core/GeometryInstanceAttribute',
        '../Core/Matrix4',
        '../Core/Rectangle',
        '../Core/Transforms',
        '../Renderer/ShaderSource',
        '../Scene/PerInstanceColorAppearance',
        '../Shaders/ShadowVolumeAppearanceFS'
], function(
        Cartographic,
        Cartesian2,
        Cartesian3,
        CesiumMath,
        Check,
        ComponentDatatype,
        defaultValue,
        defined,
        defineProperties,
        EncodedCartesian3,
        GeometryInstanceAttribute,
        Matrix4,
        Rectangle,
        Transforms,
        ShaderSource,
        PerInstanceColorAppearance,
        ShadowVolumeAppearanceFS) {
    'use strict';

    var projectionExtentDefines = {
        eastMostYhighDefine : '',
        eastMostYlowDefine : '',
        westMostYhighDefine : '',
        westMostYlowDefine : ''
    };

    /**
     * Creates shaders for a ClassificationPrimitive to use a given Appearance, as well as for picking.
     *
     * @param {Boolean} extentsCulling Discard fragments outside the instance's texture coordinate extents.
     * @param {Boolean} planarExtents If true, texture coordinates will be computed using planes instead of spherical coordinates.
     * @param {Appearance} appearance An Appearance to be used with a ClassificationPrimitive via GroundPrimitive.
     * @private
     */
    function ShadowVolumeAppearance(extentsCulling, planarExtents, appearance) {
        //>>includeStart('debug', pragmas.debug);
        Check.typeOf.bool('extentsCulling', extentsCulling);
        Check.typeOf.bool('planarExtents', planarExtents);
        Check.typeOf.object('appearance', appearance);
        //>>includeEnd('debug');

        // Compute shader dependencies
        var colorShaderDependencies = new ShaderDependencies();
        colorShaderDependencies.requiresTextureCoordinates = extentsCulling;
        colorShaderDependencies.requiresEC = !appearance.flat;

        var pickShaderDependencies = new ShaderDependencies();
        pickShaderDependencies.requiresTextureCoordinates = extentsCulling;

        if (appearance instanceof PerInstanceColorAppearance) {
            // PerInstanceColorAppearance doesn't have material.shaderSource, instead it has its own vertex and fragment shaders
            colorShaderDependencies.requiresNormalEC = !appearance.flat;
        } else {
            // Scan material source for what hookups are needed. Assume czm_materialInput materialInput.
            var materialShaderSource = appearance.material.shaderSource + '\n' + appearance.fragmentShaderSource;

            colorShaderDependencies.normalEC = materialShaderSource.indexOf('materialInput.normalEC') !== -1 || materialShaderSource.indexOf('czm_getDefaultMaterial') !== -1;
            colorShaderDependencies.positionToEyeEC = materialShaderSource.indexOf('materialInput.positionToEyeEC') !== -1;
            colorShaderDependencies.tangentToEyeMatrix = materialShaderSource.indexOf('materialInput.tangentToEyeMatrix') !== -1;
            colorShaderDependencies.st = materialShaderSource.indexOf('materialInput.st') !== -1;
        }

        this._colorShaderDependencies = colorShaderDependencies;
        this._pickShaderDependencies = pickShaderDependencies;
        this._appearance = appearance;
        this._extentsCulling = extentsCulling;
        this._planarExtents = planarExtents;
    }

    /**
     * Create the fragment shader for a ClassificationPrimitive's color pass when rendering for color.
     *
     * @param {Boolean} columbusView2D Whether the shader will be used for Columbus View or 2D.
     * @returns {ShaderSource} Shader source for the fragment shader.
     */
    ShadowVolumeAppearance.prototype.createFragmentShader = function(columbusView2D) {
        //>>includeStart('debug', pragmas.debug);
        Check.typeOf.bool('columbusView2D', columbusView2D);
        //>>includeEnd('debug');

        var appearance = this._appearance;
        var dependencies = this._colorShaderDependencies;

        var defines = [];
        if (!columbusView2D && !this._planarExtents) {
            defines.push('SPHERICAL');
        }
        if (dependencies.requiresEC) {
            defines.push('REQUIRES_EC');
        }
        if (dependencies.requiresWC) {
            defines.push('REQUIRES_WC');
        }
        if (dependencies.requiresTextureCoordinates) {
            defines.push('TEXTURE_COORDINATES');
        }
        if (this._extentsCulling) {
            defines.push('CULL_FRAGMENTS');
        }
        if (dependencies.requiresNormalEC) {
            defines.push('NORMAL_EC');
        }
        if (appearance instanceof PerInstanceColorAppearance) {
            defines.push('PER_INSTANCE_COLOR');
        }

        // Material inputs. Use of parameters in the material is different
        // from requirement of the parameters in the overall shader, for example,
        // texture coordinates may be used for fragment culling but not for the material itself.
        if (dependencies.normalEC) {
            defines.push('USES_NORMAL_EC');
        }
        if (dependencies.positionToEyeEC) {
            defines.push('USES_POSITION_TO_EYE_EC');
        }
        if (dependencies.tangentToEyeMatrix) {
            defines.push('USES_TANGENT_TO_EYE');
        }
        if (dependencies.st) {
            defines.push('USES_ST');
        }

        if (appearance.flat) {
            defines.push('FLAT');
        }

        var materialSource = '';
        if (!(appearance instanceof PerInstanceColorAppearance)) {
            materialSource = appearance.material.shaderSource;
        }

        return new ShaderSource({
            defines : defines,
            sources : [materialSource, ShadowVolumeAppearanceFS]
        });
    };

    ShadowVolumeAppearance.prototype.createPickFragmentShader = function(columbusView2D) {
        //>>includeStart('debug', pragmas.debug);
        Check.typeOf.bool('columbusView2D', columbusView2D);
        //>>includeEnd('debug');

        var dependencies = this._pickShaderDependencies;

        var defines = ['PICK'];
        if (!columbusView2D && !this._planarExtents) {
            defines.push('SPHERICAL');
        }
        if (dependencies.requiresEC) {
            defines.push('REQUIRES_EC');
        }
        if (dependencies.requiresWC) {
            defines.push('REQUIRES_WC');
        }
        if (dependencies.requiresTextureCoordinates) {
            defines.push('TEXTURE_COORDINATES');
        }
        if (this._extentsCulling) {
            defines.push('CULL_FRAGMENTS');
        }
        return new ShaderSource({
            defines : defines,
            sources : [ShadowVolumeAppearanceFS],
            pickColorQualifier : 'varying'
        });
    };

    /**
     * Create the vertex shader for a ClassificationPrimitive's color pass on the final of 3 shadow volume passes
     *
     * @param {String[]} defines External defines to pass to the vertex shader.
     * @param {String} vertexShaderSource ShadowVolumeAppearanceVS with any required modifications for computing position.
     * @param {Boolean} columbusView2D Whether the shader will be used for Columbus View or 2D.
     * @param {MapProjection} mapProjection Current scene's map projection.
     * @returns {String} Shader source for the vertex shader.
     */
    ShadowVolumeAppearance.prototype.createVertexShader = function(defines, vertexShaderSource, columbusView2D, mapProjection) {
        //>>includeStart('debug', pragmas.debug);
        Check.defined('defines', defines);
        Check.typeOf.string('vertexShaderSource', vertexShaderSource);
        Check.typeOf.bool('columbusView2D', columbusView2D);
        Check.defined('mapProjection', mapProjection);
        //>>includeEnd('debug');
        return createShadowVolumeAppearanceVS(this._colorShaderDependencies, this._planarExtents, columbusView2D, defines, vertexShaderSource, this._appearance, mapProjection);
    };

    /**
     * Create the vertex shader for a ClassificationPrimitive's pick pass on the final of 3 shadow volume passes
     *
     * @param {String[]} defines External defines to pass to the vertex shader.
     * @param {String} vertexShaderSource ShadowVolumeAppearanceVS with any required modifications for computing position and picking.
     * @param {Boolean} columbusView2D Whether the shader will be used for Columbus View or 2D.
     * @param {MapProjection} mapProjection Current scene's map projection.
     * @returns {String} Shader source for the vertex shader.
     */
    ShadowVolumeAppearance.prototype.createPickVertexShader = function(defines, vertexShaderSource, columbusView2D, mapProjection) {
        //>>includeStart('debug', pragmas.debug);
        Check.defined('defines', defines);
        Check.typeOf.string('vertexShaderSource', vertexShaderSource);
        Check.typeOf.bool('columbusView2D', columbusView2D);
        Check.defined('mapProjection', mapProjection);
        //>>includeEnd('debug');
        return createShadowVolumeAppearanceVS(this._pickShaderDependencies, this._planarExtents, columbusView2D, defines, vertexShaderSource, undefined, mapProjection);
    };

    var longitudeExtentsCartesianScratch = new Cartesian3();
    var longitudeExtentsCartographicScratch = new Cartographic();
    var longitudeExtentsEncodeScratch = {
        high : 0.0,
        low : 0.0
    };
    function createShadowVolumeAppearanceVS(shaderDependencies, planarExtents, columbusView2D, defines, vertexShaderSource, appearance, mapProjection) {
        var allDefines = defines.slice();

        if (projectionExtentDefines.eastMostYhighDefine === '') {
            var eastMostCartographic = longitudeExtentsCartographicScratch;
            eastMostCartographic.longitude = CesiumMath.PI;
            eastMostCartographic.latitude = 0.0;
            eastMostCartographic.height = 0.0;
            var eastMostCartesian = mapProjection.project(eastMostCartographic, longitudeExtentsCartesianScratch);
            var encoded = EncodedCartesian3.encode(eastMostCartesian.x, longitudeExtentsEncodeScratch);
            projectionExtentDefines.eastMostYhighDefine = 'EAST_MOST_X_HIGH ' + encoded.high.toFixed((encoded.high + '').length + 1);
            projectionExtentDefines.eastMostYlowDefine = 'EAST_MOST_X_LOW ' + encoded.low.toFixed((encoded.low + '').length + 1);

            var westMostCartographic = longitudeExtentsCartographicScratch;
            westMostCartographic.longitude = -CesiumMath.PI;
            westMostCartographic.latitude = 0.0;
            westMostCartographic.height = 0.0;
            var westMostCartesian = mapProjection.project(westMostCartographic, longitudeExtentsCartesianScratch);
            encoded = EncodedCartesian3.encode(westMostCartesian.x, longitudeExtentsEncodeScratch);
            projectionExtentDefines.westMostYhighDefine = 'WEST_MOST_X_HIGH ' + encoded.high.toFixed((encoded.high + '').length + 1);
            projectionExtentDefines.westMostYlowDefine = 'WEST_MOST_X_LOW ' + encoded.low.toFixed((encoded.low + '').length + 1);
        }

        if (columbusView2D) {
            allDefines.push(projectionExtentDefines.eastMostYhighDefine);
            allDefines.push(projectionExtentDefines.eastMostYlowDefine);
            allDefines.push(projectionExtentDefines.westMostYhighDefine);
            allDefines.push(projectionExtentDefines.westMostYlowDefine);
        }

        if (defined(appearance) && appearance instanceof PerInstanceColorAppearance) {
            allDefines.push('PER_INSTANCE_COLOR');
        }
        if (shaderDependencies.requiresTextureCoordinates) {
            allDefines.push('TEXTURE_COORDINATES');
            if (!(planarExtents || columbusView2D)) {
                allDefines.push('SPHERICAL');
            }
            if (columbusView2D) {
                allDefines.push('COLUMBUS_VIEW_2D');
            }
        }

        return new ShaderSource({
            defines : allDefines,
            sources : [vertexShaderSource]
        });
    }

    /**
     * Tracks shader dependencies.
     * @private
     */
    function ShaderDependencies() {
        this._requiresEC = false;
        this._requiresWC = false; // depends on eye coordinates, needed for material and for phong
        this._requiresNormalEC = false; // depends on eye coordinates, needed for material
        this._requiresTextureCoordinates = false; // depends on world coordinates, needed for material and for culling

        this._usesNormalEC = false;
        this._usesPositionToEyeEC = false;
        this._usesTangentToEyeMat = false;
        this._usesSt = false;
    }

    defineProperties(ShaderDependencies.prototype, {
        // Set when assessing final shading (flat vs. phong) and culling using computed texture coordinates
        requiresEC : {
            get : function() {
                return this._requiresEC;
            },
            set : function(value) {
                this._requiresEC = value || this._requiresEC;
            }
        },
        requiresWC : {
            get : function() {
                return this._requiresWC;
            },
            set : function(value) {
                this._requiresWC = value || this._requiresWC;
                this.requiresEC = this._requiresWC;
            }
        },
        requiresNormalEC : {
            get : function() {
                return this._requiresNormalEC;
            },
            set : function(value) {
                this._requiresNormalEC = value || this._requiresNormalEC;
                this.requiresEC = this._requiresNormalEC;
            }
        },
        requiresTextureCoordinates : {
            get : function() {
                return this._requiresTextureCoordinates;
            },
            set : function(value) {
                this._requiresTextureCoordinates = value || this._requiresTextureCoordinates;
                this.requiresWC = this._requiresTextureCoordinates;
            }
        },
        // Get/Set when assessing material hookups
        normalEC : {
            set : function(value) {
                this.requiresNormalEC = value;
                this._usesNormalEC = value;
            },
            get : function() {
                return this._usesNormalEC;
            }
        },
        tangentToEyeMatrix : {
            set : function(value) {
                this.requiresWC = value;
                this.requiresNormalEC = value;
                this._usesTangentToEyeMat = value;
            },
            get : function() {
                return this._usesTangentToEyeMat;
            }
        },
        positionToEyeEC : {
            set : function(value) {
                this.requiresEC = value;
                this._usesPositionToEyeEC = value;
            },
            get : function() {
                return this._usesPositionToEyeEC;
            }
        },
        st : {
            set : function(value) {
                this.requiresTextureCoordinates = value;
                this._usesSt = value;
            },
            get : function() {
                return this._usesSt;
            }
        }
    });

    function pointLineDistance(point1, point2, point) {
        return Math.abs((point2.y - point1.y) * point.x - (point2.x - point1.x) * point.y + point2.x * point1.y - point2.y * point1.x) / Cartesian2.distance(point2, point1);
    }

    var points2DScratch = [new Cartesian2(), new Cartesian2(), new Cartesian2(), new Cartesian2()];

    // textureCoordinateRotationPoints form 2 lines in the computed UV space that remap to desired texture coordinates.
    // This allows simulation of baked texture coordinates for EllipseGeometry, RectangleGeometry, and PolygonGeometry.
    function addTextureCoordinateRotationAttributes(attributes, textureCoordinateRotationPoints) {
        var points2D = points2DScratch;

        var minXYCorner = Cartesian2.unpack(textureCoordinateRotationPoints, 0, points2D[0]);
        var maxYCorner = Cartesian2.unpack(textureCoordinateRotationPoints, 2, points2D[1]);
        var maxXCorner = Cartesian2.unpack(textureCoordinateRotationPoints, 4, points2D[2]);

        attributes.uMaxVmax = new GeometryInstanceAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 4,
            normalize: false,
            value : [maxYCorner.x, maxYCorner.y, maxXCorner.x, maxXCorner.y]
        });

        var inverseExtentX = 1.0 / pointLineDistance(minXYCorner, maxYCorner, maxXCorner);
        var inverseExtentY = 1.0 / pointLineDistance(minXYCorner, maxXCorner, maxYCorner);

        attributes.uvMinAndExtents = new GeometryInstanceAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 4,
            normalize: false,
            value : [minXYCorner.x, minXYCorner.y, inverseExtentX, inverseExtentY]
        });
    }

    var cartographicScratch = new Cartographic();
    var cornerScratch = new Cartesian3();
    var northWestScratch = new Cartesian3();
    var southEastScratch = new Cartesian3();
    var highLowScratch = {high : 0.0, low : 0.0};
    function add2DTextureCoordinateAttributes(rectangle, projection, attributes) {
        // Compute corner positions in double precision
        var carto = cartographicScratch;
        carto.height = 0.0;

        carto.longitude = rectangle.west;
        carto.latitude = rectangle.south;

        var southWestCorner = projection.project(carto, cornerScratch);

        carto.latitude = rectangle.north;
        var northWest = projection.project(carto, northWestScratch);

        carto.longitude = rectangle.east;
        carto.latitude = rectangle.south;
        var southEast = projection.project(carto, southEastScratch);

        // Since these positions are all in the 2D plane, there's a lot of zeros
        // and a lot of repetition. So we only need to encode 4 values.
        // Encode:
        // x: x value for southWestCorner
        // y: y value for southWestCorner
        // z: y value for northWest
        // w: x value for southEast
        var valuesHigh = [0, 0, 0, 0];
        var valuesLow = [0, 0, 0, 0];
        var encoded = EncodedCartesian3.encode(southWestCorner.x, highLowScratch);
        valuesHigh[0] = encoded.high;
        valuesLow[0] = encoded.low;

        encoded = EncodedCartesian3.encode(southWestCorner.y, highLowScratch);
        valuesHigh[1] = encoded.high;
        valuesLow[1] = encoded.low;

        encoded = EncodedCartesian3.encode(northWest.y, highLowScratch);
        valuesHigh[2] = encoded.high;
        valuesLow[2] = encoded.low;

        encoded = EncodedCartesian3.encode(southEast.x, highLowScratch);
        valuesHigh[3] = encoded.high;
        valuesLow[3] = encoded.low;

        attributes.planes2D_HIGH = new GeometryInstanceAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 4,
            normalize: false,
            value : valuesHigh
        });

        attributes.planes2D_LOW = new GeometryInstanceAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 4,
            normalize: false,
            value : valuesLow
        });
    }

    var enuMatrixScratch = new Matrix4();
    var inverseEnuScratch = new Matrix4();
    var rectanglePointCartesianScratch = new Cartesian3();
    var rectangleCenterScratch = new Cartographic();
    var pointsCartographicScratch = [
        new Cartographic(),
        new Cartographic(),
        new Cartographic(),
        new Cartographic(),
        new Cartographic(),
        new Cartographic(),
        new Cartographic(),
        new Cartographic()
    ];
    /**
     * When computing planes to bound the rectangle,
     * need to factor in "bulge" and other distortion.
     * Flatten the ellipsoid-centered corners and edge-centers of the rectangle
     * into the plane of the local ENU system, compute bounds in 2D, and
     * project back to ellipsoid-centered.
     */
    function computeRectangleBounds(rectangle, ellipsoid, height, southWestCornerResult, eastVectorResult, northVectorResult) {
        // Compute center of rectangle
        var centerCartographic = Rectangle.center(rectangle, rectangleCenterScratch);
        centerCartographic.height = height;
        var centerCartesian = Cartographic.toCartesian(centerCartographic, ellipsoid, rectanglePointCartesianScratch);
        var enuMatrix = Transforms.eastNorthUpToFixedFrame(centerCartesian, ellipsoid, enuMatrixScratch);
        var inverseEnu = Matrix4.inverse(enuMatrix, inverseEnuScratch);

        var west = rectangle.west;
        var east = rectangle.east;
        var north = rectangle.north;
        var south = rectangle.south;

        var cartographics = pointsCartographicScratch;
        cartographics[0].latitude = south;
        cartographics[0].longitude = west;
        cartographics[1].latitude = north;
        cartographics[1].longitude = west;
        cartographics[2].latitude = north;
        cartographics[2].longitude = east;
        cartographics[3].latitude = south;
        cartographics[3].longitude = east;

        var longitudeCenter = (west + east) * 0.5;
        var latitudeCenter = (north + south) * 0.5;

        cartographics[4].latitude = south;
        cartographics[4].longitude = longitudeCenter;
        cartographics[5].latitude = north;
        cartographics[5].longitude = longitudeCenter;
        cartographics[6].latitude = latitudeCenter;
        cartographics[6].longitude = west;
        cartographics[7].latitude = latitudeCenter;
        cartographics[7].longitude = east;

        var minX = Number.POSITIVE_INFINITY;
        var maxX = Number.NEGATIVE_INFINITY;
        var minY = Number.POSITIVE_INFINITY;
        var maxY = Number.NEGATIVE_INFINITY;
        for (var i = 0; i < 8; i++) {
            cartographics[i].height = height;
            var pointCartesian = Cartographic.toCartesian(cartographics[i], ellipsoid, rectanglePointCartesianScratch);
            Matrix4.multiplyByPoint(inverseEnu, pointCartesian, pointCartesian);
            pointCartesian.z = 0.0; // flatten into XY plane of ENU coordinate system
            minX = Math.min(minX, pointCartesian.x);
            maxX = Math.max(maxX, pointCartesian.x);
            minY = Math.min(minY, pointCartesian.y);
            maxY = Math.max(maxY, pointCartesian.y);
        }

        var southWestCorner = southWestCornerResult;
        southWestCorner.x = minX;
        southWestCorner.y = minY;
        southWestCorner.z = 0.0;
        Matrix4.multiplyByPoint(enuMatrix, southWestCorner, southWestCorner);

        var southEastCorner = eastVectorResult;
        southEastCorner.x = maxX;
        southEastCorner.y = minY;
        southEastCorner.z = 0.0;
        Matrix4.multiplyByPoint(enuMatrix, southEastCorner, southEastCorner);
        // make eastward vector
        Cartesian3.subtract(southEastCorner, southWestCorner, eastVectorResult);

        var northWestCorner = northVectorResult;
        northWestCorner.x = minX;
        northWestCorner.y = maxY;
        northWestCorner.z = 0.0;
        Matrix4.multiplyByPoint(enuMatrix, northWestCorner, northWestCorner);
        // make eastward vector
        Cartesian3.subtract(northWestCorner, southWestCorner, northVectorResult);
    }

    var eastwardScratch = new Cartesian3();
    var northwardScratch = new Cartesian3();
    var encodeScratch = new EncodedCartesian3();
    /**
     * Gets an attributes object containing:
     * - 3 high-precision points as 6 GeometryInstanceAttributes. These points are used to compute eye-space planes.
     * - 1 texture coordinate rotation GeometryInstanceAttributes
     * - 2 GeometryInstanceAttributes used to compute high-precision points in 2D and Columbus View.
     *   These points are used to compute eye-space planes like above.
     *
     * Used to compute texture coordinates for small-area ClassificationPrimitives with materials or multiple non-overlapping instances.
     *
     * @see ShadowVolumeAppearance
     * @private
     *
     * @param {Rectangle} boundingRectangle Rectangle object that the points will approximately bound
     * @param {Number[]} textureCoordinateRotationPoints Points in the computed texture coordinate system for remapping texture coordinates
     * @param {Ellipsoid} ellipsoid Ellipsoid for converting Rectangle points to world coordinates
     * @param {MapProjection} projection The MapProjection used for 2D and Columbus View.
     * @param {Number} [height=0] The maximum height for the shadow volume.
     * @returns {Object} An attributes dictionary containing planar texture coordinate attributes.
     */
    ShadowVolumeAppearance.getPlanarTextureCoordinateAttributes = function(boundingRectangle, textureCoordinateRotationPoints, ellipsoid, projection, height) {
        //>>includeStart('debug', pragmas.debug);
        Check.typeOf.object('boundingRectangle', boundingRectangle);
        Check.defined('textureCoordinateRotationPoints', textureCoordinateRotationPoints);
        Check.typeOf.object('ellipsoid', ellipsoid);
        Check.typeOf.object('projection', projection);
        //>>includeEnd('debug');

        var corner = cornerScratch;
        var eastward = eastwardScratch;
        var northward = northwardScratch;
        computeRectangleBounds(boundingRectangle, ellipsoid, defaultValue(height, 0.0), corner, eastward, northward);

        var attributes = {};
        addTextureCoordinateRotationAttributes(attributes, textureCoordinateRotationPoints);

        var encoded = EncodedCartesian3.fromCartesian(corner, encodeScratch);
        attributes.southWest_HIGH = new GeometryInstanceAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            normalize: false,
            value : Cartesian3.pack(encoded.high, [0, 0, 0])
        });
        attributes.southWest_LOW = new GeometryInstanceAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            normalize: false,
            value : Cartesian3.pack(encoded.low, [0, 0, 0])
        });
        attributes.eastward = new GeometryInstanceAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            normalize: false,
            value : Cartesian3.pack(eastward, [0, 0, 0])
        });
        attributes.northward = new GeometryInstanceAttribute({
            componentDatatype: ComponentDatatype.FLOAT,
            componentsPerAttribute: 3,
            normalize: false,
            value : Cartesian3.pack(northward, [0, 0, 0])
        });

        add2DTextureCoordinateAttributes(boundingRectangle, projection, attributes);
        return attributes;
    };

    var spherePointScratch = new Cartesian3();
    function latLongToSpherical(latitude, longitude, ellipsoid, result) {
        var cartographic = cartographicScratch;
        cartographic.latitude = latitude;
        cartographic.longitude = longitude;
        cartographic.height = 0.0;

        var spherePoint = Cartographic.toCartesian(cartographic, ellipsoid, spherePointScratch);

        // Project into plane with vertical for latitude
        var magXY = Math.sqrt(spherePoint.x * spherePoint.x + spherePoint.y * spherePoint.y);

        // Use fastApproximateAtan2 for alignment with shader
        var sphereLatitude = CesiumMath.fastApproximateAtan2(magXY, spherePoint.z);
        var sphereLongitude = CesiumMath.fastApproximateAtan2(spherePoint.x, spherePoint.y);

        result.x = sphereLatitude;
        result.y = sphereLongitude;

        return result;
    }

    var sphericalScratch = new Cartesian2();
    /**
     * Gets an attributes object containing:
     * - the southwest corner of a rectangular area in spherical coordinates, as well as the inverse of the latitude/longitude range.
     *   These are computed using the same atan2 approximation used in the shader.
     * - 1 texture coordinate rotation GeometryInstanceAttributes
     * - 2 GeometryInstanceAttributes used to compute high-precision points in 2D and Columbus View.
     *   These points are used to compute eye-space planes like above.
     *
     * Used when computing texture coordinates for large-area ClassificationPrimitives with materials or
     * multiple non-overlapping instances.
     * @see ShadowVolumeAppearance
     * @private
     *
     * @param {Rectangle} boundingRectangle Rectangle object that the spherical extents will approximately bound
     * @param {Number[]} textureCoordinateRotationPoints Points in the computed texture coordinate system for remapping texture coordinates
     * @param {Ellipsoid} ellipsoid Ellipsoid for converting Rectangle points to world coordinates
     * @param {MapProjection} projection The MapProjection used for 2D and Columbus View.
     * @returns {Object} An attributes dictionary containing spherical texture coordinate attributes.
     */
    ShadowVolumeAppearance.getSphericalExtentGeometryInstanceAttributes = function(boundingRectangle, textureCoordinateRotationPoints, ellipsoid, projection) {
        //>>includeStart('debug', pragmas.debug);
        Check.typeOf.object('boundingRectangle', boundingRectangle);
        Check.defined('textureCoordinateRotationPoints', textureCoordinateRotationPoints);
        Check.typeOf.object('ellipsoid', ellipsoid);
        Check.typeOf.object('projection', projection);
        //>>includeEnd('debug');

        // rectangle cartographic coords !== spherical because it's on an ellipsoid
        var southWestExtents = latLongToSpherical(boundingRectangle.south, boundingRectangle.west, ellipsoid, sphericalScratch);

        var south = southWestExtents.x;
        var west = southWestExtents.y;

        var northEastExtents = latLongToSpherical(boundingRectangle.north, boundingRectangle.east, ellipsoid, sphericalScratch);
        var north = northEastExtents.x;
        var east = northEastExtents.y;

        // If the bounding rectangle crosses the IDL, rotate the spherical extents so the cross no longer happens.
        // This rotation must happen in the shader too.
        var rotationRadians = 0.0;
        if (west > east) {
            rotationRadians = CesiumMath.PI - west;
            west = -CesiumMath.PI;
            east += rotationRadians;
        }

        // Slightly pad extents to avoid floating point error when fragment culling at edges.
        south -= CesiumMath.EPSILON5;
        west -= CesiumMath.EPSILON5;
        north += CesiumMath.EPSILON5;
        east += CesiumMath.EPSILON5;

        var longitudeRangeInverse = 1.0 / (east - west);
        var latitudeRangeInverse = 1.0 / (north - south);

        var attributes = {
            sphericalExtents : new GeometryInstanceAttribute({
                componentDatatype: ComponentDatatype.FLOAT,
                componentsPerAttribute: 4,
                normalize: false,
                value : [south, west, latitudeRangeInverse, longitudeRangeInverse]
            }),
            longitudeRotation : new GeometryInstanceAttribute({
                componentDatatype: ComponentDatatype.FLOAT,
                componentsPerAttribute: 1,
                normalize: false,
                value : [rotationRadians]
            })
        };

        addTextureCoordinateRotationAttributes(attributes, textureCoordinateRotationPoints);
        add2DTextureCoordinateAttributes(boundingRectangle, projection, attributes);
        return attributes;
    };

    ShadowVolumeAppearance.hasAttributesForTextureCoordinatePlanes = function(attributes) {
        return defined(attributes.southWest_HIGH) && defined(attributes.southWest_LOW) &&
            defined(attributes.northward) && defined(attributes.eastward) &&
            defined(attributes.planes2D_HIGH) && defined(attributes.planes2D_LOW) &&
            defined(attributes.uMaxVmax) && defined(attributes.uvMinAndExtents);
    };

    ShadowVolumeAppearance.hasAttributesForSphericalExtents = function(attributes) {
        return defined(attributes.sphericalExtents) && defined(attributes.longitudeRotation) &&
        defined(attributes.planes2D_HIGH) && defined(attributes.planes2D_LOW) &&
        defined(attributes.uMaxVmax) && defined(attributes.uvMinAndExtents);
    };

    function shouldUseSpherical(rectangle) {
        return Math.max(rectangle.width, rectangle.height) > ShadowVolumeAppearance.MAX_WIDTH_FOR_PLANAR_EXTENTS;
    }

    /**
     * Computes whether the given rectangle is wide enough that texture coordinates
     * over its area should be computed using spherical extents instead of distance to planes.
     *
     * @param {Rectangle} rectangle A rectangle
     * @private
     */
    ShadowVolumeAppearance.shouldUseSphericalCoordinates = function(rectangle) {
        //>>includeStart('debug', pragmas.debug);
        Check.typeOf.object('rectangle', rectangle);
        //>>includeEnd('debug');

        return shouldUseSpherical(rectangle);
    };

    /**
     * Texture coordinates for ground primitives are computed either using spherical coordinates for large areas or
     * using distance from planes for small areas.
     *
     * @type {Number}
     * @constant
     * @private
     */
    ShadowVolumeAppearance.MAX_WIDTH_FOR_PLANAR_EXTENTS = CesiumMath.toRadians(1.0);

    return ShadowVolumeAppearance;
});
