API Docs for:

File: /home/lindsay/xeolabs/xeogl-next/xeogl/src/scene/scene.js

/**
 The container for all 3D graphical objects and state in a xeogl scene.

 ## Usage

 * [Creating a Scene](#creating-a-scene)
 * [Creating and accessing components](#creating-and-accessing-components)
 * [Controlling the camera](#controlling-the-camera)
 * [Taking snapshots](#taking-snapshots)
 * [Lighting](#lighting)
 * [Clipping](#clipping)
 * [Picking](#picking)
 * [Querying and tracking boundaries](#querying-and-tracking-boundaries)
 * [Controlling the viewport](#controlling-the-viewport)
 * [Controlling rendering](#controlling-rendering)
 * [Gamma correction](#gamma-correction)

 ### Creating a Scene

 Creating a Scene with its own default canvas:

 ````javascript
 var scene = new xeogl.Scene();
 ````

 Creating a Scene with an existing canvas.

 ````javascript
 var scene2 = new xeogl.Scene({
    canvas: "myCanvas"
 });

 var scene3 = new xeogl.Scene({
    canvas: document.getElementById("myCanvas");
 });
 ````

 ### Creating and accessing components

 As a brief introduction to creating Scene components, we'll create a {{#crossLink "Mesh"}}{{/crossLink}} that has a
 {{#crossLink "TeapotGeometry"}}{{/crossLink}} and a {{#crossLink "PhongMaterial"}}{{/crossLink}}:

 <a href="../../examples/#geometry_primitives_teapot"><img src="../../assets/images/screenshots/Scene/teapot.png"></img></a>

 ````javascript
 var teapotMesh = new xeogl.Mesh(scene, {
    id: "myMesh",                               // <<---------- ID automatically generated if not provided
    geometry: new xeogl.TeapotGeometry(scene),
    material: new xeogl.PhongMaterial(scene, {
        id: "myMaterial",
        diffuse: [0.2, 0.2, 1.0]
    })
 });
 ````

 Creating a {{#crossLink "Mesh"}}{{/crossLink}} within the default Scene (xeogl will automatically create the default Scene if it does not yet exist):
 ````javascript
 var teapotMesh = new xeogl.Mesh({
    id: "myMesh",
    geometry: new xeogl.TeapotGeometry(),
    material: new xeogl.PhongMaterial({
        id: "myMaterial",
        diffuse: [0.2, 0.2, 1.0]
    })
 });

 teapotMesh.scene.camera.eye = [45, 45, 45];
 ````

 The default Scene can be got from either the Mesh or the xeogl namespace:

 ````javascript
 scene = teapotMesh.scene;
 scene = xeogl.getDefaultScene();
 ````

 You can also make any Scene instance the default scene, so that components will belong to that Scene when you don't explicitly
 specify a Scene for them:

 ````javascript
 var scene = new xeogl.Scene({ ... };
 xeogl.setDefaultScene( scene );
 ````

 Find components by ID in their Scene's {{#crossLink "Scene/components:property"}}{{/crossLink}} map:

 ````javascript
 var teapotMesh = scene.components["myMesh"];
 teapotMesh.visible = false;

 var teapotMaterial = scene.components["myMaterial"];
 teapotMaterial.diffuse = [1,0,0]; // Change to red
 ````

 A Scene also has a map of component instances for each {{#crossLink "Component"}}{{/crossLink}} subtype:

 ````javascript
 var meshes = scene.types["xeogl.Mesh"];
 var teapotMesh = meshes["myMesh"];
 teapotMesh.ghosted = true;

 var phongMaterials = scene.types["xeogl.PhongMaterial"];
 var teapotMaterial = phongMaterials["myMaterial"];
 teapotMaterial.diffuse = [0,1,0]; // Change to green
 ````

 See {{#crossLink "Object"}}{{/crossLink}}, {{#crossLink "Group"}}{{/crossLink}} and {{#crossLink "Model"}}{{/crossLink}}
 for how to create and access more sophisticated content.

 ### Controlling the camera

 Use the Scene's {{#crossLink "Camera"}}{{/crossLink}} to control the current viewpoint and projection:

 ````javascript
 var camera = myScene.camera;

 camera.eye = [-10,0,0];
 camera.look = [-10,0,0];
 camera.up = [0,1,0];

 camera.projection = "perspective";
 camera.perspective.fov = 45;
 //...
 ````

 ### Managing the canvas, taking snapshots

 The Scene's {{#crossLink "Canvas"}}{{/crossLink}} component provides various conveniences relevant to the WebGL canvas, such
 as getting getting snapshots, firing resize events etc:

 ````javascript
 var canvas = scene.canvas;

 canvas.on("boundary", function(boundary) {
    //...
 });

 var imageData = canvas.getSnapshot({
    width: 500,
    height: 500,
    format: "png"
 });
 ````

 ### Lighting

 The Scene's {{#crossLink "Lights"}}{{/crossLink}} component manages lighting:

 ````javascript
 var lights = scene.lights;
 lights[1].color = [0.9, 0.9, 0.9];
 //...
 ````

 ### Clipping

 The Scene's {{#crossLink "Clips"}}{{/crossLink}} component manages clipping planes for custom cross-sections:

 ````javascript
 var clips = scene.clips;
 clips.clips = [
 new xeogl.Clip({  // Clip plane on negative diagonal
        pos: [1.0, 1.0, 1.0],
        dir: [-1.0, -1.0, -1.0],
        active: true
    }),
 new xeogl.Clip({ // Clip plane on positive diagonal
        pos: [-1.0, -1.0, -1.0],
        dir: [1.0, 1.0, 1.0],
        active: true
    }),
 //...
 ];
 ````

 ### Picking

 Use the Scene's {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}} method to pick and raycast meshes.

 For example, to pick a point on the surface of the closest mesh at the given canvas coordinates:

 ````javascript
 var hit = scene.pick({
     pickSurface: true,
     canvasPos: [23, 131]
 });

 if (hit) { // Picked a Mesh

      var mesh = hit.mesh;

      var primitive = hit.primitive; // Type of primitive that was picked, usually "triangles"
      var primIndex = hit.primIndex; // Position of triangle's first index in the picked Mesh's Geometry's indices array
      var indices = hit.indices; // UInt32Array containing the triangle's vertex indices
      var localPos = hit.localPos; // Float32Array containing the picked Local-space position on the triangle
      var worldPos = hit.worldPos; // Float32Array containing the picked World-space position on the triangle
      var viewPos = hit.viewPos; // Float32Array containing the picked View-space position on the triangle
      var bary = hit.bary; // Float32Array containing the picked barycentric position within the triangle
      var normal = hit.normal; // Float32Array containing the interpolated normal vector at the picked position on the triangle
      var uv = hit.uv; // Float32Array containing the interpolated UV coordinates at the picked position on the triangle
 }
 ````

 #### Pick masking

 We can use the {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}} method's ````includeMeshes```` and ````excludeMeshes````
 options to mask which Meshes we attempt to pick.

 This is useful for picking <em>through</em> things, to pick only the Meshes of interest.

 To pick only Meshes ````"gearbox#77.0"```` and ````"gearbox#79.0"````, picking through any other Meshes that are
 in the way, as if they weren't there:

 ````javascript
 var hit = scene.pick({
     canvasPos: [23, 131],
     includeMeshes: ["gearbox#77.0", "gearbox#79.0"]
 });

 if (hit) {
      // Mesh will always be either "gearbox#77.0" or "gearbox#79.0"
      var mesh = hit.mesh;
 }
 ````

 To pick any pickable Mesh, except for ````"gearbox#77.0"```` and ````"gearbox#79.0"````, picking through those
 Meshes if they happen to be in the way:

 ````javascript
 var hit = scene.pick({
     canvasPos: [23, 131],
     excludeMeshes: ["gearbox#77.0", "gearbox#79.0"]
 });

 if (hit) {
      // Mesh will never be "gearbox#77.0" or "gearbox#79.0"
      var mesh = hit.mesh;
 }
 ````

 See {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}} for more info on picking.

 ### Querying and tracking boundaries

 Getting a Scene's World-space axis-aligned boundary (AABB):

 ````javascript
 var aabb = scene.aabb; // [xmin, ymin, zmin, xmax, ymax, zmax]
 ````

 Subscribing to updates to the AABB, which occur whenever {{#crossLink "Meshes"}}{{/crossLink}} are transformed, their
 {{#crossLink "Geometry"}}Geometries{{/crossLink}} have been updated, or the {{#crossLink "Camera"}}Camera{{/crossLink}} has moved:

 ````javascript
 scene.on("boundary", function() {
     var aabb = scene.aabb;
 });
 ````

 Getting the AABB of the {{#crossLink "Object"}}Objects{{/crossLink}} with the given IDs:

 ````JavaScript
 scene.getAABB(); // Gets collective boundary of all Mesh Objects in the scene
 scene.getAABB("saw"); // Gets boundary of an Object
 scene.getAABB(["saw", "gearbox"]); // Gets collective boundary of two Objects
 ````

 See {{#crossLink "Scene/getAABB:method"}}Scene#getAABB(){{/crossLink}} and {{#crossLink "Object"}}{{/crossLink}} for more info on querying and tracking boundaries.

 ### Managing the viewport

 The Scene's {{#crossLink "Viewport"}}{{/crossLink}} component manages the WebGL viewport:

 ````javascript
 var viewport = scene.viewport
 viewport.boundary = [0, 0, 500, 400];;
 ````

 ### Controlling rendering

 You can configure a Scene to perform multiple "passes" (renders) per frame. This is useful when we want to render the
 scene to multiple viewports, such as for stereo effects.

 In the example, below, we'll configure the Scene to render twice on each frame, each time to different viewport. We'll do this
 with a callback that intercepts the Scene before each render and sets its {{#crossLink "Viewport"}}{{/crossLink}} to a
 different portion of the canvas. By default, the Scene will clear the canvas only before the first render, allowing the
 two views to be shown on the canvas at the same time.

 ````Javascript
 // Load a glTF model
 var model = new xeogl.GLTFModel({
    src: "models/gltf/GearboxAssy/glTF-MaterialsCommon/GearboxAssy.gltf"
 });

 var scene = model.scene;
 var viewport = scene.viewport;

 // Configure Scene to render twice for each frame
 scene.passes = 2; // Default is 1
 scene.clearEachPass = false; // Default is false

 // Render to a separate viewport on each render

 var viewport = scene.viewport;
 viewport.autoBoundary = false;

 scene.on("rendering", function (e) {
     switch (e.pass) {
         case 0:
             viewport.boundary = [0, 0, 200, 200]; // xmin, ymin, width, height
             break;

         case 1:
             viewport.boundary = [200, 0, 200, 200];
             break;
     }
 });

 // We can also intercept the Scene after each render,
 // (though we're not using this for anything here)
 scene.on("rendered", function (e) {
     switch (e.pass) {
         case 0:
             break;

         case 1:
             break;
     }
 });
 ````

 ### Gamma correction

 Within its shaders, xeogl performs shading calculations in linear space.

 By default, the Scene expects color textures (eg. {{#crossLink "PhongMaterial/diffuseMap:property"}}PhongMaterial#diffuseMap{{/crossLink}},
 {{#crossLink "MetallicMaterial/baseColorMap:property"}}MetallicMaterial#baseColorMap{{/crossLink}} and {{#crossLink "SpecularMaterial/diffuseMap:property"}}SphericalMaterial#diffuseMap{{/crossLink}}) to
 be in pre-multipled gamma space, so will convert those to linear space before they are used in shaders. Other textures are
 always expected to be in linear space.

 By default, the Scene will also gamma-correct its rendered output.

 You can configure the Scene to expect all those color textures to be linear space, so that it does not gamma-correct them:

 ````javascript
 scene.gammaInput = false;
 ````

 You would still need to gamma-correct the output, though, if it's going straight to the canvas, so normally we would
 leave that enabled:

 ````javascript
 scene.gammaOutput = true;
 ````

 See {{#crossLink "Texture"}}{{/crossLink}} for more information on texture encoding and gamma.

 @class Scene
 @module xeogl
 @submodule scene
 @constructor
 @param [cfg] Scene parameters
 @param [cfg.id] {String} Optional ID, unique among all Scenes in xeogl, generated automatically when omitted.
 @param [cfg.meta] {String:Object} Optional map of user-defined metadata to attach to this Scene.
 @param [cfg.canvasId] {String} ID of existing HTML5 canvas in the DOM - creates a full-page canvas automatically if this is omitted
 @param [cfg.webgl2=true] {Boolean} Set this false when we **don't** want to use WebGL 2 for our Scene; the Scene will fall
 back on WebGL 1 if not available. This property will be deprecated when WebGL 2 is supported everywhere.
 @param [cfg.components] {Array(Object)} JSON array containing parameters for {{#crossLink "Component"}}Component{{/crossLink}} subtypes to immediately create within the Scene.
 @param [cfg.ticksPerRender=1] {Number} The number of {{#crossLink "Scene/tick:event"}}{{/crossLink}} that happen between each render or this Scene.
 @param [cfg.passes=1] {Number} The number of times this Scene renders per frame.
 @param [cfg.clearEachPass=false] {Boolean} When doing multiple passes per frame, specifies whether to clear the
 canvas before each pass (true) or just before the first pass (false).
 @param [cfg.transparent=false] {Boolean} Whether or not the canvas is transparent.
 @param [cfg.backgroundColor] {Float32Array} RGBA color for canvas background, when canvas is not transparent. Overridden by backgroundImage.
 @param [cfg.backgroundImage] {String} URL of an image to show as the canvas background, when canvas is not transparent. Overrides backgroundImage.
 @param [cfg.gammaInput=false] {Boolean} When true, expects that all textures and colors are premultiplied gamma.
 @param [cfg.gammaOutput=true] {Boolean} Whether or not to render with pre-multiplied gama.
 @param [cfg.gammaFactor=2.2] {Number} The gamma factor to use when rendering with pre-multiplied gamma.
 @extends Component
 */

import {core} from '../core.js';
import {utils} from '../utils.js';
import {math} from '../math/math.js';
import {stats} from '../stats.js';
import {Component} from '../component.js';
import {Canvas} from '../canvas/canvas.js';
import {Renderer} from '../renderer/renderer.js';
import {Input} from '../input/input.js';
import {Viewport} from '../viewport/viewport.js';
import {Camera} from '../camera/camera.js';
import {DirLight} from '../lighting/dirLight.js';
import {BoxGeometry} from '../geometry/boxGeometry.js';
import {PhongMaterial} from '../materials/phongMaterial.js';
import {EmphasisMaterial} from '../materials/emphasisMaterial.js';
import {EdgeMaterial} from '../materials/edgeMaterial.js';
import {OutlineMaterial} from '../materials/outlineMaterial.js';
import {componentClasses} from "../componentClasses.js";

const type = "xeogl.Scene";

// Cached vars to avoid garbage collection

const localRayOrigin = math.vec3();
const localRayDir = math.vec3();
const positionA = math.vec3();
const positionB = math.vec3();
const positionC = math.vec3();
const triangleVertices = math.vec3();
const position = math.vec4();
const worldPos = math.vec3();
const viewPos = math.vec3();
const bary = math.vec3();
const normalA = math.vec3();
const normalB = math.vec3();
const normalC = math.vec3();
const uva = math.vec3();
const uvb = math.vec3();
const uvc = math.vec3();
const tempVec4a = math.vec4();
const tempVec4b = math.vec4();
const tempVec4c = math.vec4();
const tempVec3 = math.vec3();
const tempVec3b = math.vec3();
const tempVec3c = math.vec3();
const tempVec3d = math.vec3();
const tempVec3e = math.vec3();
const tempVec3f = math.vec3();
const tempVec3g = math.vec3();
const tempVec3h = math.vec3();
const tempVec3i = math.vec3();
const tempVec3j = math.vec3();
const tempVec3k = math.vec3();

function getMeshIDMap(scene, meshIds) {
    const map = {};
    let meshId;
    let mesh;
    for (let i = 0, len = meshIds.length; i < len; i++) {
        meshId = meshIds[i];
        mesh = scene.meshes[meshId];
        if (!mesh) {
            scene.warn("pick(): Mesh not found: " + meshId);
            continue;
        }
        map[meshId] = true;
    }
    return map;
}

/**
 * Fired whenever a debug message is logged on a component within this Scene.
 * @event log
 * @param {String} value The debug message
 */

/**
 * Fired whenever an error is logged on a component within this Scene.
 * @event error
 * @param {String} value The error message
 */

/**
 * Fired whenever a warning is logged on a component within this Scene.
 * @event warn
 * @param {String} value The warning message
 */
class Scene extends Component {

    /**
     JavaScript class name for this Component.

     For example: "xeogl.AmbientLight", "xeogl.MetallicMaterial" etc.

     @property type
     @type String
     @final
     */
    get type() {
        return type;
    }

    init(cfg) {

        super.init(cfg);

        const self = this;

        const transparent = !!cfg.transparent;

        /**
         The number of models currently loading.

         @property loading
         @final
         @type {Number}
         */
        this.loading = 0;

        /**
         The epoch time (in milliseconds since 1970) when this Scene was instantiated.

         @property timeCreated
         @final
         @type {Number}
         */
        this.startTime = (new Date()).getTime();

        /**
         {{#crossLink "Model"}}{{/crossLink}}s in this Scene, mapped to their IDs.

         @property models
         @final
         @type {String:xeogl.Model}
         */
        this.models = {};

        /**
         The {{#crossLink "Object"}}Objects{{/crossLink}} in this Scene, mapped to their IDs.

         @property objects
         @final
         @type {{String:Object}}
         */
        this.objects = {};

        /**
         {{#crossLink "Object"}}Objects{{/crossLink}} in this Scene that have GUIDs, mapped to their GUIDs.

         Each Object is registered in this map when its {{#crossLink "Object/guid:property"}}{{/crossLink}} is
         assigned a value.

         @property guidObjects
         @final
         @type {{String:Object}}
         */
        this.guidObjects = {};

        /**
         For each entity type, a map of IDs to {{#crossLink "Object"}}Objects{{/crossLink}} of that entity type.

         Each Object is registered in this map when its {{#crossLink "Object/entityType:property"}}{{/crossLink}} is
         assigned a value.

         @property entityTypes
         @final
         @type {String:{String:xeogl.Component}}
         */
        this.entityTypes = {};

        /**
         {{#crossLink "Object"}}Objects{{/crossLink}} in this Scene that have entity types, mapped to their IDs.

         Each Object is registered in this map when its {{#crossLink "Object/entityType:property"}}{{/crossLink}} is
         assigned a value.

         @property entities
         @final
         @type {{String:Object}}
         */
        this.entities = {};

        /**
         Visible entity {{#crossLink "Object"}}Objects{{/crossLink}} within this Scene, mapped to their IDs.

         Each Object is registered in this map when its {{#crossLink "Object/visible:property"}}{{/crossLink}} property is true and its
         {{#crossLink "Object/entityType:property"}}{{/crossLink}} is assigned a value.

         @property visibleEntities
         @final
         @type {{String:Object}}
         */
        this.visibleEntities = {};

        /**
         Ghosted entity {{#crossLink "Object"}}Objects{{/crossLink}} within this Scene, mapped to their IDs.

         Each Object is registered in this map when its {{#crossLink "Object/ghosted:property"}}{{/crossLink}} property is true and its
         {{#crossLink "Object/entityType:property"}}{{/crossLink}} is assigned a value.

         @property ghostedEntities
         @final
         @type {{String:Object}}
         */
        this.ghostedEntities = {};

        /**
         Highlighted entity {{#crossLink "Object"}}Objects{{/crossLink}} within this Scene, mapped to their IDs.

         Each Object is registered in this map when its {{#crossLink "Object/highlighted:property"}}{{/crossLink}} property is true and its
         {{#crossLink "Object/entityType:property"}}{{/crossLink}} is assigned a value.

         @property highlightedEntities
         @final
         @type {{String:Object}}
         */
        this.highlightedEntities = {};

        /**
         Selected entity {{#crossLink "Object"}}Objects{{/crossLink}} within this Scene, mapped to their IDs.

         Each Object is registered in this map when its {{#crossLink "Object/selected:property"}}{{/crossLink}} property is true and its
         {{#crossLink "Object/entityType:property"}}{{/crossLink}} is assigned a value.

         @property selectedEntities
         @final
         @type {{String:Object}}
         */
        this.selectedEntities = {};

        // Cached ID arrays, lazy-rebuilt as needed when stale after map updates

        /**
         Lazy-regenerated ID lists.
         */
        this._objectGUIDs = null;
        this._entityIds = null;
        this._visibleEntityIds = null;
        this._ghostedEntityIds = null;
        this._highlightedEntityIds = null;
        this._selectedEntityIds = null;

        /**
         The {{#crossLink "Mesh"}}Meshes{{/crossLink}} in this Scene, mapped to their IDs.

         @property meshes
         @final
         @type {String:xeogl.Mesh}
         */
        this.meshes = {};

        this._needRecompileMeshes = false;

        /**
         For each {{#crossLink "Component"}}{{/crossLink}} type, a map of
         IDs to {{#crossLink "Component"}}Components{{/crossLink}} instances of that type.

         @property types
         @final
         @type {String:{String:xeogl.Component}}
         */
        this.types = {};

        /**
         The {{#crossLink "Component"}}Component{{/crossLink}} within this Scene, mapped to their IDs.

         @property components
         @final
         @type {String:xeogl.Component}
         */
        this.components = {};

        /**
         The root {{#crossLink "Object"}}Objects{{/crossLink}} in this Scene, mapped to their IDs.

         @property rootObjects
         @final
         @type {{String:Object}}
         */
        this.rootObjects = {};

        /**
         The {{#crossLink "Clip"}}Clip{{/crossLink}} components in this Scene, mapped to their IDs.

         @property clips
         @final
         @type {{String:Clip}}
         */
        this.clips = {};

        /**
         The {{#crossLink "PointLight"}}{{/crossLink}}, {{#crossLink "DirLight"}}{{/crossLink}},
         {{#crossLink "SpotLight"}}{{/crossLink}} and {{#crossLink "AmbientLight"}}{{/crossLink}} components in this Scene, mapped to their IDs.

         @property lights
         @final
         @type {{String:Object}}
         */
        this.lights = {};

        /**
         The {{#crossLink "LightMap"}}{{/crossLink}} components in this Scene, mapped to their IDs.

         @property lightMaps
         @final
         @type {{String:LightMap}}
         */
        this.lightMaps = {};

        /**
         The {{#crossLink "ReflectionMap"}}{{/crossLink}} components in this Scene, mapped to their IDs.

         @property reflectionMaps
         @final
         @type {{String:ReflectionMap}}
         */
        this.reflectionMaps = {};

        /**
         Manages the HTML5 canvas for this Scene.
         @final
         @property canvas
         @type {Canvas}
         */
        this.canvas = new Canvas(this, {
            dontClear: true, // Never destroy this component with Scene#clear();
            canvas: cfg.canvas, // Can be canvas ID, canvas element, or null
            transparent: transparent,
            backgroundColor: cfg.backgroundColor,
            backgroundImage: cfg.backgroundImage,
            webgl2: cfg.webgl2 !== false,
            contextAttr: cfg.contextAttr || {},
            simulateWebGLContextLost: cfg.simulateWebGLContextLost
        });

        // Redraw as canvas resized
        this.canvas.on("boundary", function () {
            self._renderer.imageDirty();
        });

        this.canvas.on("webglContextFailed", function () {
            alert("xeogl failed to find WebGL!");
        });

        this._renderer = new Renderer(this, {
            transparent: transparent
        });

        this._clipsState = new (function () {

            this.clips = [];

            let hash = null;

            this.getHash = function () {
                if (hash) {
                    return hash;
                }
                const clips = this.clips;
                if (clips.length === 0) {
                    return this.hash = ";";
                }
                let clip;
                const hashParts = [];
                for (let i = 0, len = clips.length; i < len; i++) {
                    clip = clips[i];
                    hashParts.push("cp");
                }
                hashParts.push(";");
                hash = hashParts.join("");
                return hash;
            };

            this.addClip = function (clip) {
                this.clips.push(clip);
                hash = null;
            };

            this.removeClip = function (clip) {
                for (let i = 0, len = this.clips.length; i < len; i++) {
                    if (this.clips[i].id === clip.id) {
                        this.clips.splice(i, 1);
                        hash = null;
                        return;
                    }
                }
            };
        })();

        this._lightsState = new (function () {

            const DEFAULT_AMBIENT = math.vec3([0, 0, 0]);
            const ambientColor = math.vec3();

            this.lights = [];
            this.reflectionMaps = [];
            this.lightMaps = [];

            let hash = null;
            let ambientLight = null;

            this.getHash = function () {
                if (hash) {
                    return hash;
                }
                const hashParts = [];
                const lights = this.lights;
                let light;
                for (let i = 0, len = lights.length; i < len; i++) {
                    light = lights[i];
                    hashParts.push("/");
                    hashParts.push(light.type);
                    hashParts.push((light.space === "world") ? "w" : "v");
                    if (light.shadow) {
                        hashParts.push("sh");
                    }
                }
                if (this.lightMaps.length > 0) {
                    hashParts.push("/lm");
                }
                if (this.reflectionMaps.length > 0) {
                    hashParts.push("/rm");
                }
                hashParts.push(";");
                hash = hashParts.join("");
                return hash;
            };

            this.addLight = function (state) {
                this.lights.push(state);
                ambientLight = null;
                hash = null;
            };

            this.removeLight = function (state) {
                for (let i = 0, len = this.lights.length; i < len; i++) {
                    const light = this.lights[i];
                    if (light.id === state.id) {
                        this.lights.splice(i, 1);
                        if (ambientLight && ambientLight.id === state.id) {
                            ambientLight = null;
                        }
                        hash = null;
                        return;
                    }
                }
            };

            this.addReflectionMap = function (state) {
                this.reflectionMaps.push(state);
                hash = null;
            };

            this.removeReflectionMap = function (state) {
                for (let i = 0, len = this.reflectionMaps.length; i < len; i++) {
                    if (this.reflectionMaps[i].id === state.id) {
                        this.reflectionMaps.splice(i, 1);
                        hash = null;
                        return;
                    }
                }
            };

            this.addLightMap = function (state) {
                this.lightMaps.push(state);
                hash = null;
            };

            this.removeLightMap = function (state) {
                for (let i = 0, len = this.lightMaps.length; i < len; i++) {
                    if (this.lightMaps[i].id === state.id) {
                        this.lightMaps.splice(i, 1);
                        hash = null;
                        return;
                    }
                }
            };

            this.getAmbientColor = function () {
                if (!ambientLight) {
                    for (let i = 0, len = this.lights.length; i < len; i++) {
                        const light = this.lights[i];
                        if (light.type === "ambient") {
                            ambientLight = light;
                            break;
                        }
                    }
                }
                if (ambientLight) {
                    const color = ambientLight.color;
                    const intensity = ambientLight.intensity;
                    ambientColor[0] = color[0] * intensity;
                    ambientColor[1] = color[1] * intensity;
                    ambientColor[2] = color[2] * intensity;
                    return ambientColor;
                } else {
                    return DEFAULT_AMBIENT;
                }
            };

        })();

        /**
         Publishes input events that occur on this Scene's canvas.

         @final
         @property input
         @type {Input}
         @final
         */
        this.input = new Input(this, {
            dontClear: true, // Never destroy this component with Scene#clear();
            element: this.canvas.canvas
        });

        // Register Scene on xeogl
        // Do this BEFORE we add components below
        core._addScene(this);

        // Add components specified as JSON

        const componentJSONs = cfg.components;

        if (componentJSONs) {
            let componentJSON;
            let type;
            let constr;
            for (let i = 0, len = componentJSONs.length; i < len; i++) {
                componentJSON = componentJSONs[i];
                type = componentJSON.type;
                if (type) {
                    constr = window[type];
                    if (constr) {
                        new constr(this, componentJSON);
                    }
                }
            }
        }

        // Init default components

        this._initDefaults();

        // Global components

        this._viewport = new Viewport(this, {
            id: "default.viewport",
            autoBoundary: true,
            dontClear: true // Never destroy this component with Scene#clear();
        });

        this._camera = new Camera(this, {
            id: "default.camera",
            dontClear: true // Never destroy this component with Scene#clear();
        });

        // Default lights

        new DirLight(this, {
            dir: [0.8, -0.6, -0.8],
            color: [1.0, 1.0, 1.0],
            intensity: 1.0,
            space: "view"
        });

        new DirLight(this, {
            dir: [-0.8, -0.4, -0.4],
            color: [1.0, 1.0, 1.0],
            intensity: 1.0,
            space: "view"
        });

        new DirLight(this, {
            dir: [0.2, -0.8, 0.8],
            color: [0.6, 0.6, 0.6],
            intensity: 1.0,
            space: "view"
        });

        // Plug global components into renderer

        const viewport = this._viewport;
        const renderer = this._renderer;
        const camera = this._camera;

        camera.on("dirty", function () {
            renderer.imageDirty();
        });

        this.ticksPerRender = cfg.ticksPerRender;
        this.passes = cfg.passes;
        this.clearEachPass = cfg.clearEachPass;
        this.gammaInput = cfg.gammaInput;
        this.gammaOutput = cfg.gammaOutput;
        this.gammaFactor = cfg.gammaFactor;
    }

    _initDefaults() {

        // Call this Scene's property accessors to lazy-init their properties

        let dummy; // Keeps Codacy happy

        dummy = this.geometry;
        dummy = this.material;
        dummy = this.ghostMaterial;
        dummy = this.outlineMaterial;
    }

    _addComponent(component) {
        if (component.id) { // Manual ID
            if (this.components[component.id]) {
                this.error("Component " + utils.inQuotes(component.id) + " already exists in Scene - ignoring ID, will randomly-generate instead");
                component.id = null;
            }
        }
        if (!component.id) { // Auto ID
            if (window.nextID === undefined) {
                window.nextID = 0;
            }
            //component.id = math.createUUID();
            component.id = "_" + window.nextID++;
            while (this.components[component.id]) {
                component.id = math.createUUID();
            }
        }
        this.components[component.id] = component;
        // Register for class type
        const type = component.type;
        let types = this.types[component.type];
        if (!types) {
            types = this.types[type] = {};
        }
        types[component.id] = component;
    }

    _removeComponent(component) {
        delete this.components[component.id];
        const types = this.types[component.type];
        if (types) {
            delete types[component.id];
            if (utils.isEmptyObject(types)) {
                delete this.types[component.type];
            }
        }
    }

    // Methods below are called by various component types to register themselves on their
    // Scene. Violates Hollywood Principle, where we could just filter on type in _addComponent,
    // but this is faster than checking the type of each component in such a filter.

    _clipCreated(clip) {
        this.clips[clip.id] = clip;
        this.scene._clipsState.addClip(clip._state);
        this._needRecompileMeshes = true;
    }

    _lightCreated(light) {
        this.lights[light.id] = light;
        this.scene._lightsState.addLight(light._state);
        this._needRecompileMeshes = true;
    }

    _lightMapCreated(lightMap) {
        this.lightMaps[lightMap.id] = lightMap;
        this.scene._lightsState.addLightMap(lightMap._state);
        this._needRecompileMeshes = true;
    }

    _reflectionMapCreated(reflectionMap) {
        this.reflectionMaps[reflectionMap.id] = reflectionMap;
        this.scene._lightsState.addReflectionMap(reflectionMap._state);
        this._needRecompileMeshes = true;
    }

    _objectCreated(object) {
        this.objects[object.id] = object;
        if (object.guid) {
            this.guidObjects[object.id] = object;
            this._objectGUIDs = null; // To lazy-rebuild
        }
        if (!object.parent) {
            this.rootObjects[object.id] = object; // TODO: What about when a root Object is added as child to another?
        }
        stats.components.objects++;
    }

    _meshCreated(mesh) {
        this.meshes[mesh.id] = mesh;
        stats.components.meshes++;
    }

    _modelCreated(model) {
        this.models[model.id] = model;
        stats.components.models++;
    }

    _clipDestroyed(clip) {
        delete this.clips[clip.id];
        this.scene._clipsState.removeClip(clip._state);
        this._needRecompileMeshes = true;
    }

    _lightDestroyed(light) {
        delete this.lights[light.id];
        this.scene._lightsState.removeLight(light._state);
        this._needRecompileMeshes = true;
    }

    _lightMapDestroyed(lightMap) {
        delete this.lightMaps[lightMap.id];
        this.scene._lightsState.removeLightMap(lightMap._state);
        this._needRecompileMeshes = true;
    }

    _reflectionMapDestroyed(reflectionMap) {
        delete this.reflectionMaps[reflectionMap.id];
        this.scene._lightsState.removeReflectionMap(reflectionMap._state);
        this._needRecompileMeshes = true;
    }

    _objectDestroyed(object) {
        delete this.objects[object.id];
        if (object.guid) {
            delete this.guidObjects[object.guid];
            this._objectGUIDs = null; // To lazy-rebuild
        }
        if (!object.parent) {
            delete this.rootObjects[object.id];
        }
        stats.components.objects--;
    }

    _meshDestroyed(mesh) {
        stats.components.meshes--;
        delete this.meshes[mesh.id];
        stats.components.meshes--;
    }

    _modelDestroyed(model) {
        this.models[model.id] = model;
        stats.components.models++;
    }

    _entityTypeAssigned(object, newEntityType) {
        this.entities[object.id] = object;
        let objectsOfType = this.entityTypes[newEntityType];
        if (!objectsOfType) {
            objectsOfType = {};
            this.entityTypes[newEntityType] = objectsOfType;
        }
        objectsOfType[object.id] = object;
        this._entityIds = null; // Lazy regenerate
        this._entityTypeIds = null; // Lazy regenerate
    }

    _entityTypeRemoved(object, oldEntityType) {
        delete this.entities[object.id];
        const objectsOfType = this.entityTypes[oldEntityType];
        if (objectsOfType) {
            delete objectsOfType[object.id];
        }
        this._entityIds = null; // Lazy regenerate
        this._entityTypeIds = null; // Lazy regenerate
    }

    _entityVisibilityUpdated(object, visible) {
        if (visible) {
            this.visibleEntities[object.id] = object;
        } else {
            delete this.visibleEntities[object.id];
        }
        this._visibleEntityIds = null; // Lazy regenerate
    }

    _entityGhostedUpdated(object, ghosted) {
        if (ghosted) {
            this.ghostedEntities[object.id] = object;
        } else {
            delete this.ghostedEntities[object.id];
        }
        this._ghostedEntityIds = null; // Lazy regenerate
    }

    _entityHighlightedUpdated(object, highlighted) {
        if (highlighted) {
            this.highlightedEntities[object.id] = object;
        } else {
            delete this.highlightedEntities[object.id];
        }
        this._highlightedEntityIds = null; // Lazy regenerate
    }

    _entitySelectedUpdated(object, selected) {
        if (selected) {
            this.selectedEntities[object.id] = object;
        } else {
            delete this.selectedEntities[object.id];
        }
        this._selectedEntityIds = null; // Lazy regenerate
    }

    _webglContextLost() {
        //  this.loading++;
        this.canvas.spinner.processes++;
        for (const id in this.components) {
            if (this.components.hasOwnProperty(id)) {
                const component = this.components[id];
                if (component._webglContextLost) {
                    component._webglContextLost();
                }
            }
        }
        this._renderer.webglContextLost();
    }

    _webglContextRestored() {
        const gl = this.canvas.gl;
        for (const id in this.components) {
            if (this.components.hasOwnProperty(id)) {
                const component = this.components[id];
                if (component._webglContextRestored) {
                    component._webglContextRestored(gl);
                }
            }
        }
        this._renderer.webglContextRestored(gl);
        //this.loading--;
        this.canvas.spinner.processes--;
    }

    /**
     * Renders a single frame of this Scene.
     *
     * The Scene will periodically render itself after any updates, but you can call this method to force a render
     * if required. This method is typically used when we want to synchronously take a snapshot of the canvas and
     * need everything rendered right at that moment.
     *
     * @method render
     * @param {Boolean} [forceRender=false] Forces a render when true, otherwise only renders if something has changed in this Scene
     * since the last render.
     */
    render(forceRender) {

        const renderEvent = {
            sceneId: null,
            pass: 0
        };


        if (this._needRecompileMeshes) {
            this._recompileMeshes();
            this._needRecompileMeshes = false;
        }

        if (this.loading > 0 || this.canvas.spinner.processes > 0) {
            this.canvas.canvas.style.opacity = 0.0;
            return;
        }

        let opacity = Number.parseFloat(this.canvas.canvas.style.opacity);
        if (opacity < 1.0) {
            opacity += 0.1;
            this.canvas.canvas.style.opacity = opacity;
        }

        renderEvent.sceneId = this.id;

        const passes = this._passes;
        const clearEachPass = this._clearEachPass;
        let pass;
        let clear;

        for (pass = 0; pass < passes; pass++) {

            renderEvent.pass = pass;

            /**
             * Fired when about to render a frame for a Scene.
             *
             * @event rendering
             * @param {String} sceneID The ID of this Scene.
             * @param {Number} pass Index of the pass we are about to render (see {{#crossLink "Scene/passes:property"}}{{/crossLink}}).
             */
            this.fire("rendering", renderEvent, true);

            clear = clearEachPass || (pass === 0);

            this._renderer.render({pass: pass, clear: clear, force: forceRender});

            /**
             * Fired when we have just rendered a frame for a Scene.
             *
             * @event rendering
             * @param {String} sceneID The ID of this Scene.
             * @param {Number} pass Index of the pass we rendered (see {{#crossLink "Scene/passes:property"}}{{/crossLink}}).
             */
            this.fire("rendered", renderEvent, true);
        }

        this._saveAmbientColor();
    }

    _recompileMeshes() {
        for (const id in this.meshes) {
            if (this.meshes.hasOwnProperty(id)) {
                this.meshes[id]._compile();
            }
        }
    }

    _saveAmbientColor() {
        const canvas = this.canvas;
        if (!canvas.transparent && !canvas.backgroundImage && !canvas.backgroundColor) {
            const ambientColor = this._lightsState.getAmbientColor();
            if (!this._lastAmbientColor ||
                this._lastAmbientColor[0] !== ambientColor[0] ||
                this._lastAmbientColor[1] !== ambientColor[1] ||
                this._lastAmbientColor[2] !== ambientColor[2] ||
                this._lastAmbientColor[3] !== ambientColor[3]) {
                canvas.backgroundColor = ambientColor;
                if (!this._lastAmbientColor) {
                    this._lastAmbientColor = math.vec4([0, 0, 0, 1]);
                }
                this._lastAmbientColor.set(ambientColor);
            }
        } else {
            this._lastAmbientColor = null;
        }
    }

    /**
     Convenience array of entity type IDs in {{#crossLink "Scene/entityTypes:property"}}{{/crossLink}}.
     @property entityTypeIds
     @final
     @type {Array of String}
     */
    get objectGUIDs() {
        if (!this._objectGUIDs) {
            this._objectGUIDs = Object.keys(this.guidObjects);
        }
        return this._objectGUIDs;
    }

    /**
     Convenience array of entity type IDs in {{#crossLink "Scene/entityTypes:property"}}{{/crossLink}}.
     @property entityTypeIds
     @final
     @type {Array of String}
     */
    get entityTypeIds() {
        if (!this._entityTypeIds) {
            this._entityTypeIds = Object.keys(this.entityTypes);
        }
        return this._entityTypeIds;
    }

    /**
     Convenience array of IDs in {{#crossLink "Scene/entities:property"}}{{/crossLink}}.
     @property entityIds
     @final
     @type {Array of String}
     */
    get entityIds() {
        if (!this._entityIds) {
            this._entityIds = Object.keys(this.entities);
        }
        return this._entityIds;
    }

    /**
     Convenience array of IDs in {{#crossLink "Scene/visibleEntities:property"}}{{/crossLink}}.
     @property visibleEntityIds
     @final
     @type {Array of String}
     */
    get visibleEntityIds() {
        if (!this._visibleEntityIds) {
            this._visibleEntityIds = Object.keys(this.visibleEntities);
        }
        return this._visibleEntityIds;
    }

    /**
     Convenience array of IDs in {{#crossLink "Scene/ghostedEntities:property"}}{{/crossLink}}.
     @property ghostedEntityIds
     @final
     @type {Array of String}
     */
    get ghostedEntityIds() {
        if (!this._ghostedEntityIds) {
            this._ghostedEntityIds = Object.keys(this.ghostedEntities);
        }
        return this._ghostedEntityIds;
    }

    /**
     Convenience array of IDs in {{#crossLink "Scene/highlightedEntities:property"}}{{/crossLink}}.
     @property highlightedEntityIds
     @final
     @type {Array of String}
     */
    get highlightedEntityIds() {
        if (!this._highlightedEntityIds) {
            this._highlightedEntityIds = Object.keys(this.highlightedEntities);
        }
        return this._highlightedEntityIds;
    }

    /**
     Convenience array of IDs in {{#crossLink "Scene/selectedEntities:property"}}{{/crossLink}}.
     @property selectedEntityIds
     @final
     @type {Array of String}
     */
    get selectedEntityIds() {
        if (!this._selectedEntityIds) {
            this._selectedEntityIds = Object.keys(this.selectedEntities);
        }
        return this._selectedEntityIds;
    }

    /**
     The number of {{#crossLink "Scene/tick:property"}}{{/crossLink}} that happen between each render or this Scene.

     @property ticksPerRender
     @default 1
     @type Number
     */
    set ticksPerRender(value) {
        if (value === undefined || value === null) {
            value = 1;
        } else if (!utils.isNumeric(value) || value <= 0) {
            this.error("Unsupported value for 'ticksPerRender': '" + value +
                "' - should be an integer greater than zero.");
            value = 1;
        }
        if (value === this._ticksPerRender) {
            return;
        }
        this._ticksPerRender = value;
    }

    get ticksPerRender() {
        return this._ticksPerRender;
    }

    /**
     The number of times this Scene renders per frame.

     @property passes
     @default 1
     @type Number
     */
    set passes(value) {
        if (value === undefined || value === null) {
            value = 1;
        } else if (!utils.isNumeric(value) || value <= 0) {
            this.error("Unsupported value for 'passes': '" + value +
                "' - should be an integer greater than zero.");
            value = 1;
        }
        if (value === this._passes) {
            return;
        }
        this._passes = value;
        this._renderer.imageDirty();
    }

    get passes() {
        return this._passes;
    }

    /**
     When doing multiple passes per frame, specifies whether to clear the
     canvas before each pass (true) or just before the first pass (false).

     @property clearEachPass
     @default false
     @type Boolean
     */
    set clearEachPass(value) {
        value = !!value;
        if (value === this._clearEachPass) {
            return;
        }
        this._clearEachPass = value;
        this._renderer.imageDirty();
    }

    get clearEachPass() {
        return this._clearEachPass;
    }

    /**
     When true, expects all textures and colors are premultiplied gamma.

     @property gammaInput
     @default false
     @type Boolean
     */
    set gammaInput(value) {
        value = value !== false;
        if (value === this._renderer.gammaInput) {
            return;
        }
        this._renderer.gammaInput = value;
        this._needRecompileMeshes = true;
    }

    get gammaInput() {
        return this._renderer.gammaInput;
    }

    /**
     Whether or not to render pixels with pre-multiplied gama.

     @property gammaOutput
     @default true
     @type Boolean
     */
    set gammaOutput(value) {
        value = value !== false;
        if (value === this._renderer.gammaOutput) {
            return;
        }
        this._renderer.gammaOutput = value;
        this._needRecompileMeshes = true;
    }

    get gammaOutput() {
        return this._renderer.gammaOutput;
    }

    /**
     The gamma factor to use when {{#crossLink "Scene/property:gammaOutput"}}{{/crossLink}} is set true.

     @property gammaOutput
     @default 1.0
     @type Number
     */
    set gammaFactor(value) {
        value = (value === undefined || value === null) ? 2.2 : value;
        if (value === this._renderer.gammaFactor) {
            return;
        }
        this._renderer.gammaFactor = value;
        this._renderer.imageDirty();
    }

    get gammaFactor() {
        return this._renderer.gammaFactor;
    }

    /**
     The default geometry for this Scene, which is a {{#crossLink "BoxGeometry"}}BoxGeometry{{/crossLink}}.

     This {{#crossLink "BoxGeometry"}}BoxGeometry{{/crossLink}} has an {{#crossLink "Component/id:property"}}id{{/crossLink}} equal to "default.geometry".

     {{#crossLink "Mesh"}}Meshes{{/crossLink}} in this Scene are attached to this
     {{#crossLink "Geometry"}}Geometry{{/crossLink}} by default.
     @property geometry
     @final
     @type BoxGeometry
     */
    get geometry() {
        return this.components["default.geometry"] ||
            new BoxGeometry(this, {
                id: "default.geometry",
                dontClear: true
            });
    }

    /**
     The default drawing material for this Scene, which is a {{#crossLink "PhongMaterial"}}PhongMaterial{{/crossLink}}.

     This {{#crossLink "PhongMaterial"}}PhongMaterial{{/crossLink}} has
     an {{#crossLink "Component/id:property"}}id{{/crossLink}} equal to "default.material", with all
     other properties initialised to their default values.

     {{#crossLink "Mesh"}}Meshes{{/crossLink}} in this Scene are attached to this
     {{#crossLink "PhongMaterial"}}PhongMaterial{{/crossLink}} by default.
     @property material
     @final
     @type PhongMaterial
     */
    get material() {
        return this.components["default.material"] || new PhongMaterial(this, {
                id: "default.material",
                emissive: [0.4, 0.4, 0.4], // Visible by default on geometry without normals
                dontClear: true
            });
    }

    /**
     The Scene's default {{#crossLink "EmphasisMaterial"}}EmphasisMaterial{{/crossLink}} for the appearance of {{#crossLink "Meshes"}}Meshes{{/crossLink}} when they are ghosted.

     This {{#crossLink "EmphasisMaterial"}}EmphasisMaterial{{/crossLink}} has
     an {{#crossLink "Component/id:property"}}id{{/crossLink}} equal to "default.ghostMaterial", with all
     other properties initialised to their default values.

     {{#crossLink "Mesh"}}Meshes{{/crossLink}} in this Scene are attached to this
     {{#crossLink "EmphasisMaterial"}}EmphasisMaterial{{/crossLink}} by default.
     @property ghostMaterial
     @final
     @type EmphasisMaterial
     */
    get ghostMaterial() {
        return this.components["default.ghostMaterial"] || new EmphasisMaterial(this, {
                id: "default.ghostMaterial",
                preset: "sepia",
                dontClear: true
            });
    }

    /**
     The Scene's default {{#crossLink "EmphasisMaterial"}}EmphasisMaterial{{/crossLink}} for the appearance of {{#crossLink "Meshes"}}Meshes{{/crossLink}} when they are highlighted.

     This {{#crossLink "HighlightMaterial"}}HighlightMaterial{{/crossLink}} has
     an {{#crossLink "Component/id:property"}}id{{/crossLink}} equal to "default.highlightMaterial", with all
     other properties initialised to their default values.

     {{#crossLink "Mesh"}}Meshes{{/crossLink}} in this Scene are attached to this
     {{#crossLink "HighlightMaterial"}}HighlightMaterial{{/crossLink}} by default.
     @property highlightMaterial
     @final
     @type HighlightMaterial
     */
    get highlightMaterial() {
        return this.components["default.highlightMaterial"] || new EmphasisMaterial(this, {
                id: "default.highlightMaterial",
                preset: "yellowHighlight",
                dontClear: true
            });
    }

    /**
     The Scene's default {{#crossLink "EmphasisMaterial"}}EmphasisMaterial{{/crossLink}} for the appearance of {{#crossLink "Meshes"}}Meshes{{/crossLink}} when they are selected.

     This {{#crossLink "SelectedMaterial"}}SelectedMaterial{{/crossLink}} has
     an {{#crossLink "Component/id:property"}}id{{/crossLink}} equal to "default.selectedMaterial", with all
     other properties initialised to their default values.

     {{#crossLink "Mesh"}}Meshes{{/crossLink}} in this Scene are attached to this
     {{#crossLink "SelectedMaterial"}}SelectedMaterial{{/crossLink}} by default.
     @property selectedMaterial
     @final
     @type SelectedMaterial
     */
    get selectedMaterial() {
        return this.components["default.selectedMaterial"] || new EmphasisMaterial(this, {
                id: "default.selectedMaterial",
                preset: "greenSelected",
                dontClear: true
            });
    }

    /**
     The Scene's default {{#crossLink "EdgeMaterial"}}EmphasisMaterial{{/crossLink}} for the appearance of {{#crossLink "Meshes"}}Meshes{{/crossLink}} when edges are emphasized.

     This {{#crossLink "EdgeMaterial"}}EdgeMaterial{{/crossLink}} has
     an {{#crossLink "Component/id:property"}}id{{/crossLink}} equal to "default.edgeMaterial", with all
     other properties initialised to their default values.

     {{#crossLink "Mesh"}}Meshes{{/crossLink}} in this Scene are attached to this
     {{#crossLink "EdgeMaterial"}}EdgeMaterial{{/crossLink}} by default.
     @property edgeMaterial
     @final
     @type EdgeMaterial
     */
    get edgeMaterial() {
        return this.components["default.edgeMaterial"] || new EdgeMaterial(this, {
                id: "default.edgeMaterial",
                preset: "default",
                edgeColor: [0.0, 0.0, 0.0],
                edgeAlpha: 1.0,
                edgeWidth: 1,
                dontClear: true
            });
    }

    /**
     The Scene's default {{#crossLink "OutlineMaterial"}}OutlineMaterial{{/crossLink}} for the appearance of {{#crossLink "Meshes"}}Meshes{{/crossLink}} when they are outlined.

     This {{#crossLink "OutlineMaterial"}}OutlineMaterial{{/crossLink}} has
     an {{#crossLink "Component/id:property"}}id{{/crossLink}} equal to "default.outlineMaterial", with all
     other properties initialised to their default values.

     {{#crossLink "Mesh"}}Meshes{{/crossLink}} in this Scene are attached to this
     {{#crossLink "OutlineMaterial"}}OutlineMaterial{{/crossLink}} by default.
     @property outlineMaterial
     @final
     @type OutlineMaterial
     */
    get outlineMaterial() {
        return this.components["default.outlineMaterial"] || new OutlineMaterial(this, {
                id: "default.outlineMaterial",
                dontClear: true
            });
    }

    /**
     The {{#crossLink "Viewport"}}{{/crossLink}} belonging to this Scene.

     @property viewport
     @final
     @type Viewport
     */
    get viewport() {
        return this._viewport;
    }

    /**
     The {{#crossLink "Camera"}}Camera{{/crossLink}} belonging to this Scene.

     @property camera
     @final
     @type Camera
     */
    get camera() {
        return this._camera;
    }

    /**
     World-space 3D center of this Scene.

     @property center
     @final
     @type {Float32Array}
     */
    get center() {
        if (this._aabbDirty || !this._center) {
            if (!this._center || !this._center) {
                this._center = math.vec3();
            }
            const aabb = this.aabb;
            this._center[0] = (aabb[0] + aabb[3] ) / 2;
            this._center[1] = (aabb[1] + aabb[4] ) / 2;
            this._center[2] = (aabb[2] + aabb[5] ) / 2;
        }
        return this._center;
    }

    /**
     World-space axis-aligned 3D boundary (AABB) of this Scene.

     The AABB is represented by a six-element Float32Array containing the min/max extents of the
     axis-aligned volume, ie. ````[xmin, ymin,zmin,xmax,ymax, zmax]````.

     @property aabb
     @final
     @type {Float32Array}
     */
    get aabb() {
        if (this._aabbDirty) {
            if (!this._aabb) {
                this._aabb = math.AABB3();
            }
            let xmin = math.MAX_DOUBLE;
            let ymin = math.MAX_DOUBLE;
            let zmin = math.MAX_DOUBLE;
            let xmax = -math.MAX_DOUBLE;
            let ymax = -math.MAX_DOUBLE;
            let zmax = -math.MAX_DOUBLE;
            let aabb;
            const meshes = this.meshes;
            let mesh;
            for (const meshId in meshes) {
                if (meshes.hasOwnProperty(meshId)) {
                    mesh = meshes[meshId];
                    if (!mesh.collidable) {
                        continue;
                    }
                    aabb = mesh.aabb;
                    if (aabb[0] < xmin) {
                        xmin = aabb[0];
                    }
                    if (aabb[1] < ymin) {
                        ymin = aabb[1];
                    }
                    if (aabb[2] < zmin) {
                        zmin = aabb[2];
                    }
                    if (aabb[3] > xmax) {
                        xmax = aabb[3];
                    }
                    if (aabb[4] > ymax) {
                        ymax = aabb[4];
                    }
                    if (aabb[5] > zmax) {
                        zmax = aabb[5];
                    }
                }
            }
            this._aabb[0] = xmin;
            this._aabb[1] = ymin;
            this._aabb[2] = zmin;
            this._aabb[3] = xmax;
            this._aabb[4] = ymax;
            this._aabb[5] = zmax;
            this._aabbDirty = false;
        }
        return this._aabb;
    }

    _setBoundaryDirty() {
        //if (!this._aabbDirty) {
        this._aabbDirty = true;
        this.fire("boundary");
        // }
    }

    /**
     Attempts to pick an {{#crossLink "Mesh"}}Mesh{{/crossLink}} in this Scene.

     Ignores {{#crossLink "Mesh"}}Meshes{{/crossLink}} with {{#crossLink "Mesh/pickable:property"}}pickable{{/crossLink}}
     set *false*.

     When a {{#crossLink "Mesh"}}{{/crossLink}} is picked, fires a "pick" event on the {{#crossLink "Mesh"}}{{/crossLink}}
     with the hit result as parameters.

     Picking the {{#crossLink "Mesh"}}{{/crossLink}} at the given canvas coordinates:

     ````javascript
     var hit = scene.pick({
              canvasPos: [23, 131]
           });

     if (hit) { // Picked a Mesh
              var mesh = hit.mesh;
          }
     ````

     **Usage:**

     Picking the {{#crossLink "Mesh"}}{{/crossLink}} that intersects a ray cast through the canvas:

     ````javascript
     var hit = scene.pick({
              pickSurface: true,
              canvasPos: [23, 131]
           });

     if (hit) { // Picked a Mesh

              var mesh = hit.mesh;

              // These properties are only on the hit result when we do a ray-pick:

              var primitive = hit.primitive; // Type of primitive that was picked, usually "triangles"
              var primIndex = hit.primIndex; // Position of triangle's first index in the picked Mesh's Geometry's indices array
              var indices = hit.indices; // UInt32Array containing the triangle's vertex indices
              var localPos = hit.localPos; // Float32Array containing the picked Local-space position on the triangle
              var worldPos = hit.worldPos; // Float32Array containing the picked World-space position on the triangle
              var viewPos = hit.viewPos; // Float32Array containing the picked View-space position on the triangle
              var bary = hit.bary; // Float32Array containing the picked barycentric position within the triangle
              var normal = hit.normal; // Float32Array containing the interpolated normal vector at the picked position on the triangle
              var uv = hit.uv; // Float32Array containing the interpolated UV coordinates at the picked position on the triangle
          }
     ````

     Picking the {{#crossLink "Mesh"}}{{/crossLink}} that intersects an arbitrarily-aligned World-space ray:

     ````javascript
     var hit = scene.pick({
              pickSurface: true,       // Picking with arbitrarily-positioned ray
              origin: [0,0,-5],    // Ray origin
              direction: [0,0,1]   // Ray direction
          });

     if (hit) { // Picked a Mesh with the ray

              var mesh = hit.mesh;

              var primitive = hit.primitive; // Type of primitive that was picked, usually "triangles"
              var primIndex = hit.primIndex; // Position of triangle's first index in the picked Mesh's Geometry's indices array
              var indices = hit.indices; // UInt32Array containing the triangle's vertex indices
              var localPos = hit.localPos; // Float32Array containing the picked Local-space position on the triangle
              var worldPos = hit.worldPos; // Float32Array containing the picked World-space position on the triangle
              var viewPos = hit.viewPos; // Float32Array containing the picked View-space position on the triangle
              var bary = hit.bary; // Float32Array containing the picked barycentric position within the triangle
              var normal = hit.normal; // Float32Array containing the interpolated normal vector at the picked position on the triangle
              var uv = hit.uv; // Float32Array containing the interpolated UV coordinates at the picked position on the triangle
              var origin = hit.origin; // Float32Array containing the World-space ray origin
              var direction = hit.direction; // Float32Array containing the World-space ray direction
          }
     ````
     @method pick

     @param {*} params Picking parameters.
     @param {Boolean} [params.pickSurface=false] Whether to find the picked position on the surface of the Mesh.
     @param {Float32Array} [params.canvasPos] Canvas-space coordinates. When ray-picking, this will override the
     **origin** and ** direction** parameters and will cause the ray to be fired through the canvas at this position,
     directly along the negative View-space Z-axis.
     @param {Float32Array} [params.origin] World-space ray origin when ray-picking. Ignored when canvasPos given.
     @param {Float32Array} [params.direction] World-space ray direction when ray-picking. Also indicates the length of the ray. Ignored when canvasPos given.
     @param {Array} [params.includeMeshes] IDs of {{#crossLink "Mesh"}}Meshes{{/crossLink}} to restrict picking to. When given, ignores {{#crossLink "Mesh"}}Meshes{{/crossLink}} whose IDs are not in this list.
     @param {Array} [params.excludeMeshes] IDs of {{#crossLink "Mesh"}}Meshes{{/crossLink}} to ignore. When given, will pick *through* these {{#crossLink "Mesh"}}Meshes{{/crossLink}}, as if they were not there.
     @returns {*} Hit record, returned when an {{#crossLink "Mesh"}}{{/crossLink}} is picked, else null. See
     method comments for description.
     */
    pick(params) {

        if (this.canvas.boundary[2] === 0 || this.canvas.boundary[3] === 0) {
            this.error("Picking not allowed while canvas has zero width or height");
            return null;
        }

        params = params || {};

        params.pickSurface = params.pickSurface || params.rayPick; // Backwards compatibility

        if (!params.canvasPos && (!params.origin || !params.direction)) {
            this.warn("picking without canvasPos or ray origin and direction");
        }

        const includeMeshes = params.includeMeshes || params.include; // Backwards compat
        if (includeMeshes) {
            params.includeMeshIds = getMeshIDMap(this, includeMeshes);
        }

        const excludeMeshes = params.excludeMeshes || params.exclude; // Backwards compat
        if (excludeMeshes) {
            params.excludeMeshIds = getMeshIDMap(this, excludeMeshes);
        }

        // if (params.includeEntityTypes) {
        //     params.includeObjects = getMeshIDMapFromEntityTypes(this, params.includeEntityTypes);
        // }
        //
        // if (params.excludeEntityTypes) {
        //     params.excludeObjects = getMeshIDMapFromEntityTypes(this, params.excludeEntityTypes);
        // }

        const hit = this._renderer.pick(params);

        if (hit) {

            hit.object = hit.mesh; // Backwards compat

            if (params.pickSurface) {

                if (hit.primIndex !== undefined && hit.primIndex > -1) {

                    const geometry = hit.mesh.geometry._state;

                    if (geometry.primitiveName === "triangles") {

                        // Triangle picked; this only happens when the
                        // Mesh has a Geometry that has primitives of type "triangle"

                        hit.primitive = "triangle";

                        // Get the World-space positions of the triangle's vertices

                        const i = hit.primIndex; // Indicates the first triangle index in the indices array

                        const indices = geometry.indices; // Indices into geometry arrays, not into shared VertexBufs
                        const positions = geometry.positions;

                        let ia3;
                        let ib3;
                        let ic3;

                        if (indices) {

                            var ia = indices[i + 0];
                            var ib = indices[i + 1];
                            var ic = indices[i + 2];

                            triangleVertices[0] = ia;
                            triangleVertices[1] = ib;
                            triangleVertices[2] = ic;

                            hit.indices = triangleVertices;

                            ia3 = ia * 3;
                            ib3 = ib * 3;
                            ic3 = ic * 3;

                        } else {

                            ia3 = i * 3;
                            ib3 = ia3 + 3;
                            ic3 = ib3 + 3;
                        }

                        positionA[0] = positions[ia3 + 0];
                        positionA[1] = positions[ia3 + 1];
                        positionA[2] = positions[ia3 + 2];

                        positionB[0] = positions[ib3 + 0];
                        positionB[1] = positions[ib3 + 1];
                        positionB[2] = positions[ib3 + 2];

                        positionC[0] = positions[ic3 + 0];
                        positionC[1] = positions[ic3 + 1];
                        positionC[2] = positions[ic3 + 2];

                        if (geometry.quantized) {

                            // Decompress vertex positions

                            const positionsDecodeMatrix = geometry.positionsDecodeMatrix;
                            if (positionsDecodeMatrix) {
                                math.decompressPosition(positionA, positionsDecodeMatrix, positionA);
                                math.decompressPosition(positionB, positionsDecodeMatrix, positionB);
                                math.decompressPosition(positionC, positionsDecodeMatrix, positionC);
                            }
                        }

                        // Attempt to ray-pick the triangle in local space

                        let canvasPos;

                        if (params.canvasPos) {
                            canvasPos = params.canvasPos;
                            hit.canvasPos = params.canvasPos;
                            math.canvasPosToLocalRay(this.camera, hit.mesh, canvasPos, localRayOrigin, localRayDir);

                        } else if (params.origin && params.direction) {
                            math.worldRayToLocalRay(hit.mesh, params.origin, params.direction, localRayOrigin, localRayDir);
                        }

                        math.normalizeVec3(localRayDir);
                        math.rayPlaneIntersect(localRayOrigin, localRayDir, positionA, positionB, positionC, position);

                        // Get Local-space cartesian coordinates of the ray-triangle intersection

                        hit.localPos = position;
                        hit.position = position;

                        // Get interpolated World-space coordinates

                        // Need to transform homogeneous coords

                        tempVec4a[0] = position[0];
                        tempVec4a[1] = position[1];
                        tempVec4a[2] = position[2];
                        tempVec4a[3] = 1;

                        // Get World-space cartesian coordinates of the ray-triangle intersection

                        math.transformVec4(hit.mesh.worldMatrix, tempVec4a, tempVec4b);

                        worldPos[0] = tempVec4b[0];
                        worldPos[1] = tempVec4b[1];
                        worldPos[2] = tempVec4b[2];

                        hit.worldPos = worldPos;

                        // Get View-space cartesian coordinates of the ray-triangle intersection

                        math.transformVec4(hit.mesh.scene.camera.matrix, tempVec4b, tempVec4c);

                        viewPos[0] = tempVec4c[0];
                        viewPos[1] = tempVec4c[1];
                        viewPos[2] = tempVec4c[2];

                        hit.viewPos = viewPos;

                        // Get barycentric coordinates of the ray-triangle intersection

                        math.cartesianToBarycentric(position, positionA, positionB, positionC, bary);

                        hit.bary = bary;

                        // Get interpolated normal vector

                        const normals = geometry.normals;

                        if (normals) {

                            if (geometry.quantized) {

                                // Decompress vertex normals

                                const ia2 = ia * 2;
                                const ib2 = ib * 2;
                                const ic2 = ic * 2;

                                math.octDecodeVec2(normals.subarray(ia2, ia2 + 2), normalA);
                                math.octDecodeVec2(normals.subarray(ib2, ib2 + 2), normalB);
                                math.octDecodeVec2(normals.subarray(ic2, ic2 + 2), normalC);

                            } else {

                                normalA[0] = normals[ia3];
                                normalA[1] = normals[ia3 + 1];
                                normalA[2] = normals[ia3 + 2];

                                normalB[0] = normals[ib3];
                                normalB[1] = normals[ib3 + 1];
                                normalB[2] = normals[ib3 + 2];

                                normalC[0] = normals[ic3];
                                normalC[1] = normals[ic3 + 1];
                                normalC[2] = normals[ic3 + 2];
                            }

                            const normal = math.addVec3(math.addVec3(
                                math.mulVec3Scalar(normalA, bary[0], tempVec3),
                                math.mulVec3Scalar(normalB, bary[1], tempVec3b), tempVec3c),
                                math.mulVec3Scalar(normalC, bary[2], tempVec3d), tempVec3e);

                            hit.normal = math.transformVec3(hit.mesh.worldNormalMatrix, normal, tempVec3f);
                        }

                        // Get interpolated UV coordinates

                        const uvs = geometry.uv;

                        if (uvs) {

                            uva[0] = uvs[(ia * 2)];
                            uva[1] = uvs[(ia * 2) + 1];

                            uvb[0] = uvs[(ib * 2)];
                            uvb[1] = uvs[(ib * 2) + 1];

                            uvc[0] = uvs[(ic * 2)];
                            uvc[1] = uvs[(ic * 2) + 1];

                            if (geometry.quantized) {

                                // Decompress vertex UVs

                                const uvDecodeMatrix = geometry.uvDecodeMatrix;
                                if (uvDecodeMatrix) {
                                    math.decompressUV(uva, uvDecodeMatrix, uva);
                                    math.decompressUV(uvb, uvDecodeMatrix, uvb);
                                    math.decompressUV(uvc, uvDecodeMatrix, uvc);
                                }
                            }

                            hit.uv = math.addVec3(
                                math.addVec3(
                                    math.mulVec2Scalar(uva, bary[0], tempVec3g),
                                    math.mulVec2Scalar(uvb, bary[1], tempVec3h), tempVec3i),
                                math.mulVec2Scalar(uvc, bary[2], tempVec3j), tempVec3k);
                        }
                    }
                }
            }

            hit.mesh.fire("picked", hit);

            return hit;
        }
    }

    /**
     Returns the collective axis-aligned bounding box of the {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     When no arguments are given, returns the total boundary of all objects in the scene.

     Only {{#crossLink "Mesh"}}Meshes{{/crossLink}} with {{#crossLink "Mesh/collidable:property"}}collidable{{/crossLink}}
     set ````true```` are included in the boundary.

     ## Usage

     ````JavaScript
     scene.getAABB(); // Gets collective boundary of all objects in the scene
     scene.getAABB("saw"); // Gets collective boundary of all objects in saw model
     scene.getAABB(["saw", "gearbox"]); // Gets collective boundary of all objects in saw and gearbox models
     scene.getAABB("saw#0.1"); // Get boundary of an object in the saw model
     scene.getAABB(["saw#0.1", "saw#0.2"]); // Get collective boundary of two objects in saw model
     scene.getAABB(["saw#0.1", "surface", "support"]); // Get collective boundary an object, and all objects of the given two entity classes.
     ````

     @method getAABB
     @param {String|String[]} target {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @returns {[Number, Number, Number, Number, Number, Number]} An axis-aligned World-space bounding box, given as elements ````[xmin, ymin, zmin, xmax, ymax, zmax]````.
     */
    getAABB(target) {
        if (target === undefined) {
            return this.aabb;
        }
        if (utils.isString(target)) {
            const object = this.objects[target];
            if (object) {
                return object.aabb;
            }
            target = [target]; // Must be an entity type
        }
        if (target.length === 0) {
            return this.aabb;
        }
        let xmin = 100000;
        let ymin = 100000;
        let zmin = 100000;
        let xmax = -100000;
        let ymax = -100000;
        let zmax = -100000;
        let valid;
        this.withObjects(target, object => {
                const aabb = object.aabb;
                if (aabb[0] < xmin) {
                    xmin = aabb[0];
                }
                if (aabb[1] < ymin) {
                    ymin = aabb[1];
                }
                if (aabb[2] < zmin) {
                    zmin = aabb[2];
                }
                if (aabb[3] > xmax) {
                    xmax = aabb[3];
                }
                if (aabb[4] > ymax) {
                    ymax = aabb[4];
                }
                if (aabb[5] > zmax) {
                    zmax = aabb[5];
                }
                valid = true;
            }
        );
        if (valid) {
            const aabb2 = new math.AABB3();
            aabb2[0] = xmin;
            aabb2[1] = ymin;
            aabb2[2] = zmin;
            aabb2[3] = xmax;
            aabb2[4] = ymax;
            aabb2[5] = zmax;
            return aabb2;
        } else {
            return this.aabb; // Scene AABB
        }
    }

    /**
     Resets this Scene to its default state.

     References to any components in this Scene will become invalid.

     @method clear
     */
    clear() {
        var component;
        for (const id in this.components) {
            if (this.components.hasOwnProperty(id)) {
                // Each component fires "destroyed" as it is destroyed,
                // which this Scene handles by removing the component
                component = this.components[id];
                if (!component._dontClear) { // Don't destroy components like xeogl.Camera, xeogl.Input, xeogl.Viewport etc.
                    component.destroy();
                }
            }
        }
    }

    /**
     Convenience method that destroys all light sources.

     Removes all {{#crossLink "AmbientLight"}}AmbientLights{{/crossLink}}, {{#crossLink "PointLight"}}PointLights{{/crossLink}},
     {{#crossLink "DirLight"}}DirLights{{/crossLink}} and {{#crossLink "SpotLight"}}SpotLights{{/crossLink}}.

     @method clearLights
     */
    clearLights() {
        const ids = Object.keys(this.lights);
        for (let i = 0, len = ids.length; i < len; i++) {
            this.lights[ids[i]].destroy();
        }
    }

    /**
     Convenience method that destroys all {{#crossLink "Clip"}}Clips{{/crossLink}}.

     @method clearClips
     */
    clearClips() {
        const ids = Object.keys(this.clips);
        for (let i = 0, len = ids.length; i < len; i++) {
            this.clips[ids[i]].destroy();
        }
    }

    /**
     Shows or hides a batch of {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     Each Object indicates its visibility status in its {{#crossLink "Object/visibility:property"}}{{/crossLink}} property.

     Each visible Object is registered in the {{#crossLink "Scene"}}{{/crossLink}}'s
     {{#crossLink "Scene/visibleEntities:property"}}{{/crossLink}} map while its {{#crossLink "Object/entityType:property"}}{{/crossLink}}
     is assigned a value.

     @method setVisible
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param visible {Boolean} The new visibility state.
     @returns {Boolean} True if any {{#crossLink "Object"}}Objects{{/crossLink}} changed visibility, else false if all updates were redundant and not applied.
     */
    setVisible(ids, visible) {
        return this.withObjects(ids, object => {
            const changed = (object.visible !== visible);
            object.visible = visible;
            return changed;
        });
    }

    /**
     Culls or unculls a batch of {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     Each Object indicates its culled status in its {{#crossLink "Object/visibility:property"}}{{/crossLink}} property.

     @method setVisible
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param culled {Boolean} The new cull state.
     @returns {Boolean} True if any {{#crossLink "Object"}}Objects{{/crossLink}} changed culled state, else false if all updates were redundant and not applied.
     */
    setCulled(ids, culled) {
        return this.withObjects(ids, object => {
            const changed = (object.culled !== culled);
            object.culled = culled;
            return changed;
        });
    }

    /**
     Selects or de-selects a batch of {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     Each Object indicates its selected status in its {{#crossLink "Object/selected:property"}}{{/crossLink}} property.

     Each selected Object is registered in the {{#crossLink "Scene"}}{{/crossLink}}'s
     {{#crossLink "Scene/selectedEntities:property"}}{{/crossLink}} map while its {{#crossLink "Object/entityType:property"}}{{/crossLink}}
     is assigned a value.

     @method setSelected
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param selected {Boolean} Whether to select or deselect.
     @returns {Boolean} True if any {{#crossLink "Object"}}Objects{{/crossLink}} changed selection state, else false if all updates were redundant and not applied.
     */
    setSelected(ids, selected) {
        return this.withObjects(ids, object => {
            const changed = (object.selected !== selected);
            object.selected = selected;
            return changed;
        });
    }

    /**
     Highlights or de-highlights a batch of {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     Each Object indicates its highlight status in its {{#crossLink "Object/highlighted:property"}}{{/crossLink}} property.

     Each highlighted Object is registered in the {{#crossLink "Scene"}}{{/crossLink}}'s
     {{#crossLink "Scene/highlightedEntities:property"}}{{/crossLink}} map while its {{#crossLink "Object/entityType:property"}}{{/crossLink}}
     is assigned a value.

     @method setHighlighted
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param highlighted {Boolean} Whether to highlight or un-highlight.
     @returns {Boolean} True if any {{#crossLink "Object"}}Objects{{/crossLink}} changed highlighted state, else false if all updates were redundant and not applied.
     */
    setHighlighted(ids, highlighted) {
        return this.withObjects(ids, object => {
            const changed = (object.highlighted !== highlighted);
            object.highlighted = highlighted;
            return changed;
        });
    }

    /**
     Ghosts or un-ghosts a batch of {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     Each Object indicates its ghosted status in its {{#crossLink "Object/ghosted:property"}}{{/crossLink}} property.

     Each ghosted Object is registered in the {{#crossLink "Scene"}}{{/crossLink}}'s
     {{#crossLink "Scene/ghostedEntities:property"}}{{/crossLink}} map when its {{#crossLink "Object/entityType:property"}}{{/crossLink}}
     is assigned a value.

     @method setGhosted
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param ghosted {Float32Array} Whether to ghost or un-ghost.
     @returns {Boolean} True if any {{#crossLink "Object"}}Objects{{/crossLink}} changed ghosted state, else false if all updates were redundant and not applied.
     */
    setGhosted(ids, ghosted) {
        return this.withObjects(ids, object => {
            const changed = (object.ghosted !== ghosted);
            object.ghosted = ghosted;
            return changed;
        });
    }

    /**
     Shows or hides wireeframe edges for batch of {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     @method setEdges
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param edges {Float32Array} Whether to show or hide edges.
     @returns {Boolean} True if any {{#crossLink "Object"}}Objects{{/crossLink}} changed edges state, else false if all updates were redundant and not applied.
     */
    setEdges(ids, edges) {
        return this.withObjects(ids, object => {
            const changed = (object.edges !== edges);
            object.edges = edges;
            return changed;
        });
    }

    /**
     Shows or hides an outline around a batch of {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     Each Object indicates its outlined status in its {{#crossLink "Object/outlined:property"}}{{/crossLink}} property.

     Each outlined Object is registered in the {{#crossLink "Scene"}}{{/crossLink}}'s
     {{#crossLink "Scene/outlinedEntities:property"}}{{/crossLink}} map when its {{#crossLink "Object/entityType:property"}}{{/crossLink}}
     is assigned a value.

     @method setOutlined
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param outlined {Float32Array} Whether to show or hide the outline.
     @returns {Boolean} True if any {{#crossLink "Object"}}Objects{{/crossLink}} changed outlined state, else false if all updates were redundant and not applied.
     */
    setOutlined(ids, outlined) {
        return this.withObjects(ids, object => {
            const changed = (object.outlined !== outlined);
            object.outlined = outlined;
            return changed;
        });
    }

    /**
     Colorizes a batch of {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     @method setColorize
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param [colorize=(1,1,1)] Float32Array RGB colorize factors, multiplied by the rendered pixel colors.
     */
    setColorize(ids, colorize) {
        return this.withObjects(ids, object => {
            object.colorize = colorize;
        });
    }

    /**
     Updates opacities of a batch of {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     @method setOpacity
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param [opacity=1] Number Opacity factor in range ````[0..1]````, multiplies by the rendered pixel alphas.
     */
    setOpacity(ids, opacity) {
        return this.withObjects(ids, object => {
            object.opacity = opacity;
        });
    }

    /**
     Sets a batch of {{#crossLink "Object"}}Objects{{/crossLink}} pickable or unpickable, specified by their IDs, GUIDs and/or entity types.

     Picking is done via calls to {{#crossLink "Scene/pick:method"}}Scene#pick(){{/crossLink}}.

     @method setPickable
     @param ids {Array} Array of  {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param pickable {Float32Array} Whether to ghost or un-ghost.
     @returns {Boolean} True if any {{#crossLink "Object"}}Objects{{/crossLink}} changed pickable state, else false if all updates were redundant and not applied.
     */
    setPickable(ids, pickable) {
        return this.withObjects(ids, object => {
            const changed = (object.pickable !== pickable);
            object.pickable = pickable;
            return changed;
        });
    }

    /**
     Iterates with a callback over {{#crossLink "Object"}}Objects{{/crossLink}}, specified by their IDs, GUIDs and/or entity types.

     @method withObjects
     @param ids {String|Array} One or more {{#crossLink "Object"}}{{/crossLink}} IDs, GUIDs or entity types.
     @param callback {Function} The callback, which takes each object as its argument.
     */
    withObjects(ids, callback) {
        if (utils.isString(ids)) {
            ids = [ids];
        }
        let changed = false;
        for (let i = 0, len = ids.length; i < len; i++) {
            const id = ids[i];
            let object = this.objects[id];
            if (object) {
                changed = callback(object) || changed;
            } else {
                object = this.guidObjects[id];
                if (object) {
                    changed = callback(object) || changed;
                } else {
                    const objects = this.entityTypes[id];
                    if (objects) {
                        for (const objectId in objects) {
                            if (objects.hasOwnProperty(objectId)) {
                                changed = callback(objects[objectId]) || changed;
                            }
                        }
                    }
                }
            }
        }
        return changed;
    }

    destroy() {

        super.destroy();

        for (const id in this.components) {
            if (this.components.hasOwnProperty(id)) {
                this.components[id].destroy();
            }
        }

        this.canvas.gl = null;

        // Memory leak prevention
        this.models = null;
        this.objects = null;
        this.guidObjects = null;
        this.entityTypes = null;
        this.entities = null;
        this.visibleEntities = null;
        this.ghostedEntities = null;
        this.highlightedEntities = null;
        this.selectedEntities = null;
        this.clips = null;
        this.lights = null;
        this.lightMaps = null;
        this.reflectionMaps = null;
        this._objectGUIDs = null;
        this._entityIds = null;
        this._visibleEntityIds = null;
        this._ghostedEntityIds = null;
        this._highlightedEntityIds = null;
        this._selectedEntityIds = null;
        this.meshes = null;
        this.types = null;
        this.components = null;
        this.rootObjects = null;
        this.canvas = null;
        this._renderer = null;
        this.input = null;
        this._viewport = null;
        this._camera = null;
    }
}

componentClasses[type] = Scene;

export {Scene};