text: /*global defineSuite*/
  defineSuite([
          'Core/BoundingSphere',
          'Core/Cartesian2',
          'Core/Cartesian3',
          'Core/defaultValue',
          'Core/Ellipsoid',
          'Core/GeometryInstance',
          'Core/loadImage',
          'Core/Math',
          'Core/Occluder',
          'Core/PolygonGeometry',
          'Renderer/Sampler',
          'Renderer/TextureMagnificationFilter',
          'Renderer/TextureMinificationFilter',
          'Scene/BillboardCollection',
          'Scene/EllipsoidSurfaceAppearance',
          'Scene/HorizontalOrigin',
          'Scene/LabelCollection',
          'Scene/Material',
          'Scene/OrthographicFrustum',
          'Scene/PolylineCollection',
          'Scene/Primitive',
          'Scene/PrimitiveCollection',
          'Scene/SceneMode',
          'Scene/TextureAtlas',
          'Scene/VerticalOrigin',
          'Specs/createCamera',
          'Specs/createContext',
          'Specs/createFrameState',
          'Specs/render'
      ], 'Scene/PrimitiveCulling', function(
          BoundingSphere,
          Cartesian2,
          Cartesian3,
          defaultValue,
          Ellipsoid,
          GeometryInstance,
          loadImage,
          CesiumMath,
          Occluder,
          PolygonGeometry,
          Sampler,
          TextureMagnificationFilter,
          TextureMinificationFilter,
          BillboardCollection,
          EllipsoidSurfaceAppearance,
          HorizontalOrigin,
          LabelCollection,
          Material,
          OrthographicFrustum,
          PolylineCollection,
          Primitive,
          PrimitiveCollection,
          SceneMode,
          TextureAtlas,
          VerticalOrigin,
          createCamera,
          createContext,
          createFrameState,
          render) {
      "use strict";
  
      var context;
      var frameState;
      var primitives;
      var us;
      var camera;
      var greenImage;
  
      beforeAll(function() {
          context = createContext();
  
          return loadImage('./Data/Images/Green.png').then(function(image) {
              greenImage = image;
          });
      });
  
      afterAll(function() {
          context.destroyForSpecs();
      });
  
      beforeEach(function() {
          primitives = new PrimitiveCollection();
  
          camera = createCamera();
  
          frameState = createFrameState(context, camera);
          us = context.uniformState;
          us.update(frameState);
      });
  
      afterEach(function() {
          primitives = primitives && primitives.destroy();
          us = undefined;
      });
  
      function testCullIn3D(primitive) {
          primitives.add(primitive);
  
          var savedVolume = frameState.cullingVolume;
          var savedCamera = frameState.camera;
  
          frameState.camera = camera;
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          // get bounding volume for primitive and reposition camera so its in the the frustum.
          primitive.update(frameState);
          var commandList = frameState.commandList;
          var bv = commandList[0].boundingVolume;
          camera.position = Cartesian3.clone(bv.center);
          camera.position = Cartesian3.multiplyByScalar(Cartesian3.normalize(camera.position, new Cartesian3()), Cartesian3.magnitude(camera.position) + 1.0, new Cartesian3());
          camera.direction = Cartesian3.normalize(Cartesian3.negate(camera.position, new Cartesian3()), new Cartesian3());
          camera.right = Cartesian3.cross(camera.direction, Cartesian3.UNIT_Z, new Cartesian3());
          camera.up = Cartesian3.cross(camera.right, camera.direction, new Cartesian3());
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          var numRendered = render(frameState, primitives);
          expect(numRendered).toBeGreaterThan(0);
  
          // reposition camera so bounding volume is outside frustum.
          Cartesian3.add(camera.position, Cartesian3.multiplyByScalar(camera.right, 8000000000.0, new Cartesian3()), camera.position);
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          numRendered = render(frameState, primitives);
          expect(numRendered).toEqual(0);
  
          frameState.camera = savedCamera;
          frameState.cullingVolume = savedVolume;
      }
  
      function testCullInColumbusView(primitive) {
          primitives.add(primitive);
  
          var savedVolume = frameState.cullingVolume;
          var savedCamera = frameState.camera;
          var savedMode = frameState.mode;
  
          frameState.camera = camera;
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
          frameState.mode = SceneMode.COLUMBUS_VIEW;
  
          // get bounding volume for primitive and reposition camera so its in the the frustum.
          primitive.update(frameState);
          var commandList = frameState.commandList;
          var bv = commandList[0].boundingVolume;
          camera.position = Cartesian3.clone(bv.center);
          camera.position.z += 1.0;
          camera.direction = Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3());
          camera.up = Cartesian3.clone(Cartesian3.UNIT_Y);
          camera.right = Cartesian3.cross(camera.direction, camera.up, new Cartesian3());
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          var numRendered = render(frameState, primitives);
          expect(numRendered).toBeGreaterThan(0);
  
          // reposition camera so bounding volume is outside frustum.
          Cartesian3.add(camera.position, Cartesian3.multiplyByScalar(camera.right, 8000000000.0, new Cartesian3()), camera.position);
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          numRendered = render(frameState, primitives);
          expect(numRendered).toEqual(0);
  
          frameState.mode = savedMode;
          frameState.camera = savedCamera;
          frameState.cullingVolume = savedVolume;
      }
  
      function testCullIn2D(primitive) {
          primitives.add(primitive);
  
          var mode = frameState.mode;
          frameState.mode = SceneMode.SCENE2D;
  
          var savedCamera = frameState.camera;
          frameState.camera = camera;
  
          var savedVolume = frameState.cullingVolume;
          var orthoFrustum = new OrthographicFrustum();
          orthoFrustum.right = 1.0;
          orthoFrustum.left = -orthoFrustum.right;
          orthoFrustum.top = orthoFrustum.right;
          orthoFrustum.bottom = -orthoFrustum.top;
          orthoFrustum.near = camera.frustum.near;
          orthoFrustum.far = camera.frustum.far;
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          // get bounding volume for primitive and reposition camera so its in the the frustum.
          primitive.update(frameState);
          var commandList = frameState.commandList;
          var bv = commandList[0].boundingVolume;
          camera.position = Cartesian3.clone(bv.center);
          camera.position.z += 1.0;
          camera.direction = Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3());
          camera.up = Cartesian3.clone(Cartesian3.UNIT_Y);
          camera.right = Cartesian3.cross(camera.direction, camera.up, new Cartesian3());
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          var numRendered = render(frameState, primitives);
          expect(numRendered).toBeGreaterThan(0);
  
          // reposition camera so bounding volume is outside frustum.
          Cartesian3.add(camera.position, Cartesian3.multiplyByScalar(camera.right, 8000000000.0, new Cartesian3()), camera.position);
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          numRendered = render(frameState, primitives);
          expect(numRendered).toEqual(0);
  
          frameState.mode = mode;
          frameState.camera = savedCamera;
          frameState.cullingVolume = savedVolume;
      }
  
      function testOcclusionCull(primitive) {
          primitives.add(primitive);
  
          var savedCamera = frameState.camera;
          frameState.camera = camera;
  
          // get bounding volume for primitive and reposition camera so its in the the frustum.
          primitive.update(frameState);
          var commandList = frameState.commandList;
          var bv = commandList[0].boundingVolume;
          camera.position = Cartesian3.clone(bv.center);
          camera.position = Cartesian3.multiplyByScalar(Cartesian3.normalize(camera.position, new Cartesian3()), Cartesian3.magnitude(camera.position) + 1.0, new Cartesian3());
          camera.direction = Cartesian3.normalize(Cartesian3.negate(camera.position, new Cartesian3()), new Cartesian3());
          camera.right = Cartesian3.cross(camera.direction, Cartesian3.UNIT_Z, new Cartesian3());
          camera.up = Cartesian3.cross(camera.right, camera.direction, new Cartesian3());
  
          var occluder = new Occluder(new BoundingSphere(Cartesian3.ZERO, bv.radius * 2.0), camera.position);
          frameState.occluder = occluder;
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          var numRendered = render(frameState, primitives);
          expect(numRendered).toBeGreaterThan(0);
  
          // reposition camera so bounding volume on the other side of the ellipsoid.
          camera.position = Cartesian3.negate(camera.position, new Cartesian3());
          camera.direction = Cartesian3.normalize(Cartesian3.negate(camera.position, new Cartesian3()), new Cartesian3());
          camera.right = Cartesian3.cross(camera.direction, Cartesian3.UNIT_Z, new Cartesian3());
          camera.up = Cartesian3.cross(camera.right, camera.direction, new Cartesian3());
  
          occluder.cameraPosition = camera.position;
          frameState.cullingVolume = camera.frustum.computeCullingVolume(camera.position, camera.direction, camera.up);
  
          numRendered = render(frameState, primitives);
          expect(numRendered).toEqual(0);
  
          frameState.camera = savedCamera;
          frameState.occluder = undefined;
      }
  
      // This function is used instead of the testOcclusionCull function for billboards/labels because the
      // bounding volume is view-dependent. All of the "magic numbers" come from adding a billboard/label
      // collection to a Cesium app and looking for when it is/is not occluded.
      function testBillboardOcclusion(billboard) {
          primitives.add(billboard);
  
          camera.position = new Cartesian3(2414237.2401024024, -8854079.165742973, 7501568.895960614);
          camera.direction = Cartesian3.normalize(Cartesian3.negate(camera.position, new Cartesian3()), new Cartesian3());
  
          var savedCamera = frameState.camera;
          frameState.camera = camera;
  
          var occluder = new Occluder(new BoundingSphere(Cartesian3.ZERO, Ellipsoid.WGS84.minimumRadius), camera.position);
          frameState.occluder = occluder;
  
          var numRendered = render(frameState, primitives);
          expect(numRendered).toEqual(1);
  
          camera.position  = Cartesian3.negate(camera.position, new Cartesian3());
          camera.direction = Cartesian3.negate(camera.direction, new Cartesian3());
  
          occluder = new Occluder(new BoundingSphere(Cartesian3.ZERO, 536560539.60104907), camera.position);
          frameState.occluder = occluder;
  
          numRendered = render(frameState, primitives);
          expect(numRendered).toEqual(0);
  
          frameState.camera = savedCamera;
          frameState.occluder = undefined;
      }
  
      function createPolygon(degree, ellipsoid) {
          degree = defaultValue(degree, 50.0);
          var polygon = new Primitive({
              geometryInstances: new GeometryInstance({
                  geometry: PolygonGeometry.fromPositions({
                      positions: Cartesian3.fromDegreesArray([
                          -degree, -degree,
                          degree, -degree,
                          degree, degree,
                          -degree, degree
                      ]),
                      vertexFormat: EllipsoidSurfaceAppearance.VERTEX_FORMAT,
                      ellipsoid: ellipsoid,
                      granularity: CesiumMath.toRadians(20.0)
                  })
              }),
              appearance: new EllipsoidSurfaceAppearance({
                  aboveGround: false
              }),
              asynchronous: false
          });
          polygon.appearance.material.translucent = false;
  
          return polygon;
      }
  
      it('frustum culls polygon in 3D', function() {
          var polygon = createPolygon(10.0, Ellipsoid.WGS84);
          testCullIn3D(polygon);
      });
  
      it('frustum culls polygon in Columbus view', function() {
          var polygon = createPolygon(10.0, Ellipsoid.WGS84);
          testCullInColumbusView(polygon);
      });
  
      it('frustum culls polygon in 2D', function() {
          var polygon = createPolygon(10.0, Ellipsoid.WGS84);
          testCullIn2D(polygon);
      });
  
      it('polygon occlusion', function() {
          var polygon = createPolygon(1.0);
          testOcclusionCull(polygon);
      });
  
      function createLabels(position) {
          position = defaultValue(position, {
              x : -1.0,
              y : 0.0,
              z : 0.0
          });
          var labels = new LabelCollection();
          labels.add({
              position : position,
              text : 'x',
              horizontalOrigin : HorizontalOrigin.CENTER,
              verticalOrigin : VerticalOrigin.CENTER
          });
          return labels;
      }
  
      it('frustum culls labels in 3D', function() {
          var labels = createLabels();
          testCullIn3D(labels);
      });
  
      it('frustum culls labels in Columbus view', function() {
          var labels = createLabels();
          testCullInColumbusView(labels);
      });
  
      it('frustum culls labels in 2D', function() {
          var labels = createLabels();
          testCullIn2D(labels);
      });
  
      it('label occlusion', function() {
          var labels = new LabelCollection();
          labels.add({
              position : Cartesian3.fromDegrees(-75.10, 39.57),
              text : 'x',
              horizontalOrigin : HorizontalOrigin.CENTER,
              verticalOrigin : VerticalOrigin.CENTER
          });
  
          testBillboardOcclusion(labels);
      });
  
      function createBillboard() {
          var atlas = new TextureAtlas({
              context : context,
              borderWidthInPixels : 1,
              initialSize : new Cartesian2(3, 3)
          });
  
          // ANGLE Workaround
          atlas.texture.sampler = new Sampler({
              minificationFilter : TextureMinificationFilter.NEAREST,
              magnificationFilter : TextureMagnificationFilter.NEAREST
          });
  
          var billboards = new BillboardCollection();
          billboards.textureAtlas = atlas;
          billboards.add({
              position : Cartesian3.fromDegrees(-75.10, 39.57),
              image : greenImage
          });
  
          return billboards;
      }
  
      it('frustum culls billboards in 3D', function() {
          var billboards = createBillboard();
          testCullIn3D(billboards);
      });
  
      it('frustum culls billboards in Columbus view', function() {
          var billboards = createBillboard();
          testCullInColumbusView(billboards);
      });
  
      it('frustum culls billboards in 2D', function() {
          var billboards = createBillboard();
          testCullIn2D(billboards);
      });
  
      it('billboard occlusion', function() {
          var billboards = createBillboard();
          testBillboardOcclusion(billboards);
      });
  
      function createPolylines() {
          var material = Material.fromType('Color');
          material.translucent = false;
  
          var polylines = new PolylineCollection();
          polylines.add({
              positions : Cartesian3.fromDegreesArray([
                  -75.10, 39.57,
                  -80.12, 25.46
              ]),
              material : material
          });
          return polylines;
      }
  
      it('frustum culls polylines in 3D', function() {
          var polylines = createPolylines();
          testCullIn3D(polylines);
      });
  
      it('frustum culls polylines in Columbus view', function() {
          var polylines = createPolylines();
          testCullInColumbusView(polylines);
      });
  
      it('frustum culls polylines in 2D', function() {
          var polylines = createPolylines();
          testCullIn2D(polylines);
      });
  
      it('polyline occlusion', function() {
          var polylines = createPolylines();
          testOcclusionCull(polylines);
      });
  }, 'WebGL');
  