(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
	typeof define === 'function' && define.amd ? define(['exports'], factory) :
	(global = global || self, factory(global.Potree = {}));
}(this, (function (exports) { 'use strict';

	/**
	 * @author mrdoob / http://mrdoob.com/ https://github.com/mrdoob/eventdispatcher.js
	 * 
	 * with slight modifications by mschuetz, http://potree.org
	 * 
	 */

	// The MIT License
	// 
	// Copyright (c) 2011 Mr.doob
	// 
	// Permission is hereby granted, free of charge, to any person obtaining a copy
	// of this software and associated documentation files (the "Software"), to deal
	// in the Software without restriction, including without limitation the rights
	// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	// copies of the Software, and to permit persons to whom the Software is
	// furnished to do so, subject to the following conditions:
	// 
	// The above copyright notice and this permission notice shall be included in
	// all copies or substantial portions of the Software.
	// 
	// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	// THE SOFTWARE.





	class EventDispatcher{

		constructor(){
			this._listeners = {};
		}

		addEventListener(type, listener){

			const listeners = this._listeners;

			if(listeners[type] === undefined){
				listeners[type] = [];
			}

			if(listeners[type].indexOf(listener) === - 1){
				listeners[type].push( listener );
			}

		}

		hasEventListener(type, listener){

			const listeners = this._listeners;

			return listeners[type] !== undefined && listeners[type].indexOf(listener) !== - 1;
		}

		removeEventListener(type, listener){

			let listeners = this._listeners;
			let listenerArray = listeners[type];

			if (listenerArray !== undefined){

				let index = listenerArray.indexOf(listener);

				if(index !== - 1){
					listenerArray.splice(index, 1);
				}
			}

		}

		removeEventListeners(type){
			if(this._listeners[type] !== undefined){
				delete this._listeners[type];
			}
		};

		dispatchEvent(event){

			let listeners = this._listeners;
			let listenerArray = listeners[event.type];

			if ( listenerArray !== undefined ) {
				event.target = this;

				for(let listener of listenerArray.slice(0)){
					listener.call(this, event);
				}
			}

		}

	}

	class Action extends EventDispatcher {
		constructor (args = {}) {
			super();

			this.icon = args.icon || '';
			this.tooltip = args.tooltip;

			if (args.onclick !== undefined) {
				this.onclick = args.onclick;
			}
		}

		onclick (event) {

		}

		pairWith (object) {

		}

		setIcon (newIcon) {
			let oldIcon = this.icon;

			if (newIcon === oldIcon) {
				return;
			}

			this.icon = newIcon;

			this.dispatchEvent({
				type: 'icon_changed',
				action: this,
				icon: newIcon,
				oldIcon: oldIcon
			});
		}
	};

	//Potree.Actions = {};
	//
	//Potree.Actions.ToggleAnnotationVisibility = class ToggleAnnotationVisibility extends Potree.Action {
	//	constructor (args = {}) {
	//		super(args);
	//
	//		this.icon = Potree.resourcePath + '/icons/eye.svg';
	//		this.showIn = 'sidebar';
	//		this.tooltip = 'toggle visibility';
	//	}
	//
	//	pairWith (annotation) {
	//		if (annotation.visible) {
	//			this.setIcon(Potree.resourcePath + '/icons/eye.svg');
	//		} else {
	//			this.setIcon(Potree.resourcePath + '/icons/eye_crossed.svg');
	//		}
	//
	//		annotation.addEventListener('visibility_changed', e => {
	//			let annotation = e.annotation;
	//
	//			if (annotation.visible) {
	//				this.setIcon(Potree.resourcePath + '/icons/eye.svg');
	//			} else {
	//				this.setIcon(Potree.resourcePath + '/icons/eye_crossed.svg');
	//			}
	//		});
	//	}
	//
	//	onclick (event) {
	//		let annotation = event.annotation;
	//
	//		annotation.visible = !annotation.visible;
	//
	//		if (annotation.visible) {
	//			this.setIcon(Potree.resourcePath + '/icons/eye.svg');
	//		} else {
	//			this.setIcon(Potree.resourcePath + '/icons/eye_crossed.svg');
	//		}
	//	}
	//};

	// threejs.org/license
	const REVISION = '124';
	const MOUSE = { LEFT: 0, MIDDLE: 1, RIGHT: 2, ROTATE: 0, DOLLY: 1, PAN: 2 };
	const TOUCH = { ROTATE: 0, PAN: 1, DOLLY_PAN: 2, DOLLY_ROTATE: 3 };
	const CullFaceNone = 0;
	const CullFaceBack = 1;
	const CullFaceFront = 2;
	const CullFaceFrontBack = 3;
	const BasicShadowMap = 0;
	const PCFShadowMap = 1;
	const PCFSoftShadowMap = 2;
	const VSMShadowMap = 3;
	const FrontSide = 0;
	const BackSide = 1;
	const DoubleSide = 2;
	const FlatShading = 1;
	const SmoothShading = 2;
	const NoBlending = 0;
	const NormalBlending = 1;
	const AdditiveBlending = 2;
	const SubtractiveBlending = 3;
	const MultiplyBlending = 4;
	const CustomBlending = 5;
	const AddEquation = 100;
	const SubtractEquation = 101;
	const ReverseSubtractEquation = 102;
	const MinEquation = 103;
	const MaxEquation = 104;
	const ZeroFactor = 200;
	const OneFactor = 201;
	const SrcColorFactor = 202;
	const OneMinusSrcColorFactor = 203;
	const SrcAlphaFactor = 204;
	const OneMinusSrcAlphaFactor = 205;
	const DstAlphaFactor = 206;
	const OneMinusDstAlphaFactor = 207;
	const DstColorFactor = 208;
	const OneMinusDstColorFactor = 209;
	const SrcAlphaSaturateFactor = 210;
	const NeverDepth = 0;
	const AlwaysDepth = 1;
	const LessDepth = 2;
	const LessEqualDepth = 3;
	const EqualDepth = 4;
	const GreaterEqualDepth = 5;
	const GreaterDepth = 6;
	const NotEqualDepth = 7;
	const MultiplyOperation = 0;
	const MixOperation = 1;
	const AddOperation = 2;
	const NoToneMapping = 0;
	const LinearToneMapping = 1;
	const ReinhardToneMapping = 2;
	const CineonToneMapping = 3;
	const ACESFilmicToneMapping = 4;
	const CustomToneMapping = 5;

	const UVMapping = 300;
	const CubeReflectionMapping = 301;
	const CubeRefractionMapping = 302;
	const EquirectangularReflectionMapping = 303;
	const EquirectangularRefractionMapping = 304;
	const CubeUVReflectionMapping = 306;
	const CubeUVRefractionMapping = 307;
	const RepeatWrapping = 1000;
	const ClampToEdgeWrapping = 1001;
	const MirroredRepeatWrapping = 1002;
	const NearestFilter = 1003;
	const NearestMipmapNearestFilter = 1004;
	const NearestMipMapNearestFilter = 1004;
	const NearestMipmapLinearFilter = 1005;
	const NearestMipMapLinearFilter = 1005;
	const LinearFilter = 1006;
	const LinearMipmapNearestFilter = 1007;
	const LinearMipMapNearestFilter = 1007;
	const LinearMipmapLinearFilter = 1008;
	const LinearMipMapLinearFilter = 1008;
	const UnsignedByteType = 1009;
	const ByteType = 1010;
	const ShortType = 1011;
	const UnsignedShortType = 1012;
	const IntType = 1013;
	const UnsignedIntType = 1014;
	const FloatType = 1015;
	const HalfFloatType = 1016;
	const UnsignedShort4444Type = 1017;
	const UnsignedShort5551Type = 1018;
	const UnsignedShort565Type = 1019;
	const UnsignedInt248Type$1 = 1020;
	const AlphaFormat = 1021;
	const RGBFormat = 1022;
	const RGBAFormat = 1023;
	const LuminanceFormat = 1024;
	const LuminanceAlphaFormat = 1025;
	const RGBEFormat = RGBAFormat;
	const DepthFormat = 1026;
	const DepthStencilFormat = 1027;
	const RedFormat = 1028;
	const RedIntegerFormat = 1029;
	const RGFormat = 1030;
	const RGIntegerFormat = 1031;
	const RGBIntegerFormat = 1032;
	const RGBAIntegerFormat = 1033;

	const RGB_S3TC_DXT1_Format = 33776;
	const RGBA_S3TC_DXT1_Format$1 = 33777;
	const RGBA_S3TC_DXT3_Format = 33778;
	const RGBA_S3TC_DXT5_Format$1 = 33779;
	const RGB_PVRTC_4BPPV1_Format = 35840;
	const RGB_PVRTC_2BPPV1_Format = 35841;
	const RGBA_PVRTC_4BPPV1_Format = 35842;
	const RGBA_PVRTC_2BPPV1_Format = 35843;
	const RGB_ETC1_Format = 36196;
	const RGB_ETC2_Format = 37492;
	const RGBA_ETC2_EAC_Format = 37496;
	const RGBA_ASTC_4x4_Format = 37808;
	const RGBA_ASTC_5x4_Format = 37809;
	const RGBA_ASTC_5x5_Format = 37810;
	const RGBA_ASTC_6x5_Format = 37811;
	const RGBA_ASTC_6x6_Format = 37812;
	const RGBA_ASTC_8x5_Format = 37813;
	const RGBA_ASTC_8x6_Format = 37814;
	const RGBA_ASTC_8x8_Format = 37815;
	const RGBA_ASTC_10x5_Format = 37816;
	const RGBA_ASTC_10x6_Format = 37817;
	const RGBA_ASTC_10x8_Format = 37818;
	const RGBA_ASTC_10x10_Format = 37819;
	const RGBA_ASTC_12x10_Format = 37820;
	const RGBA_ASTC_12x12_Format = 37821;
	const RGBA_BPTC_Format = 36492;
	const SRGB8_ALPHA8_ASTC_4x4_Format = 37840;
	const SRGB8_ALPHA8_ASTC_5x4_Format = 37841;
	const SRGB8_ALPHA8_ASTC_5x5_Format = 37842;
	const SRGB8_ALPHA8_ASTC_6x5_Format = 37843;
	const SRGB8_ALPHA8_ASTC_6x6_Format = 37844;
	const SRGB8_ALPHA8_ASTC_8x5_Format = 37845;
	const SRGB8_ALPHA8_ASTC_8x6_Format = 37846;
	const SRGB8_ALPHA8_ASTC_8x8_Format = 37847;
	const SRGB8_ALPHA8_ASTC_10x5_Format = 37848;
	const SRGB8_ALPHA8_ASTC_10x6_Format = 37849;
	const SRGB8_ALPHA8_ASTC_10x8_Format = 37850;
	const SRGB8_ALPHA8_ASTC_10x10_Format = 37851;
	const SRGB8_ALPHA8_ASTC_12x10_Format = 37852;
	const SRGB8_ALPHA8_ASTC_12x12_Format = 37853;
	const LoopOnce = 2200;
	const LoopRepeat = 2201;
	const LoopPingPong = 2202;
	const InterpolateDiscrete = 2300;
	const InterpolateLinear = 2301;
	const InterpolateSmooth = 2302;
	const ZeroCurvatureEnding = 2400;
	const ZeroSlopeEnding = 2401;
	const WrapAroundEnding = 2402;
	const NormalAnimationBlendMode = 2500;
	const AdditiveAnimationBlendMode = 2501;
	const TrianglesDrawMode = 0;
	const TriangleStripDrawMode = 1;
	const TriangleFanDrawMode = 2;
	const LinearEncoding = 3000;
	const sRGBEncoding = 3001;
	const GammaEncoding = 3007;
	const RGBEEncoding = 3002;
	const LogLuvEncoding = 3003;
	const RGBM7Encoding = 3004;
	const RGBM16Encoding = 3005;
	const RGBDEncoding = 3006;
	const BasicDepthPacking = 3200;
	const RGBADepthPacking = 3201;
	const TangentSpaceNormalMap = 0;
	const ObjectSpaceNormalMap = 1;

	const ZeroStencilOp = 0;
	const KeepStencilOp = 7680;
	const ReplaceStencilOp = 7681;
	const IncrementStencilOp = 7682;
	const DecrementStencilOp = 7683;
	const IncrementWrapStencilOp = 34055;
	const DecrementWrapStencilOp = 34056;
	const InvertStencilOp = 5386;

	const NeverStencilFunc = 512;
	const LessStencilFunc = 513;
	const EqualStencilFunc = 514;
	const LessEqualStencilFunc = 515;
	const GreaterStencilFunc = 516;
	const NotEqualStencilFunc = 517;
	const GreaterEqualStencilFunc = 518;
	const AlwaysStencilFunc = 519;

	const StaticDrawUsage = 35044;
	const DynamicDrawUsage = 35048;
	const StreamDrawUsage = 35040;
	const StaticReadUsage = 35045;
	const DynamicReadUsage = 35049;
	const StreamReadUsage = 35041;
	const StaticCopyUsage = 35046;
	const DynamicCopyUsage = 35050;
	const StreamCopyUsage = 35042;

	const GLSL1 = '100';
	const GLSL3 = '300 es';

	/**
	 * https://github.com/mrdoob/eventdispatcher.js/
	 */

	function EventDispatcher$1() {}

	Object.assign( EventDispatcher$1.prototype, {

		addEventListener: function ( type, listener ) {

			if ( this._listeners === undefined ) this._listeners = {};

			const listeners = this._listeners;

			if ( listeners[ type ] === undefined ) {

				listeners[ type ] = [];

			}

			if ( listeners[ type ].indexOf( listener ) === - 1 ) {

				listeners[ type ].push( listener );

			}

		},

		hasEventListener: function ( type, listener ) {

			if ( this._listeners === undefined ) return false;

			const listeners = this._listeners;

			return listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== - 1;

		},

		removeEventListener: function ( type, listener ) {

			if ( this._listeners === undefined ) return;

			const listeners = this._listeners;
			const listenerArray = listeners[ type ];

			if ( listenerArray !== undefined ) {

				const index = listenerArray.indexOf( listener );

				if ( index !== - 1 ) {

					listenerArray.splice( index, 1 );

				}

			}

		},

		dispatchEvent: function ( event ) {

			if ( this._listeners === undefined ) return;

			const listeners = this._listeners;
			const listenerArray = listeners[ event.type ];

			if ( listenerArray !== undefined ) {

				event.target = this;

				// Make a copy, in case listeners are removed while iterating.
				const array = listenerArray.slice( 0 );

				for ( let i = 0, l = array.length; i < l; i ++ ) {

					array[ i ].call( this, event );

				}

			}

		}

	} );

	const _lut = [];

	for ( let i = 0; i < 256; i ++ ) {

		_lut[ i ] = ( i < 16 ? '0' : '' ) + ( i ).toString( 16 );

	}

	let _seed = 1234567;

	const MathUtils = {

		DEG2RAD: Math.PI / 180,
		RAD2DEG: 180 / Math.PI,

		generateUUID: function () {

			// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/21963136#21963136

			const d0 = Math.random() * 0xffffffff | 0;
			const d1 = Math.random() * 0xffffffff | 0;
			const d2 = Math.random() * 0xffffffff | 0;
			const d3 = Math.random() * 0xffffffff | 0;
			const uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' +
				_lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' +
				_lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] +
				_lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ];

			// .toUpperCase() here flattens concatenated strings to save heap memory space.
			return uuid.toUpperCase();

		},

		clamp: function ( value, min, max ) {

			return Math.max( min, Math.min( max, value ) );

		},

		// compute euclidian modulo of m % n
		// https://en.wikipedia.org/wiki/Modulo_operation

		euclideanModulo: function ( n, m ) {

			return ( ( n % m ) + m ) % m;

		},

		// Linear mapping from range <a1, a2> to range <b1, b2>

		mapLinear: function ( x, a1, a2, b1, b2 ) {

			return b1 + ( x - a1 ) * ( b2 - b1 ) / ( a2 - a1 );

		},

		// https://en.wikipedia.org/wiki/Linear_interpolation

		lerp: function ( x, y, t ) {

			return ( 1 - t ) * x + t * y;

		},

		// http://en.wikipedia.org/wiki/Smoothstep

		smoothstep: function ( x, min, max ) {

			if ( x <= min ) return 0;
			if ( x >= max ) return 1;

			x = ( x - min ) / ( max - min );

			return x * x * ( 3 - 2 * x );

		},

		smootherstep: function ( x, min, max ) {

			if ( x <= min ) return 0;
			if ( x >= max ) return 1;

			x = ( x - min ) / ( max - min );

			return x * x * x * ( x * ( x * 6 - 15 ) + 10 );

		},

		// Random integer from <low, high> interval

		randInt: function ( low, high ) {

			return low + Math.floor( Math.random() * ( high - low + 1 ) );

		},

		// Random float from <low, high> interval

		randFloat: function ( low, high ) {

			return low + Math.random() * ( high - low );

		},

		// Random float from <-range/2, range/2> interval

		randFloatSpread: function ( range ) {

			return range * ( 0.5 - Math.random() );

		},

		// Deterministic pseudo-random float in the interval [ 0, 1 ]

		seededRandom: function ( s ) {

			if ( s !== undefined ) _seed = s % 2147483647;

			// Park-Miller algorithm

			_seed = _seed * 16807 % 2147483647;

			return ( _seed - 1 ) / 2147483646;

		},

		degToRad: function ( degrees ) {

			return degrees * MathUtils.DEG2RAD;

		},

		radToDeg: function ( radians ) {

			return radians * MathUtils.RAD2DEG;

		},

		isPowerOfTwo: function ( value ) {

			return ( value & ( value - 1 ) ) === 0 && value !== 0;

		},

		ceilPowerOfTwo: function ( value ) {

			return Math.pow( 2, Math.ceil( Math.log( value ) / Math.LN2 ) );

		},

		floorPowerOfTwo: function ( value ) {

			return Math.pow( 2, Math.floor( Math.log( value ) / Math.LN2 ) );

		},

		setQuaternionFromProperEuler: function ( q, a, b, c, order ) {

			// Intrinsic Proper Euler Angles - see https://en.wikipedia.org/wiki/Euler_angles

			// rotations are applied to the axes in the order specified by 'order'
			// rotation by angle 'a' is applied first, then by angle 'b', then by angle 'c'
			// angles are in radians

			const cos = Math.cos;
			const sin = Math.sin;

			const c2 = cos( b / 2 );
			const s2 = sin( b / 2 );

			const c13 = cos( ( a + c ) / 2 );
			const s13 = sin( ( a + c ) / 2 );

			const c1_3 = cos( ( a - c ) / 2 );
			const s1_3 = sin( ( a - c ) / 2 );

			const c3_1 = cos( ( c - a ) / 2 );
			const s3_1 = sin( ( c - a ) / 2 );

			switch ( order ) {

				case 'XYX':
					q.set( c2 * s13, s2 * c1_3, s2 * s1_3, c2 * c13 );
					break;

				case 'YZY':
					q.set( s2 * s1_3, c2 * s13, s2 * c1_3, c2 * c13 );
					break;

				case 'ZXZ':
					q.set( s2 * c1_3, s2 * s1_3, c2 * s13, c2 * c13 );
					break;

				case 'XZX':
					q.set( c2 * s13, s2 * s3_1, s2 * c3_1, c2 * c13 );
					break;

				case 'YXY':
					q.set( s2 * c3_1, c2 * s13, s2 * s3_1, c2 * c13 );
					break;

				case 'ZYZ':
					q.set( s2 * s3_1, s2 * c3_1, c2 * s13, c2 * c13 );
					break;

				default:
					console.warn( 'THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: ' + order );

			}

		}

	};

	class Vector2 {

		constructor( x = 0, y = 0 ) {

			Object.defineProperty( this, 'isVector2', { value: true } );

			this.x = x;
			this.y = y;

		}

		get width() {

			return this.x;

		}

		set width( value ) {

			this.x = value;

		}

		get height() {

			return this.y;

		}

		set height( value ) {

			this.y = value;

		}

		set( x, y ) {

			this.x = x;
			this.y = y;

			return this;

		}

		setScalar( scalar ) {

			this.x = scalar;
			this.y = scalar;

			return this;

		}

		setX( x ) {

			this.x = x;

			return this;

		}

		setY( y ) {

			this.y = y;

			return this;

		}

		setComponent( index, value ) {

			switch ( index ) {

				case 0: this.x = value; break;
				case 1: this.y = value; break;
				default: throw new Error( 'index is out of range: ' + index );

			}

			return this;

		}

		getComponent( index ) {

			switch ( index ) {

				case 0: return this.x;
				case 1: return this.y;
				default: throw new Error( 'index is out of range: ' + index );

			}

		}

		clone() {

			return new this.constructor( this.x, this.y );

		}

		copy( v ) {

			this.x = v.x;
			this.y = v.y;

			return this;

		}

		add( v, w ) {

			if ( w !== undefined ) {

				console.warn( 'THREE.Vector2: .add() now only accepts one argument. Use .addVectors( a, b ) instead.' );
				return this.addVectors( v, w );

			}

			this.x += v.x;
			this.y += v.y;

			return this;

		}

		addScalar( s ) {

			this.x += s;
			this.y += s;

			return this;

		}

		addVectors( a, b ) {

			this.x = a.x + b.x;
			this.y = a.y + b.y;

			return this;

		}

		addScaledVector( v, s ) {

			this.x += v.x * s;
			this.y += v.y * s;

			return this;

		}

		sub( v, w ) {

			if ( w !== undefined ) {

				console.warn( 'THREE.Vector2: .sub() now only accepts one argument. Use .subVectors( a, b ) instead.' );
				return this.subVectors( v, w );

			}

			this.x -= v.x;
			this.y -= v.y;

			return this;

		}

		subScalar( s ) {

			this.x -= s;
			this.y -= s;

			return this;

		}

		subVectors( a, b ) {

			this.x = a.x - b.x;
			this.y = a.y - b.y;

			return this;

		}

		multiply( v ) {

			this.x *= v.x;
			this.y *= v.y;

			return this;

		}

		multiplyScalar( scalar ) {

			this.x *= scalar;
			this.y *= scalar;

			return this;

		}

		divide( v ) {

			this.x /= v.x;
			this.y /= v.y;

			return this;

		}

		divideScalar( scalar ) {

			return this.multiplyScalar( 1 / scalar );

		}

		applyMatrix3( m ) {

			const x = this.x, y = this.y;
			const e = m.elements;

			this.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ];
			this.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ];

			return this;

		}

		min( v ) {

			this.x = Math.min( this.x, v.x );
			this.y = Math.min( this.y, v.y );

			return this;

		}

		max( v ) {

			this.x = Math.max( this.x, v.x );
			this.y = Math.max( this.y, v.y );

			return this;

		}

		clamp( min, max ) {

			// assumes min < max, componentwise

			this.x = Math.max( min.x, Math.min( max.x, this.x ) );
			this.y = Math.max( min.y, Math.min( max.y, this.y ) );

			return this;

		}

		clampScalar( minVal, maxVal ) {

			this.x = Math.max( minVal, Math.min( maxVal, this.x ) );
			this.y = Math.max( minVal, Math.min( maxVal, this.y ) );

			return this;

		}

		clampLength( min, max ) {

			const length = this.length();

			return this.divideScalar( length || 1 ).multiplyScalar( Math.max( min, Math.min( max, length ) ) );

		}

		floor() {

			this.x = Math.floor( this.x );
			this.y = Math.floor( this.y );

			return this;

		}

		ceil() {

			this.x = Math.ceil( this.x );
			this.y = Math.ceil( this.y );

			return this;

		}

		round() {

			this.x = Math.round( this.x );
			this.y = Math.round( this.y );

			return this;

		}

		roundToZero() {

			this.x = ( this.x < 0 ) ? Math.ceil( this.x ) : Math.floor( this.x );
			this.y = ( this.y < 0 ) ? Math.ceil( this.y ) : Math.floor( this.y );

			return this;

		}

		negate() {

			this.x = - this.x;
			this.y = - this.y;

			return this;

		}

		dot( v ) {

			return this.x * v.x + this.y * v.y;

		}

		cross( v ) {

			return this.x * v.y - this.y * v.x;

		}

		lengthSq() {

			return this.x * this.x + this.y * this.y;

		}

		length() {

			return Math.sqrt( this.x * this.x + this.y * this.y );

		}

		manhattanLength() {

			return Math.abs( this.x ) + Math.abs( this.y );

		}

		normalize() {

			return this.divideScalar( this.length() || 1 );

		}

		angle() {

			// computes the angle in radians with respect to the positive x-axis

			const angle = Math.atan2( - this.y, - this.x ) + Math.PI;

			return angle;

		}

		distanceTo( v ) {

			return Math.sqrt( this.distanceToSquared( v ) );

		}

		distanceToSquared( v ) {

			const dx = this.x - v.x, dy = this.y - v.y;
			return dx * dx + dy * dy;

		}

		manhattanDistanceTo( v ) {

			return Math.abs( this.x - v.x ) + Math.abs( this.y - v.y );

		}

		setLength( length ) {

			return this.normalize().multiplyScalar( length );

		}

		lerp( v, alpha ) {

			this.x += ( v.x - this.x ) * alpha;
			this.y += ( v.y - this.y ) * alpha;

			return this;

		}

		lerpVectors( v1, v2, alpha ) {

			this.x = v1.x + ( v2.x - v1.x ) * alpha;
			this.y = v1.y + ( v2.y - v1.y ) * alpha;

			return this;

		}

		equals( v ) {

			return ( ( v.x === this.x ) && ( v.y === this.y ) );

		}

		fromArray( array, offset = 0 ) {

			this.x = array[ offset ];
			this.y = array[ offset + 1 ];

			return this;

		}

		toArray( array = [], offset = 0 ) {

			array[ offset ] = this.x;
			array[ offset + 1 ] = this.y;

			return array;

		}

		fromBufferAttribute( attribute, index, offset ) {

			if ( offset !== undefined ) {

				console.warn( 'THREE.Vector2: offset has been removed from .fromBufferAttribute().' );

			}

			this.x = attribute.getX( index );
			this.y = attribute.getY( index );

			return this;

		}

		rotateAround( center, angle ) {

			const c = Math.cos( angle ), s = Math.sin( angle );

			const x = this.x - center.x;
			const y = this.y - center.y;

			this.x = x * c - y * s + center.x;
			this.y = x * s + y * c + center.y;

			return this;

		}

		random() {

			this.x = Math.random();
			this.y = Math.random();

			return this;

		}

	}

	class Matrix3 {

		constructor() {

			Object.defineProperty( this, 'isMatrix3', { value: true } );

			this.elements = [

				1, 0, 0,
				0, 1, 0,
				0, 0, 1

			];

			if ( arguments.length > 0 ) {

				console.error( 'THREE.Matrix3: the constructor no longer reads arguments. use .set() instead.' );

			}

		}

		set( n11, n12, n13, n21, n22, n23, n31, n32, n33 ) {

			const te = this.elements;

			te[ 0 ] = n11; te[ 1 ] = n21; te[ 2 ] = n31;
			te[ 3 ] = n12; te[ 4 ] = n22; te[ 5 ] = n32;
			te[ 6 ] = n13; te[ 7 ] = n23; te[ 8 ] = n33;

			return this;

		}

		identity() {

			this.set(

				1, 0, 0,
				0, 1, 0,
				0, 0, 1

			);

			return this;

		}

		clone() {

			return new this.constructor().fromArray( this.elements );

		}

		copy( m ) {

			const te = this.elements;
			const me = m.elements;

			te[ 0 ] = me[ 0 ]; te[ 1 ] = me[ 1 ]; te[ 2 ] = me[ 2 ];
			te[ 3 ] = me[ 3 ]; te[ 4 ] = me[ 4 ]; te[ 5 ] = me[ 5 ];
			te[ 6 ] = me[ 6 ]; te[ 7 ] = me[ 7 ]; te[ 8 ] = me[ 8 ];

			return this;

		}

		extractBasis( xAxis, yAxis, zAxis ) {

			xAxis.setFromMatrix3Column( this, 0 );
			yAxis.setFromMatrix3Column( this, 1 );
			zAxis.setFromMatrix3Column( this, 2 );

			return this;

		}

		setFromMatrix4( m ) {

			const me = m.elements;

			this.set(

				me[ 0 ], me[ 4 ], me[ 8 ],
				me[ 1 ], me[ 5 ], me[ 9 ],
				me[ 2 ], me[ 6 ], me[ 10 ]

			);

			return this;

		}

		multiply( m ) {

			return this.multiplyMatrices( this, m );

		}

		premultiply( m ) {

			return this.multiplyMatrices( m, this );

		}

		multiplyMatrices( a, b ) {

			const ae = a.elements;
			const be = b.elements;
			const te = this.elements;

			const a11 = ae[ 0 ], a12 = ae[ 3 ], a13 = ae[ 6 ];
			const a21 = ae[ 1 ], a22 = ae[ 4 ], a23 = ae[ 7 ];
			const a31 = ae[ 2 ], a32 = ae[ 5 ], a33 = ae[ 8 ];

			const b11 = be[ 0 ], b12 = be[ 3 ], b13 = be[ 6 ];
			const b21 = be[ 1 ], b22 = be[ 4 ], b23 = be[ 7 ];
			const b31 = be[ 2 ], b32 = be[ 5 ], b33 = be[ 8 ];

			te[ 0 ] = a11 * b11 + a12 * b21 + a13 * b31;
			te[ 3 ] = a11 * b12 + a12 * b22 + a13 * b32;
			te[ 6 ] = a11 * b13 + a12 * b23 + a13 * b33;

			te[ 1 ] = a21 * b11 + a22 * b21 + a23 * b31;
			te[ 4 ] = a21 * b12 + a22 * b22 + a23 * b32;
			te[ 7 ] = a21 * b13 + a22 * b23 + a23 * b33;

			te[ 2 ] = a31 * b11 + a32 * b21 + a33 * b31;
			te[ 5 ] = a31 * b12 + a32 * b22 + a33 * b32;
			te[ 8 ] = a31 * b13 + a32 * b23 + a33 * b33;

			return this;

		}

		multiplyScalar( s ) {

			const te = this.elements;

			te[ 0 ] *= s; te[ 3 ] *= s; te[ 6 ] *= s;
			te[ 1 ] *= s; te[ 4 ] *= s; te[ 7 ] *= s;
			te[ 2 ] *= s; te[ 5 ] *= s; te[ 8 ] *= s;

			return this;

		}

		determinant() {

			const te = this.elements;

			const a = te[ 0 ], b = te[ 1 ], c = te[ 2 ],
				d = te[ 3 ], e = te[ 4 ], f = te[ 5 ],
				g = te[ 6 ], h = te[ 7 ], i = te[ 8 ];

			return a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g;

		}

		invert() {

			const te = this.elements,

				n11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ],
				n12 = te[ 3 ], n22 = te[ 4 ], n32 = te[ 5 ],
				n13 = te[ 6 ], n23 = te[ 7 ], n33 = te[ 8 ],

				t11 = n33 * n22 - n32 * n23,
				t12 = n32 * n13 - n33 * n12,
				t13 = n23 * n12 - n22 * n13,

				det = n11 * t11 + n21 * t12 + n31 * t13;

			if ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0 );

			const detInv = 1 / det;

			te[ 0 ] = t11 * detInv;
			te[ 1 ] = ( n31 * n23 - n33 * n21 ) * detInv;
			te[ 2 ] = ( n32 * n21 - n31 * n22 ) * detInv;

			te[ 3 ] = t12 * detInv;
			te[ 4 ] = ( n33 * n11 - n31 * n13 ) * detInv;
			te[ 5 ] = ( n31 * n12 - n32 * n11 ) * detInv;

			te[ 6 ] = t13 * detInv;
			te[ 7 ] = ( n21 * n13 - n23 * n11 ) * detInv;
			te[ 8 ] = ( n22 * n11 - n21 * n12 ) * detInv;

			return this;

		}

		transpose() {

			let tmp;
			const m = this.elements;

			tmp = m[ 1 ]; m[ 1 ] = m[ 3 ]; m[ 3 ] = tmp;
			tmp = m[ 2 ]; m[ 2 ] = m[ 6 ]; m[ 6 ] = tmp;
			tmp = m[ 5 ]; m[ 5 ] = m[ 7 ]; m[ 7 ] = tmp;

			return this;

		}

		getNormalMatrix( matrix4 ) {

			return this.setFromMatrix4( matrix4 ).copy( this ).invert().transpose();

		}

		transposeIntoArray( r ) {

			const m = this.elements;

			r[ 0 ] = m[ 0 ];
			r[ 1 ] = m[ 3 ];
			r[ 2 ] = m[ 6 ];
			r[ 3 ] = m[ 1 ];
			r[ 4 ] = m[ 4 ];
			r[ 5 ] = m[ 7 ];
			r[ 6 ] = m[ 2 ];
			r[ 7 ] = m[ 5 ];
			r[ 8 ] = m[ 8 ];

			return this;

		}

		setUvTransform( tx, ty, sx, sy, rotation, cx, cy ) {

			const c = Math.cos( rotation );
			const s = Math.sin( rotation );

			this.set(
				sx * c, sx * s, - sx * ( c * cx + s * cy ) + cx + tx,
				- sy * s, sy * c, - sy * ( - s * cx + c * cy ) + cy + ty,
				0, 0, 1
			);

			return this;

		}

		scale( sx, sy ) {

			const te = this.elements;

			te[ 0 ] *= sx; te[ 3 ] *= sx; te[ 6 ] *= sx;
			te[ 1 ] *= sy; te[ 4 ] *= sy; te[ 7 ] *= sy;

			return this;

		}

		rotate( theta ) {

			const c = Math.cos( theta );
			const s = Math.sin( theta );

			const te = this.elements;

			const a11 = te[ 0 ], a12 = te[ 3 ], a13 = te[ 6 ];
			const a21 = te[ 1 ], a22 = te[ 4 ], a23 = te[ 7 ];

			te[ 0 ] = c * a11 + s * a21;
			te[ 3 ] = c * a12 + s * a22;
			te[ 6 ] = c * a13 + s * a23;

			te[ 1 ] = - s * a11 + c * a21;
			te[ 4 ] = - s * a12 + c * a22;
			te[ 7 ] = - s * a13 + c * a23;

			return this;

		}

		translate( tx, ty ) {

			const te = this.elements;

			te[ 0 ] += tx * te[ 2 ]; te[ 3 ] += tx * te[ 5 ]; te[ 6 ] += tx * te[ 8 ];
			te[ 1 ] += ty * te[ 2 ]; te[ 4 ] += ty * te[ 5 ]; te[ 7 ] += ty * te[ 8 ];

			return this;

		}

		equals( matrix ) {

			const te = this.elements;
			const me = matrix.elements;

			for ( let i = 0; i < 9; i ++ ) {

				if ( te[ i ] !== me[ i ] ) return false;

			}

			return true;

		}

		fromArray( array, offset = 0 ) {

			for ( let i = 0; i < 9; i ++ ) {

				this.elements[ i ] = array[ i + offset ];

			}

			return this;

		}

		toArray( array = [], offset = 0 ) {

			const te = this.elements;

			array[ offset ] = te[ 0 ];
			array[ offset + 1 ] = te[ 1 ];
			array[ offset + 2 ] = te[ 2 ];

			array[ offset + 3 ] = te[ 3 ];
			array[ offset + 4 ] = te[ 4 ];
			array[ offset + 5 ] = te[ 5 ];

			array[ offset + 6 ] = te[ 6 ];
			array[ offset + 7 ] = te[ 7 ];
			array[ offset + 8 ] = te[ 8 ];

			return array;

		}

	}

	let _canvas;

	const ImageUtils = {

		getDataURL: function ( image ) {

			if ( /^data:/i.test( image.src ) ) {

				return image.src;

			}

			if ( typeof HTMLCanvasElement == 'undefined' ) {

				return image.src;

			}

			let canvas;

			if ( image instanceof HTMLCanvasElement ) {

				canvas = image;

			} else {

				if ( _canvas === undefined ) _canvas = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'canvas' );

				_canvas.width = image.width;
				_canvas.height = image.height;

				const context = _canvas.getContext( '2d' );

				if ( image instanceof ImageData ) {

					context.putImageData( image, 0, 0 );

				} else {

					context.drawImage( image, 0, 0, image.width, image.height );

				}

				canvas = _canvas;

			}

			if ( canvas.width > 2048 || canvas.height > 2048 ) {

				return canvas.toDataURL( 'image/jpeg', 0.6 );

			} else {

				return canvas.toDataURL( 'image/png' );

			}

		}

	};

	let textureId = 0;

	function Texture( image = Texture.DEFAULT_IMAGE, mapping = Texture.DEFAULT_MAPPING, wrapS = ClampToEdgeWrapping, wrapT = ClampToEdgeWrapping, magFilter = LinearFilter, minFilter = LinearMipmapLinearFilter, format = RGBAFormat, type = UnsignedByteType, anisotropy = 1, encoding = LinearEncoding ) {

		Object.defineProperty( this, 'id', { value: textureId ++ } );

		this.uuid = MathUtils.generateUUID();

		this.name = '';

		this.image = image;
		this.mipmaps = [];

		this.mapping = mapping;

		this.wrapS = wrapS;
		this.wrapT = wrapT;

		this.magFilter = magFilter;
		this.minFilter = minFilter;

		this.anisotropy = anisotropy;

		this.format = format;
		this.internalFormat = null;
		this.type = type;

		this.offset = new Vector2( 0, 0 );
		this.repeat = new Vector2( 1, 1 );
		this.center = new Vector2( 0, 0 );
		this.rotation = 0;

		this.matrixAutoUpdate = true;
		this.matrix = new Matrix3();

		this.generateMipmaps = true;
		this.premultiplyAlpha = false;
		this.flipY = true;
		this.unpackAlignment = 4;	// valid values: 1, 2, 4, 8 (see http://www.khronos.org/opengles/sdk/docs/man/xhtml/glPixelStorei.xml)

		// Values of encoding !== THREE.LinearEncoding only supported on map, envMap and emissiveMap.
		//
		// Also changing the encoding after already used by a Material will not automatically make the Material
		// update. You need to explicitly call Material.needsUpdate to trigger it to recompile.
		this.encoding = encoding;

		this.version = 0;
		this.onUpdate = null;

	}

	Texture.DEFAULT_IMAGE = undefined;
	Texture.DEFAULT_MAPPING = UVMapping;

	Texture.prototype = Object.assign( Object.create( EventDispatcher$1.prototype ), {

		constructor: Texture,

		isTexture: true,

		updateMatrix: function () {

			this.matrix.setUvTransform( this.offset.x, this.offset.y, this.repeat.x, this.repeat.y, this.rotation, this.center.x, this.center.y );

		},

		clone: function () {

			return new this.constructor().copy( this );

		},

		copy: function ( source ) {

			this.name = source.name;

			this.image = source.image;
			this.mipmaps = source.mipmaps.slice( 0 );

			this.mapping = source.mapping;

			this.wrapS = source.wrapS;
			this.wrapT = source.wrapT;

			this.magFilter = source.magFilter;
			this.minFilter = source.minFilter;

			this.anisotropy = source.anisotropy;

			this.format = source.format;
			this.internalFormat = source.internalFormat;
			this.type = source.type;

			this.offset.copy( source.offset );
			this.repeat.copy( source.repeat );
			this.center.copy( source.center );
			this.rotation = source.rotation;

			this.matrixAutoUpdate = source.matrixAutoUpdate;
			this.matrix.copy( source.matrix );

			this.generateMipmaps = source.generateMipmaps;
			this.premultiplyAlpha = source.premultiplyAlpha;
			this.flipY = source.flipY;
			this.unpackAlignment = source.unpackAlignment;
			this.encoding = source.encoding;

			return this;

		},

		toJSON: function ( meta ) {

			const isRootObject = ( meta === undefined || typeof meta === 'string' );

			if ( ! isRootObject && meta.textures[ this.uuid ] !== undefined ) {

				return meta.textures[ this.uuid ];

			}

			const output = {

				metadata: {
					version: 4.5,
					type: 'Texture',
					generator: 'Texture.toJSON'
				},

				uuid: this.uuid,
				name: this.name,

				mapping: this.mapping,

				repeat: [ this.repeat.x, this.repeat.y ],
				offset: [ this.offset.x, this.offset.y ],
				center: [ this.center.x, this.center.y ],
				rotation: this.rotation,

				wrap: [ this.wrapS, this.wrapT ],

				format: this.format,
				type: this.type,
				encoding: this.encoding,

				minFilter: this.minFilter,
				magFilter: this.magFilter,
				anisotropy: this.anisotropy,

				flipY: this.flipY,

				premultiplyAlpha: this.premultiplyAlpha,
				unpackAlignment: this.unpackAlignment

			};

			if ( this.image !== undefined ) {

				// TODO: Move to THREE.Image

				const image = this.image;

				if ( image.uuid === undefined ) {

					image.uuid = MathUtils.generateUUID(); // UGH

				}

				if ( ! isRootObject && meta.images[ image.uuid ] === undefined ) {

					let url;

					if ( Array.isArray( image ) ) {

						// process array of images e.g. CubeTexture

						url = [];

						for ( let i = 0, l = image.length; i < l; i ++ ) {

							// check cube texture with data textures

							if ( image[ i ].isDataTexture ) {

								url.push( serializeImage( image[ i ].image ) );

							} else {

								url.push( serializeImage( image[ i ] ) );

							}

						}

					} else {

						// process single image

						url = serializeImage( image );

					}

					meta.images[ image.uuid ] = {
						uuid: image.uuid,
						url: url
					};

				}

				output.image = image.uuid;

			}

			if ( ! isRootObject ) {

				meta.textures[ this.uuid ] = output;

			}

			return output;

		},

		dispose: function () {

			this.dispatchEvent( { type: 'dispose' } );

		},

		transformUv: function ( uv ) {

			if ( this.mapping !== UVMapping ) return uv;

			uv.applyMatrix3( this.matrix );

			if ( uv.x < 0 || uv.x > 1 ) {

				switch ( this.wrapS ) {

					case RepeatWrapping:

						uv.x = uv.x - Math.floor( uv.x );
						break;

					case ClampToEdgeWrapping:

						uv.x = uv.x < 0 ? 0 : 1;
						break;

					case MirroredRepeatWrapping:

						if ( Math.abs( Math.floor( uv.x ) % 2 ) === 1 ) {

							uv.x = Math.ceil( uv.x ) - uv.x;

						} else {

							uv.x = uv.x - Math.floor( uv.x );

						}

						break;

				}

			}

			if ( uv.y < 0 || uv.y > 1 ) {

				switch ( this.wrapT ) {

					case RepeatWrapping:

						uv.y = uv.y - Math.floor( uv.y );
						break;

					case ClampToEdgeWrapping:

						uv.y = uv.y < 0 ? 0 : 1;
						break;

					case MirroredRepeatWrapping:

						if ( Math.abs( Math.floor( uv.y ) % 2 ) === 1 ) {

							uv.y = Math.ceil( uv.y ) - uv.y;

						} else {

							uv.y = uv.y - Math.floor( uv.y );

						}

						break;

				}

			}

			if ( this.flipY ) {

				uv.y = 1 - uv.y;

			}

			return uv;

		}

	} );

	Object.defineProperty( Texture.prototype, 'needsUpdate', {

		set: function ( value ) {

			if ( value === true ) this.version ++;

		}

	} );

	function serializeImage( image ) {

		if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
			( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
			( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {

			// default images

			return ImageUtils.getDataURL( image );

		} else {

			if ( image.data ) {

				// images of DataTexture

				return {
					data: Array.prototype.slice.call( image.data ),
					width: image.width,
					height: image.height,
					type: image.data.constructor.name
				};

			} else {

				console.warn( 'THREE.Texture: Unable to serialize Texture.' );
				return {};

			}

		}

	}

	class Vector4 {

		constructor( x = 0, y = 0, z = 0, w = 1 ) {

			Object.defineProperty( this, 'isVector4', { value: true } );

			this.x = x;
			this.y = y;
			this.z = z;
			this.w = w;

		}

		get width() {

			return this.z;

		}

		set width( value ) {

			this.z = value;

		}

		get height() {

			return this.w;

		}

		set height( value ) {

			this.w = value;

		}

		set( x, y, z, w ) {

			this.x = x;
			this.y = y;
			this.z = z;
			this.w = w;

			return this;

		}

		setScalar( scalar ) {

			this.x = scalar;
			this.y = scalar;
			this.z = scalar;
			this.w = scalar;

			return this;

		}

		setX( x ) {

			this.x = x;

			return this;

		}

		setY( y ) {

			this.y = y;

			return this;

		}

		setZ( z ) {

			this.z = z;

			return this;

		}

		setW( w ) {

			this.w = w;

			return this;

		}

		setComponent( index, value ) {

			switch ( index ) {

				case 0: this.x = value; break;
				case 1: this.y = value; break;
				case 2: this.z = value; break;
				case 3: this.w = value; break;
				default: throw new Error( 'index is out of range: ' + index );

			}

			return this;

		}

		getComponent( index ) {

			switch ( index ) {

				case 0: return this.x;
				case 1: return this.y;
				case 2: return this.z;
				case 3: return this.w;
				default: throw new Error( 'index is out of range: ' + index );

			}

		}

		clone() {

			return new this.constructor( this.x, this.y, this.z, this.w );

		}

		copy( v ) {

			this.x = v.x;
			this.y = v.y;
			this.z = v.z;
			this.w = ( v.w !== undefined ) ? v.w : 1;

			return this;

		}

		add( v, w ) {

			if ( w !== undefined ) {

				console.warn( 'THREE.Vector4: .add() now only accepts one argument. Use .addVectors( a, b ) instead.' );
				return this.addVectors( v, w );

			}

			this.x += v.x;
			this.y += v.y;
			this.z += v.z;
			this.w += v.w;

			return this;

		}

		addScalar( s ) {

			this.x += s;
			this.y += s;
			this.z += s;
			this.w += s;

			return this;

		}

		addVectors( a, b ) {

			this.x = a.x + b.x;
			this.y = a.y + b.y;
			this.z = a.z + b.z;
			this.w = a.w + b.w;

			return this;

		}

		addScaledVector( v, s ) {

			this.x += v.x * s;
			this.y += v.y * s;
			this.z += v.z * s;
			this.w += v.w * s;

			return this;

		}

		sub( v, w ) {

			if ( w !== undefined ) {

				console.warn( 'THREE.Vector4: .sub() now only accepts one argument. Use .subVectors( a, b ) instead.' );
				return this.subVectors( v, w );

			}

			this.x -= v.x;
			this.y -= v.y;
			this.z -= v.z;
			this.w -= v.w;

			return this;

		}

		subScalar( s ) {

			this.x -= s;
			this.y -= s;
			this.z -= s;
			this.w -= s;

			return this;

		}

		subVectors( a, b ) {

			this.x = a.x - b.x;
			this.y = a.y - b.y;
			this.z = a.z - b.z;
			this.w = a.w - b.w;

			return this;

		}

		multiplyScalar( scalar ) {

			this.x *= scalar;
			this.y *= scalar;
			this.z *= scalar;
			this.w *= scalar;

			return this;

		}

		applyMatrix4( m ) {

			const x = this.x, y = this.y, z = this.z, w = this.w;
			const e = m.elements;

			this.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] * w;
			this.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] * w;
			this.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] * w;
			this.w = e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] * w;

			return this;

		}

		divideScalar( scalar ) {

			return this.multiplyScalar( 1 / scalar );

		}

		setAxisAngleFromQuaternion( q ) {

			// http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToAngle/index.htm

			// q is assumed to be normalized

			this.w = 2 * Math.acos( q.w );

			const s = Math.sqrt( 1 - q.w * q.w );

			if ( s < 0.0001 ) {

				this.x = 1;
				this.y = 0;
				this.z = 0;

			} else {

				this.x = q.x / s;
				this.y = q.y / s;
				this.z = q.z / s;

			}

			return this;

		}

		setAxisAngleFromRotationMatrix( m ) {

			// http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToAngle/index.htm

			// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)

			let angle, x, y, z; // variables for result
			const epsilon = 0.01,		// margin to allow for rounding errors
				epsilon2 = 0.1,		// margin to distinguish between 0 and 180 degrees

				te = m.elements,

				m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ],
				m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ],
				m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ];

			if ( ( Math.abs( m12 - m21 ) < epsilon ) &&
			     ( Math.abs( m13 - m31 ) < epsilon ) &&
			     ( Math.abs( m23 - m32 ) < epsilon ) ) {

				// singularity found
				// first check for identity matrix which must have +1 for all terms
				// in leading diagonal and zero in other terms

				if ( ( Math.abs( m12 + m21 ) < epsilon2 ) &&
				     ( Math.abs( m13 + m31 ) < epsilon2 ) &&
				     ( Math.abs( m23 + m32 ) < epsilon2 ) &&
				     ( Math.abs( m11 + m22 + m33 - 3 ) < epsilon2 ) ) {

					// this singularity is identity matrix so angle = 0

					this.set( 1, 0, 0, 0 );

					return this; // zero angle, arbitrary axis

				}

				// otherwise this singularity is angle = 180

				angle = Math.PI;

				const xx = ( m11 + 1 ) / 2;
				const yy = ( m22 + 1 ) / 2;
				const zz = ( m33 + 1 ) / 2;
				const xy = ( m12 + m21 ) / 4;
				const xz = ( m13 + m31 ) / 4;
				const yz = ( m23 + m32 ) / 4;

				if ( ( xx > yy ) && ( xx > zz ) ) {

					// m11 is the largest diagonal term

					if ( xx < epsilon ) {

						x = 0;
						y = 0.707106781;
						z = 0.707106781;

					} else {

						x = Math.sqrt( xx );
						y = xy / x;
						z = xz / x;

					}

				} else if ( yy > zz ) {

					// m22 is the largest diagonal term

					if ( yy < epsilon ) {

						x = 0.707106781;
						y = 0;
						z = 0.707106781;

					} else {

						y = Math.sqrt( yy );
						x = xy / y;
						z = yz / y;

					}

				} else {

					// m33 is the largest diagonal term so base result on this

					if ( zz < epsilon ) {

						x = 0.707106781;
						y = 0.707106781;
						z = 0;

					} else {

						z = Math.sqrt( zz );
						x = xz / z;
						y = yz / z;

					}

				}

				this.set( x, y, z, angle );

				return this; // return 180 deg rotation

			}

			// as we have reached here there are no singularities so we can handle normally

			let s = Math.sqrt( ( m32 - m23 ) * ( m32 - m23 ) +
				( m13 - m31 ) * ( m13 - m31 ) +
				( m21 - m12 ) * ( m21 - m12 ) ); // used to normalize

			if ( Math.abs( s ) < 0.001 ) s = 1;

			// prevent divide by zero, should not happen if matrix is orthogonal and should be
			// caught by singularity test above, but I've left it in just in case

			this.x = ( m32 - m23 ) / s;
			this.y = ( m13 - m31 ) / s;
			this.z = ( m21 - m12 ) / s;
			this.w = Math.acos( ( m11 + m22 + m33 - 1 ) / 2 );

			return this;

		}

		min( v ) {

			this.x = Math.min( this.x, v.x );
			this.y = Math.min( this.y, v.y );
			this.z = Math.min( this.z, v.z );
			this.w = Math.min( this.w, v.w );

			return this;

		}

		max( v ) {

			this.x = Math.max( this.x, v.x );
			this.y = Math.max( this.y, v.y );
			this.z = Math.max( this.z, v.z );
			this.w = Math.max( this.w, v.w );

			return this;

		}

		clamp( min, max ) {

			// assumes min < max, componentwise

			this.x = Math.max( min.x, Math.min( max.x, this.x ) );
			this.y = Math.max( min.y, Math.min( max.y, this.y ) );
			this.z = Math.max( min.z, Math.min( max.z, this.z ) );
			this.w = Math.max( min.w, Math.min( max.w, this.w ) );

			return this;

		}

		clampScalar( minVal, maxVal ) {

			this.x = Math.max( minVal, Math.min( maxVal, this.x ) );
			this.y = Math.max( minVal, Math.min( maxVal, this.y ) );
			this.z = Math.max( minVal, Math.min( maxVal, this.z ) );
			this.w = Math.max( minVal, Math.min( maxVal, this.w ) );

			return this;

		}

		clampLength( min, max ) {

			const length = this.length();

			return this.divideScalar( length || 1 ).multiplyScalar( Math.max( min, Math.min( max, length ) ) );

		}

		floor() {

			this.x = Math.floor( this.x );
			this.y = Math.floor( this.y );
			this.z = Math.floor( this.z );
			this.w = Math.floor( this.w );

			return this;

		}

		ceil() {

			this.x = Math.ceil( this.x );
			this.y = Math.ceil( this.y );
			this.z = Math.ceil( this.z );
			this.w = Math.ceil( this.w );

			return this;

		}

		round() {

			this.x = Math.round( this.x );
			this.y = Math.round( this.y );
			this.z = Math.round( this.z );
			this.w = Math.round( this.w );

			return this;

		}

		roundToZero() {

			this.x = ( this.x < 0 ) ? Math.ceil( this.x ) : Math.floor( this.x );
			this.y = ( this.y < 0 ) ? Math.ceil( this.y ) : Math.floor( this.y );
			this.z = ( this.z < 0 ) ? Math.ceil( this.z ) : Math.floor( this.z );
			this.w = ( this.w < 0 ) ? Math.ceil( this.w ) : Math.floor( this.w );

			return this;

		}

		negate() {

			this.x = - this.x;
			this.y = - this.y;
			this.z = - this.z;
			this.w = - this.w;

			return this;

		}

		dot( v ) {

			return this.x * v.x + this.y * v.y + this.z * v.z + this.w * v.w;

		}

		lengthSq() {

			return this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w;

		}

		length() {

			return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w );

		}

		manhattanLength() {

			return Math.abs( this.x ) + Math.abs( this.y ) + Math.abs( this.z ) + Math.abs( this.w );

		}

		normalize() {

			return this.divideScalar( this.length() || 1 );

		}

		setLength( length ) {

			return this.normalize().multiplyScalar( length );

		}

		lerp( v, alpha ) {

			this.x += ( v.x - this.x ) * alpha;
			this.y += ( v.y - this.y ) * alpha;
			this.z += ( v.z - this.z ) * alpha;
			this.w += ( v.w - this.w ) * alpha;

			return this;

		}

		lerpVectors( v1, v2, alpha ) {

			this.x = v1.x + ( v2.x - v1.x ) * alpha;
			this.y = v1.y + ( v2.y - v1.y ) * alpha;
			this.z = v1.z + ( v2.z - v1.z ) * alpha;
			this.w = v1.w + ( v2.w - v1.w ) * alpha;

			return this;

		}

		equals( v ) {

			return ( ( v.x === this.x ) && ( v.y === this.y ) && ( v.z === this.z ) && ( v.w === this.w ) );

		}

		fromArray( array, offset = 0 ) {

			this.x = array[ offset ];
			this.y = array[ offset + 1 ];
			this.z = array[ offset + 2 ];
			this.w = array[ offset + 3 ];

			return this;

		}

		toArray( array = [], offset = 0 ) {

			array[ offset ] = this.x;
			array[ offset + 1 ] = this.y;
			array[ offset + 2 ] = this.z;
			array[ offset + 3 ] = this.w;

			return array;

		}

		fromBufferAttribute( attribute, index, offset ) {

			if ( offset !== undefined ) {

				console.warn( 'THREE.Vector4: offset has been removed from .fromBufferAttribute().' );

			}

			this.x = attribute.getX( index );
			this.y = attribute.getY( index );
			this.z = attribute.getZ( index );
			this.w = attribute.getW( index );

			return this;

		}

		random() {

			this.x = Math.random();
			this.y = Math.random();
			this.z = Math.random();
			this.w = Math.random();

			return this;

		}

	}

	/*
	 In options, we can specify:
	 * Texture parameters for an auto-generated target texture
	 * depthBuffer/stencilBuffer: Booleans to indicate if we should generate these buffers
	*/
	function WebGLRenderTarget( width, height, options ) {

		this.width = width;
		this.height = height;

		this.scissor = new Vector4( 0, 0, width, height );
		this.scissorTest = false;

		this.viewport = new Vector4( 0, 0, width, height );

		options = options || {};

		this.texture = new Texture( undefined, options.mapping, options.wrapS, options.wrapT, options.magFilter, options.minFilter, options.format, options.type, options.anisotropy, options.encoding );

		this.texture.image = {};
		this.texture.image.width = width;
		this.texture.image.height = height;

		this.texture.generateMipmaps = options.generateMipmaps !== undefined ? options.generateMipmaps : false;
		this.texture.minFilter = options.minFilter !== undefined ? options.minFilter : LinearFilter;

		this.depthBuffer = options.depthBuffer !== undefined ? options.depthBuffer : true;
		this.stencilBuffer = options.stencilBuffer !== undefined ? options.stencilBuffer : false;
		this.depthTexture = options.depthTexture !== undefined ? options.depthTexture : null;

	}

	WebGLRenderTarget.prototype = Object.assign( Object.create( EventDispatcher$1.prototype ), {

		constructor: WebGLRenderTarget,

		isWebGLRenderTarget: true,

		setSize: function ( width, height ) {

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

				this.width = width;
				this.height = height;

				this.texture.image.width = width;
				this.texture.image.height = height;

				this.dispose();

			}

			this.viewport.set( 0, 0, width, height );
			this.scissor.set( 0, 0, width, height );

		},

		clone: function () {

			return new this.constructor().copy( this );

		},

		copy: function ( source ) {

			this.width = source.width;
			this.height = source.height;

			this.viewport.copy( source.viewport );

			this.texture = source.texture.clone();

			this.depthBuffer = source.depthBuffer;
			this.stencilBuffer = source.stencilBuffer;
			this.depthTexture = source.depthTexture;

			return this;

		},

		dispose: function () {

			this.dispatchEvent( { type: 'dispose' } );

		}

	} );

	function WebGLMultisampleRenderTarget( width, height, options ) {

		WebGLRenderTarget.call( this, width, height, options );

		this.samples = 4;

	}

	WebGLMultisampleRenderTarget.prototype = Object.assign( Object.create( WebGLRenderTarget.prototype ), {

		constructor: WebGLMultisampleRenderTarget,

		isWebGLMultisampleRenderTarget: true,

		copy: function ( source ) {

			WebGLRenderTarget.prototype.copy.call( this, source );

			this.samples = source.samples;

			return this;

		}

	} );

	class Quaternion {

		constructor( x = 0, y = 0, z = 0, w = 1 ) {

			Object.defineProperty( this, 'isQuaternion', { value: true } );

			this._x = x;
			this._y = y;
			this._z = z;
			this._w = w;

		}

		static slerp( qa, qb, qm, t ) {

			return qm.copy( qa ).slerp( qb, t );

		}

		static slerpFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1, t ) {

			// fuzz-free, array-based Quaternion SLERP operation

			let x0 = src0[ srcOffset0 + 0 ],
				y0 = src0[ srcOffset0 + 1 ],
				z0 = src0[ srcOffset0 + 2 ],
				w0 = src0[ srcOffset0 + 3 ];

			const x1 = src1[ srcOffset1 + 0 ],
				y1 = src1[ srcOffset1 + 1 ],
				z1 = src1[ srcOffset1 + 2 ],
				w1 = src1[ srcOffset1 + 3 ];

			if ( w0 !== w1 || x0 !== x1 || y0 !== y1 || z0 !== z1 ) {

				let s = 1 - t;
				const cos = x0 * x1 + y0 * y1 + z0 * z1 + w0 * w1,
					dir = ( cos >= 0 ? 1 : - 1 ),
					sqrSin = 1 - cos * cos;

				// Skip the Slerp for tiny steps to avoid numeric problems:
				if ( sqrSin > Number.EPSILON ) {

					const sin = Math.sqrt( sqrSin ),
						len = Math.atan2( sin, cos * dir );

					s = Math.sin( s * len ) / sin;
					t = Math.sin( t * len ) / sin;

				}

				const tDir = t * dir;

				x0 = x0 * s + x1 * tDir;
				y0 = y0 * s + y1 * tDir;
				z0 = z0 * s + z1 * tDir;
				w0 = w0 * s + w1 * tDir;

				// Normalize in case we just did a lerp:
				if ( s === 1 - t ) {

					const f = 1 / Math.sqrt( x0 * x0 + y0 * y0 + z0 * z0 + w0 * w0 );

					x0 *= f;
					y0 *= f;
					z0 *= f;
					w0 *= f;

				}

			}

			dst[ dstOffset ] = x0;
			dst[ dstOffset + 1 ] = y0;
			dst[ dstOffset + 2 ] = z0;
			dst[ dstOffset + 3 ] = w0;

		}

		static multiplyQuaternionsFlat( dst, dstOffset, src0, srcOffset0, src1, srcOffset1 ) {

			const x0 = src0[ srcOffset0 ];
			const y0 = src0[ srcOffset0 + 1 ];
			const z0 = src0[ srcOffset0 + 2 ];
			const w0 = src0[ srcOffset0 + 3 ];

			const x1 = src1[ srcOffset1 ];
			const y1 = src1[ srcOffset1 + 1 ];
			const z1 = src1[ srcOffset1 + 2 ];
			const w1 = src1[ srcOffset1 + 3 ];

			dst[ dstOffset ] = x0 * w1 + w0 * x1 + y0 * z1 - z0 * y1;
			dst[ dstOffset + 1 ] = y0 * w1 + w0 * y1 + z0 * x1 - x0 * z1;
			dst[ dstOffset + 2 ] = z0 * w1 + w0 * z1 + x0 * y1 - y0 * x1;
			dst[ dstOffset + 3 ] = w0 * w1 - x0 * x1 - y0 * y1 - z0 * z1;

			return dst;

		}

		get x() {

			return this._x;

		}

		set x( value ) {

			this._x = value;
			this._onChangeCallback();

		}

		get y() {

			return this._y;

		}

		set y( value ) {

			this._y = value;
			this._onChangeCallback();

		}

		get z() {

			return this._z;

		}

		set z( value ) {

			this._z = value;
			this._onChangeCallback();

		}

		get w() {

			return this._w;

		}

		set w( value ) {

			this._w = value;
			this._onChangeCallback();

		}

		set( x, y, z, w ) {

			this._x = x;
			this._y = y;
			this._z = z;
			this._w = w;

			this._onChangeCallback();

			return this;

		}

		clone() {

			return new this.constructor( this._x, this._y, this._z, this._w );

		}

		copy( quaternion ) {

			this._x = quaternion.x;
			this._y = quaternion.y;
			this._z = quaternion.z;
			this._w = quaternion.w;

			this._onChangeCallback();

			return this;

		}

		setFromEuler( euler, update ) {

			if ( ! ( euler && euler.isEuler ) ) {

				throw new Error( 'THREE.Quaternion: .setFromEuler() now expects an Euler rotation rather than a Vector3 and order.' );

			}

			const x = euler._x, y = euler._y, z = euler._z, order = euler._order;

			// http://www.mathworks.com/matlabcentral/fileexchange/
			// 	20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/
			//	content/SpinCalc.m

			const cos = Math.cos;
			const sin = Math.sin;

			const c1 = cos( x / 2 );
			const c2 = cos( y / 2 );
			const c3 = cos( z / 2 );

			const s1 = sin( x / 2 );
			const s2 = sin( y / 2 );
			const s3 = sin( z / 2 );

			switch ( order ) {

				case 'XYZ':
					this._x = s1 * c2 * c3 + c1 * s2 * s3;
					this._y = c1 * s2 * c3 - s1 * c2 * s3;
					this._z = c1 * c2 * s3 + s1 * s2 * c3;
					this._w = c1 * c2 * c3 - s1 * s2 * s3;
					break;

				case 'YXZ':
					this._x = s1 * c2 * c3 + c1 * s2 * s3;
					this._y = c1 * s2 * c3 - s1 * c2 * s3;
					this._z = c1 * c2 * s3 - s1 * s2 * c3;
					this._w = c1 * c2 * c3 + s1 * s2 * s3;
					break;

				case 'ZXY':
					this._x = s1 * c2 * c3 - c1 * s2 * s3;
					this._y = c1 * s2 * c3 + s1 * c2 * s3;
					this._z = c1 * c2 * s3 + s1 * s2 * c3;
					this._w = c1 * c2 * c3 - s1 * s2 * s3;
					break;

				case 'ZYX':
					this._x = s1 * c2 * c3 - c1 * s2 * s3;
					this._y = c1 * s2 * c3 + s1 * c2 * s3;
					this._z = c1 * c2 * s3 - s1 * s2 * c3;
					this._w = c1 * c2 * c3 + s1 * s2 * s3;
					break;

				case 'YZX':
					this._x = s1 * c2 * c3 + c1 * s2 * s3;
					this._y = c1 * s2 * c3 + s1 * c2 * s3;
					this._z = c1 * c2 * s3 - s1 * s2 * c3;
					this._w = c1 * c2 * c3 - s1 * s2 * s3;
					break;

				case 'XZY':
					this._x = s1 * c2 * c3 - c1 * s2 * s3;
					this._y = c1 * s2 * c3 - s1 * c2 * s3;
					this._z = c1 * c2 * s3 + s1 * s2 * c3;
					this._w = c1 * c2 * c3 + s1 * s2 * s3;
					break;

				default:
					console.warn( 'THREE.Quaternion: .setFromEuler() encountered an unknown order: ' + order );

			}

			if ( update !== false ) this._onChangeCallback();

			return this;

		}

		setFromAxisAngle( axis, angle ) {

			// http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm

			// assumes axis is normalized

			const halfAngle = angle / 2, s = Math.sin( halfAngle );

			this._x = axis.x * s;
			this._y = axis.y * s;
			this._z = axis.z * s;
			this._w = Math.cos( halfAngle );

			this._onChangeCallback();

			return this;

		}

		setFromRotationMatrix( m ) {

			// http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/index.htm

			// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)

			const te = m.elements,

				m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ],
				m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ],
				m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ],

				trace = m11 + m22 + m33;

			if ( trace > 0 ) {

				const s = 0.5 / Math.sqrt( trace + 1.0 );

				this._w = 0.25 / s;
				this._x = ( m32 - m23 ) * s;
				this._y = ( m13 - m31 ) * s;
				this._z = ( m21 - m12 ) * s;

			} else if ( m11 > m22 && m11 > m33 ) {

				const s = 2.0 * Math.sqrt( 1.0 + m11 - m22 - m33 );

				this._w = ( m32 - m23 ) / s;
				this._x = 0.25 * s;
				this._y = ( m12 + m21 ) / s;
				this._z = ( m13 + m31 ) / s;

			} else if ( m22 > m33 ) {

				const s = 2.0 * Math.sqrt( 1.0 + m22 - m11 - m33 );

				this._w = ( m13 - m31 ) / s;
				this._x = ( m12 + m21 ) / s;
				this._y = 0.25 * s;
				this._z = ( m23 + m32 ) / s;

			} else {

				const s = 2.0 * Math.sqrt( 1.0 + m33 - m11 - m22 );

				this._w = ( m21 - m12 ) / s;
				this._x = ( m13 + m31 ) / s;
				this._y = ( m23 + m32 ) / s;
				this._z = 0.25 * s;

			}

			this._onChangeCallback();

			return this;

		}

		setFromUnitVectors( vFrom, vTo ) {

			// assumes direction vectors vFrom and vTo are normalized

			const EPS = 0.000001;

			let r = vFrom.dot( vTo ) + 1;

			if ( r < EPS ) {

				r = 0;

				if ( Math.abs( vFrom.x ) > Math.abs( vFrom.z ) ) {

					this._x = - vFrom.y;
					this._y = vFrom.x;
					this._z = 0;
					this._w = r;

				} else {

					this._x = 0;
					this._y = - vFrom.z;
					this._z = vFrom.y;
					this._w = r;

				}

			} else {

				// crossVectors( vFrom, vTo ); // inlined to avoid cyclic dependency on Vector3

				this._x = vFrom.y * vTo.z - vFrom.z * vTo.y;
				this._y = vFrom.z * vTo.x - vFrom.x * vTo.z;
				this._z = vFrom.x * vTo.y - vFrom.y * vTo.x;
				this._w = r;

			}

			return this.normalize();

		}

		angleTo( q ) {

			return 2 * Math.acos( Math.abs( MathUtils.clamp( this.dot( q ), - 1, 1 ) ) );

		}

		rotateTowards( q, step ) {

			const angle = this.angleTo( q );

			if ( angle === 0 ) return this;

			const t = Math.min( 1, step / angle );

			this.slerp( q, t );

			return this;

		}

		identity() {

			return this.set( 0, 0, 0, 1 );

		}

		invert() {

			// quaternion is assumed to have unit length

			return this.conjugate();

		}

		conjugate() {

			this._x *= - 1;
			this._y *= - 1;
			this._z *= - 1;

			this._onChangeCallback();

			return this;

		}

		dot( v ) {

			return this._x * v._x + this._y * v._y + this._z * v._z + this._w * v._w;

		}

		lengthSq() {

			return this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w;

		}

		length() {

			return Math.sqrt( this._x * this._x + this._y * this._y + this._z * this._z + this._w * this._w );

		}

		normalize() {

			let l = this.length();

			if ( l === 0 ) {

				this._x = 0;
				this._y = 0;
				this._z = 0;
				this._w = 1;

			} else {

				l = 1 / l;

				this._x = this._x * l;
				this._y = this._y * l;
				this._z = this._z * l;
				this._w = this._w * l;

			}

			this._onChangeCallback();

			return this;

		}

		multiply( q, p ) {

			if ( p !== undefined ) {

				console.warn( 'THREE.Quaternion: .multiply() now only accepts one argument. Use .multiplyQuaternions( a, b ) instead.' );
				return this.multiplyQuaternions( q, p );

			}

			return this.multiplyQuaternions( this, q );

		}

		premultiply( q ) {

			return this.multiplyQuaternions( q, this );

		}

		multiplyQuaternions( a, b ) {

			// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm

			const qax = a._x, qay = a._y, qaz = a._z, qaw = a._w;
			const qbx = b._x, qby = b._y, qbz = b._z, qbw = b._w;

			this._x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
			this._y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
			this._z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
			this._w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;

			this._onChangeCallback();

			return this;

		}

		slerp( qb, t ) {

			if ( t === 0 ) return this;
			if ( t === 1 ) return this.copy( qb );

			const x = this._x, y = this._y, z = this._z, w = this._w;

			// http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/

			let cosHalfTheta = w * qb._w + x * qb._x + y * qb._y + z * qb._z;

			if ( cosHalfTheta < 0 ) {

				this._w = - qb._w;
				this._x = - qb._x;
				this._y = - qb._y;
				this._z = - qb._z;

				cosHalfTheta = - cosHalfTheta;

			} else {

				this.copy( qb );

			}

			if ( cosHalfTheta >= 1.0 ) {

				this._w = w;
				this._x = x;
				this._y = y;
				this._z = z;

				return this;

			}

			const sqrSinHalfTheta = 1.0 - cosHalfTheta * cosHalfTheta;

			if ( sqrSinHalfTheta <= Number.EPSILON ) {

				const s = 1 - t;
				this._w = s * w + t * this._w;
				this._x = s * x + t * this._x;
				this._y = s * y + t * this._y;
				this._z = s * z + t * this._z;

				this.normalize();
				this._onChangeCallback();

				return this;

			}

			const sinHalfTheta = Math.sqrt( sqrSinHalfTheta );
			const halfTheta = Math.atan2( sinHalfTheta, cosHalfTheta );
			const ratioA = Math.sin( ( 1 - t ) * halfTheta ) / sinHalfTheta,
				ratioB = Math.sin( t * halfTheta ) / sinHalfTheta;

			this._w = ( w * ratioA + this._w * ratioB );
			this._x = ( x * ratioA + this._x * ratioB );
			this._y = ( y * ratioA + this._y * ratioB );
			this._z = ( z * ratioA + this._z * ratioB );

			this._onChangeCallback();

			return this;

		}

		equals( quaternion ) {

			return ( quaternion._x === this._x ) && ( quaternion._y === this._y ) && ( quaternion._z === this._z ) && ( quaternion._w === this._w );

		}

		fromArray( array, offset = 0 ) {

			this._x = array[ offset ];
			this._y = array[ offset + 1 ];
			this._z = array[ offset + 2 ];
			this._w = array[ offset + 3 ];

			this._onChangeCallback();

			return this;

		}

		toArray( array = [], offset = 0 ) {

			array[ offset ] = this._x;
			array[ offset + 1 ] = this._y;
			array[ offset + 2 ] = this._z;
			array[ offset + 3 ] = this._w;

			return array;

		}

		fromBufferAttribute( attribute, index ) {

			this._x = attribute.getX( index );
			this._y = attribute.getY( index );
			this._z = attribute.getZ( index );
			this._w = attribute.getW( index );

			return this;

		}

		_onChange( callback ) {

			this._onChangeCallback = callback;

			return this;

		}

		_onChangeCallback() {}

	}

	class Vector3 {

		constructor( x = 0, y = 0, z = 0 ) {

			Object.defineProperty( this, 'isVector3', { value: true } );

			this.x = x;
			this.y = y;
			this.z = z;

		}

		set( x, y, z ) {

			if ( z === undefined ) z = this.z; // sprite.scale.set(x,y)

			this.x = x;
			this.y = y;
			this.z = z;

			return this;

		}

		setScalar( scalar ) {

			this.x = scalar;
			this.y = scalar;
			this.z = scalar;

			return this;

		}

		setX( x ) {

			this.x = x;

			return this;

		}

		setY( y ) {

			this.y = y;

			return this;

		}

		setZ( z ) {

			this.z = z;

			return this;

		}

		setComponent( index, value ) {

			switch ( index ) {

				case 0: this.x = value; break;
				case 1: this.y = value; break;
				case 2: this.z = value; break;
				default: throw new Error( 'index is out of range: ' + index );

			}

			return this;

		}

		getComponent( index ) {

			switch ( index ) {

				case 0: return this.x;
				case 1: return this.y;
				case 2: return this.z;
				default: throw new Error( 'index is out of range: ' + index );

			}

		}

		clone() {

			return new this.constructor( this.x, this.y, this.z );

		}

		copy( v ) {

			this.x = v.x;
			this.y = v.y;
			this.z = v.z;

			return this;

		}

		add( v, w ) {

			if ( w !== undefined ) {

				console.warn( 'THREE.Vector3: .add() now only accepts one argument. Use .addVectors( a, b ) instead.' );
				return this.addVectors( v, w );

			}

			this.x += v.x;
			this.y += v.y;
			this.z += v.z;

			return this;

		}

		addScalar( s ) {

			this.x += s;
			this.y += s;
			this.z += s;

			return this;

		}

		addVectors( a, b ) {

			this.x = a.x + b.x;
			this.y = a.y + b.y;
			this.z = a.z + b.z;

			return this;

		}

		addScaledVector( v, s ) {

			this.x += v.x * s;
			this.y += v.y * s;
			this.z += v.z * s;

			return this;

		}

		sub( v, w ) {

			if ( w !== undefined ) {

				console.warn( 'THREE.Vector3: .sub() now only accepts one argument. Use .subVectors( a, b ) instead.' );
				return this.subVectors( v, w );

			}

			this.x -= v.x;
			this.y -= v.y;
			this.z -= v.z;

			return this;

		}

		subScalar( s ) {

			this.x -= s;
			this.y -= s;
			this.z -= s;

			return this;

		}

		subVectors( a, b ) {

			this.x = a.x - b.x;
			this.y = a.y - b.y;
			this.z = a.z - b.z;

			return this;

		}

		multiply( v, w ) {

			if ( w !== undefined ) {

				console.warn( 'THREE.Vector3: .multiply() now only accepts one argument. Use .multiplyVectors( a, b ) instead.' );
				return this.multiplyVectors( v, w );

			}

			this.x *= v.x;
			this.y *= v.y;
			this.z *= v.z;

			return this;

		}

		multiplyScalar( scalar ) {

			this.x *= scalar;
			this.y *= scalar;
			this.z *= scalar;

			return this;

		}

		multiplyVectors( a, b ) {

			this.x = a.x * b.x;
			this.y = a.y * b.y;
			this.z = a.z * b.z;

			return this;

		}

		applyEuler( euler ) {

			if ( ! ( euler && euler.isEuler ) ) {

				console.error( 'THREE.Vector3: .applyEuler() now expects an Euler rotation rather than a Vector3 and order.' );

			}

			return this.applyQuaternion( _quaternion.setFromEuler( euler ) );

		}

		applyAxisAngle( axis, angle ) {

			return this.applyQuaternion( _quaternion.setFromAxisAngle( axis, angle ) );

		}

		applyMatrix3( m ) {

			const x = this.x, y = this.y, z = this.z;
			const e = m.elements;

			this.x = e[ 0 ] * x + e[ 3 ] * y + e[ 6 ] * z;
			this.y = e[ 1 ] * x + e[ 4 ] * y + e[ 7 ] * z;
			this.z = e[ 2 ] * x + e[ 5 ] * y + e[ 8 ] * z;

			return this;

		}

		applyNormalMatrix( m ) {

			return this.applyMatrix3( m ).normalize();

		}

		applyMatrix4( m ) {

			const x = this.x, y = this.y, z = this.z;
			const e = m.elements;

			const w = 1 / ( e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] );

			this.x = ( e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] ) * w;
			this.y = ( e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] ) * w;
			this.z = ( e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] ) * w;

			return this;

		}

		applyQuaternion( q ) {

			const x = this.x, y = this.y, z = this.z;
			const qx = q.x, qy = q.y, qz = q.z, qw = q.w;

			// calculate quat * vector

			const ix = qw * x + qy * z - qz * y;
			const iy = qw * y + qz * x - qx * z;
			const iz = qw * z + qx * y - qy * x;
			const iw = - qx * x - qy * y - qz * z;

			// calculate result * inverse quat

			this.x = ix * qw + iw * - qx + iy * - qz - iz * - qy;
			this.y = iy * qw + iw * - qy + iz * - qx - ix * - qz;
			this.z = iz * qw + iw * - qz + ix * - qy - iy * - qx;

			return this;

		}

		project( camera ) {

			return this.applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix );

		}

		unproject( camera ) {

			return this.applyMatrix4( camera.projectionMatrixInverse ).applyMatrix4( camera.matrixWorld );

		}

		transformDirection( m ) {

			// input: THREE.Matrix4 affine matrix
			// vector interpreted as a direction

			const x = this.x, y = this.y, z = this.z;
			const e = m.elements;

			this.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z;
			this.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z;
			this.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z;

			return this.normalize();

		}

		divide( v ) {

			this.x /= v.x;
			this.y /= v.y;
			this.z /= v.z;

			return this;

		}

		divideScalar( scalar ) {

			return this.multiplyScalar( 1 / scalar );

		}

		min( v ) {

			this.x = Math.min( this.x, v.x );
			this.y = Math.min( this.y, v.y );
			this.z = Math.min( this.z, v.z );

			return this;

		}

		max( v ) {

			this.x = Math.max( this.x, v.x );
			this.y = Math.max( this.y, v.y );
			this.z = Math.max( this.z, v.z );

			return this;

		}

		clamp( min, max ) {

			// assumes min < max, componentwise

			this.x = Math.max( min.x, Math.min( max.x, this.x ) );
			this.y = Math.max( min.y, Math.min( max.y, this.y ) );
			this.z = Math.max( min.z, Math.min( max.z, this.z ) );

			return this;

		}

		clampScalar( minVal, maxVal ) {

			this.x = Math.max( minVal, Math.min( maxVal, this.x ) );
			this.y = Math.max( minVal, Math.min( maxVal, this.y ) );
			this.z = Math.max( minVal, Math.min( maxVal, this.z ) );

			return this;

		}

		clampLength( min, max ) {

			const length = this.length();

			return this.divideScalar( length || 1 ).multiplyScalar( Math.max( min, Math.min( max, length ) ) );

		}

		floor() {

			this.x = Math.floor( this.x );
			this.y = Math.floor( this.y );
			this.z = Math.floor( this.z );

			return this;

		}

		ceil() {

			this.x = Math.ceil( this.x );
			this.y = Math.ceil( this.y );
			this.z = Math.ceil( this.z );

			return this;

		}

		round() {

			this.x = Math.round( this.x );
			this.y = Math.round( this.y );
			this.z = Math.round( this.z );

			return this;

		}

		roundToZero() {

			this.x = ( this.x < 0 ) ? Math.ceil( this.x ) : Math.floor( this.x );
			this.y = ( this.y < 0 ) ? Math.ceil( this.y ) : Math.floor( this.y );
			this.z = ( this.z < 0 ) ? Math.ceil( this.z ) : Math.floor( this.z );

			return this;

		}

		negate() {

			this.x = - this.x;
			this.y = - this.y;
			this.z = - this.z;

			return this;

		}

		dot( v ) {

			return this.x * v.x + this.y * v.y + this.z * v.z;

		}

		// TODO lengthSquared?

		lengthSq() {

			return this.x * this.x + this.y * this.y + this.z * this.z;

		}

		length() {

			return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );

		}

		manhattanLength() {

			return Math.abs( this.x ) + Math.abs( this.y ) + Math.abs( this.z );

		}

		normalize() {

			return this.divideScalar( this.length() || 1 );

		}

		setLength( length ) {

			return this.normalize().multiplyScalar( length );

		}

		lerp( v, alpha ) {

			this.x += ( v.x - this.x ) * alpha;
			this.y += ( v.y - this.y ) * alpha;
			this.z += ( v.z - this.z ) * alpha;

			return this;

		}

		lerpVectors( v1, v2, alpha ) {

			this.x = v1.x + ( v2.x - v1.x ) * alpha;
			this.y = v1.y + ( v2.y - v1.y ) * alpha;
			this.z = v1.z + ( v2.z - v1.z ) * alpha;

			return this;

		}

		cross( v, w ) {

			if ( w !== undefined ) {

				console.warn( 'THREE.Vector3: .cross() now only accepts one argument. Use .crossVectors( a, b ) instead.' );
				return this.crossVectors( v, w );

			}

			return this.crossVectors( this, v );

		}

		crossVectors( a, b ) {

			const ax = a.x, ay = a.y, az = a.z;
			const bx = b.x, by = b.y, bz = b.z;

			this.x = ay * bz - az * by;
			this.y = az * bx - ax * bz;
			this.z = ax * by - ay * bx;

			return this;

		}

		projectOnVector( v ) {

			const denominator = v.lengthSq();

			if ( denominator === 0 ) return this.set( 0, 0, 0 );

			const scalar = v.dot( this ) / denominator;

			return this.copy( v ).multiplyScalar( scalar );

		}

		projectOnPlane( planeNormal ) {

			_vector.copy( this ).projectOnVector( planeNormal );

			return this.sub( _vector );

		}

		reflect( normal ) {

			// reflect incident vector off plane orthogonal to normal
			// normal is assumed to have unit length

			return this.sub( _vector.copy( normal ).multiplyScalar( 2 * this.dot( normal ) ) );

		}

		angleTo( v ) {

			const denominator = Math.sqrt( this.lengthSq() * v.lengthSq() );

			if ( denominator === 0 ) return Math.PI / 2;

			const theta = this.dot( v ) / denominator;

			// clamp, to handle numerical problems

			return Math.acos( MathUtils.clamp( theta, - 1, 1 ) );

		}

		distanceTo( v ) {

			return Math.sqrt( this.distanceToSquared( v ) );

		}

		distanceToSquared( v ) {

			const dx = this.x - v.x, dy = this.y - v.y, dz = this.z - v.z;

			return dx * dx + dy * dy + dz * dz;

		}

		manhattanDistanceTo( v ) {

			return Math.abs( this.x - v.x ) + Math.abs( this.y - v.y ) + Math.abs( this.z - v.z );

		}

		setFromSpherical( s ) {

			return this.setFromSphericalCoords( s.radius, s.phi, s.theta );

		}

		setFromSphericalCoords( radius, phi, theta ) {

			const sinPhiRadius = Math.sin( phi ) * radius;

			this.x = sinPhiRadius * Math.sin( theta );
			this.y = Math.cos( phi ) * radius;
			this.z = sinPhiRadius * Math.cos( theta );

			return this;

		}

		setFromCylindrical( c ) {

			return this.setFromCylindricalCoords( c.radius, c.theta, c.y );

		}

		setFromCylindricalCoords( radius, theta, y ) {

			this.x = radius * Math.sin( theta );
			this.y = y;
			this.z = radius * Math.cos( theta );

			return this;

		}

		setFromMatrixPosition( m ) {

			const e = m.elements;

			this.x = e[ 12 ];
			this.y = e[ 13 ];
			this.z = e[ 14 ];

			return this;

		}

		setFromMatrixScale( m ) {

			const sx = this.setFromMatrixColumn( m, 0 ).length();
			const sy = this.setFromMatrixColumn( m, 1 ).length();
			const sz = this.setFromMatrixColumn( m, 2 ).length();

			this.x = sx;
			this.y = sy;
			this.z = sz;

			return this;

		}

		setFromMatrixColumn( m, index ) {

			return this.fromArray( m.elements, index * 4 );

		}

		setFromMatrix3Column( m, index ) {

			return this.fromArray( m.elements, index * 3 );

		}

		equals( v ) {

			return ( ( v.x === this.x ) && ( v.y === this.y ) && ( v.z === this.z ) );

		}

		fromArray( array, offset = 0 ) {

			this.x = array[ offset ];
			this.y = array[ offset + 1 ];
			this.z = array[ offset + 2 ];

			return this;

		}

		toArray( array = [], offset = 0 ) {

			array[ offset ] = this.x;
			array[ offset + 1 ] = this.y;
			array[ offset + 2 ] = this.z;

			return array;

		}

		fromBufferAttribute( attribute, index, offset ) {

			if ( offset !== undefined ) {

				console.warn( 'THREE.Vector3: offset has been removed from .fromBufferAttribute().' );

			}

			this.x = attribute.getX( index );
			this.y = attribute.getY( index );
			this.z = attribute.getZ( index );

			return this;

		}

		random() {

			this.x = Math.random();
			this.y = Math.random();
			this.z = Math.random();

			return this;

		}

	}

	const _vector = /*@__PURE__*/ new Vector3();
	const _quaternion = /*@__PURE__*/ new Quaternion();

	class Box3 {

		constructor( min, max ) {

			Object.defineProperty( this, 'isBox3', { value: true } );

			this.min = ( min !== undefined ) ? min : new Vector3( + Infinity, + Infinity, + Infinity );
			this.max = ( max !== undefined ) ? max : new Vector3( - Infinity, - Infinity, - Infinity );

		}

		set( min, max ) {

			this.min.copy( min );
			this.max.copy( max );

			return this;

		}

		setFromArray( array ) {

			let minX = + Infinity;
			let minY = + Infinity;
			let minZ = + Infinity;

			let maxX = - Infinity;
			let maxY = - Infinity;
			let maxZ = - Infinity;

			for ( let i = 0, l = array.length; i < l; i += 3 ) {

				const x = array[ i ];
				const y = array[ i + 1 ];
				const z = array[ i + 2 ];

				if ( x < minX ) minX = x;
				if ( y < minY ) minY = y;
				if ( z < minZ ) minZ = z;

				if ( x > maxX ) maxX = x;
				if ( y > maxY ) maxY = y;
				if ( z > maxZ ) maxZ = z;

			}

			this.min.set( minX, minY, minZ );
			this.max.set( maxX, maxY, maxZ );

			return this;

		}

		setFromBufferAttribute( attribute ) {

			let minX = + Infinity;
			let minY = + Infinity;
			let minZ = + Infinity;

			let maxX = - Infinity;
			let maxY = - Infinity;
			let maxZ = - Infinity;

			for ( let i = 0, l = attribute.count; i < l; i ++ ) {

				const x = attribute.getX( i );
				const y = attribute.getY( i );
				const z = attribute.getZ( i );

				if ( x < minX ) minX = x;
				if ( y < minY ) minY = y;
				if ( z < minZ ) minZ = z;

				if ( x > maxX ) maxX = x;
				if ( y > maxY ) maxY = y;
				if ( z > maxZ ) maxZ = z;

			}

			this.min.set( minX, minY, minZ );
			this.max.set( maxX, maxY, maxZ );

			return this;

		}

		setFromPoints( points ) {

			this.makeEmpty();

			for ( let i = 0, il = points.length; i < il; i ++ ) {

				this.expandByPoint( points[ i ] );

			}

			return this;

		}

		setFromCenterAndSize( center, size ) {

			const halfSize = _vector$1.copy( size ).multiplyScalar( 0.5 );

			this.min.copy( center ).sub( halfSize );
			this.max.copy( center ).add( halfSize );

			return this;

		}

		setFromObject( object ) {

			this.makeEmpty();

			return this.expandByObject( object );

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( box ) {

			this.min.copy( box.min );
			this.max.copy( box.max );

			return this;

		}

		makeEmpty() {

			this.min.x = this.min.y = this.min.z = + Infinity;
			this.max.x = this.max.y = this.max.z = - Infinity;

			return this;

		}

		isEmpty() {

			// this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes

			return ( this.max.x < this.min.x ) || ( this.max.y < this.min.y ) || ( this.max.z < this.min.z );

		}

		getCenter( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Box3: .getCenter() target is now required' );
				target = new Vector3();

			}

			return this.isEmpty() ? target.set( 0, 0, 0 ) : target.addVectors( this.min, this.max ).multiplyScalar( 0.5 );

		}

		getSize( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Box3: .getSize() target is now required' );
				target = new Vector3();

			}

			return this.isEmpty() ? target.set( 0, 0, 0 ) : target.subVectors( this.max, this.min );

		}

		expandByPoint( point ) {

			this.min.min( point );
			this.max.max( point );

			return this;

		}

		expandByVector( vector ) {

			this.min.sub( vector );
			this.max.add( vector );

			return this;

		}

		expandByScalar( scalar ) {

			this.min.addScalar( - scalar );
			this.max.addScalar( scalar );

			return this;

		}

		expandByObject( object ) {

			// Computes the world-axis-aligned bounding box of an object (including its children),
			// accounting for both the object's, and children's, world transforms

			object.updateWorldMatrix( false, false );

			const geometry = object.geometry;

			if ( geometry !== undefined ) {

				if ( geometry.boundingBox === null ) {

					geometry.computeBoundingBox();

				}

				_box.copy( geometry.boundingBox );
				_box.applyMatrix4( object.matrixWorld );

				this.union( _box );

			}

			const children = object.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				this.expandByObject( children[ i ] );

			}

			return this;

		}

		containsPoint( point ) {

			return point.x < this.min.x || point.x > this.max.x ||
				point.y < this.min.y || point.y > this.max.y ||
				point.z < this.min.z || point.z > this.max.z ? false : true;

		}

		containsBox( box ) {

			return this.min.x <= box.min.x && box.max.x <= this.max.x &&
				this.min.y <= box.min.y && box.max.y <= this.max.y &&
				this.min.z <= box.min.z && box.max.z <= this.max.z;

		}

		getParameter( point, target ) {

			// This can potentially have a divide by zero if the box
			// has a size dimension of 0.

			if ( target === undefined ) {

				console.warn( 'THREE.Box3: .getParameter() target is now required' );
				target = new Vector3();

			}

			return target.set(
				( point.x - this.min.x ) / ( this.max.x - this.min.x ),
				( point.y - this.min.y ) / ( this.max.y - this.min.y ),
				( point.z - this.min.z ) / ( this.max.z - this.min.z )
			);

		}

		intersectsBox( box ) {

			// using 6 splitting planes to rule out intersections.
			return box.max.x < this.min.x || box.min.x > this.max.x ||
				box.max.y < this.min.y || box.min.y > this.max.y ||
				box.max.z < this.min.z || box.min.z > this.max.z ? false : true;

		}

		intersectsSphere( sphere ) {

			// Find the point on the AABB closest to the sphere center.
			this.clampPoint( sphere.center, _vector$1 );

			// If that point is inside the sphere, the AABB and sphere intersect.
			return _vector$1.distanceToSquared( sphere.center ) <= ( sphere.radius * sphere.radius );

		}

		intersectsPlane( plane ) {

			// We compute the minimum and maximum dot product values. If those values
			// are on the same side (back or front) of the plane, then there is no intersection.

			let min, max;

			if ( plane.normal.x > 0 ) {

				min = plane.normal.x * this.min.x;
				max = plane.normal.x * this.max.x;

			} else {

				min = plane.normal.x * this.max.x;
				max = plane.normal.x * this.min.x;

			}

			if ( plane.normal.y > 0 ) {

				min += plane.normal.y * this.min.y;
				max += plane.normal.y * this.max.y;

			} else {

				min += plane.normal.y * this.max.y;
				max += plane.normal.y * this.min.y;

			}

			if ( plane.normal.z > 0 ) {

				min += plane.normal.z * this.min.z;
				max += plane.normal.z * this.max.z;

			} else {

				min += plane.normal.z * this.max.z;
				max += plane.normal.z * this.min.z;

			}

			return ( min <= - plane.constant && max >= - plane.constant );

		}

		intersectsTriangle( triangle ) {

			if ( this.isEmpty() ) {

				return false;

			}

			// compute box center and extents
			this.getCenter( _center );
			_extents.subVectors( this.max, _center );

			// translate triangle to aabb origin
			_v0.subVectors( triangle.a, _center );
			_v1.subVectors( triangle.b, _center );
			_v2.subVectors( triangle.c, _center );

			// compute edge vectors for triangle
			_f0.subVectors( _v1, _v0 );
			_f1.subVectors( _v2, _v1 );
			_f2.subVectors( _v0, _v2 );

			// test against axes that are given by cross product combinations of the edges of the triangle and the edges of the aabb
			// make an axis testing of each of the 3 sides of the aabb against each of the 3 sides of the triangle = 9 axis of separation
			// axis_ij = u_i x f_j (u0, u1, u2 = face normals of aabb = x,y,z axes vectors since aabb is axis aligned)
			let axes = [
				0, - _f0.z, _f0.y, 0, - _f1.z, _f1.y, 0, - _f2.z, _f2.y,
				_f0.z, 0, - _f0.x, _f1.z, 0, - _f1.x, _f2.z, 0, - _f2.x,
				- _f0.y, _f0.x, 0, - _f1.y, _f1.x, 0, - _f2.y, _f2.x, 0
			];
			if ( ! satForAxes( axes, _v0, _v1, _v2, _extents ) ) {

				return false;

			}

			// test 3 face normals from the aabb
			axes = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
			if ( ! satForAxes( axes, _v0, _v1, _v2, _extents ) ) {

				return false;

			}

			// finally testing the face normal of the triangle
			// use already existing triangle edge vectors here
			_triangleNormal.crossVectors( _f0, _f1 );
			axes = [ _triangleNormal.x, _triangleNormal.y, _triangleNormal.z ];

			return satForAxes( axes, _v0, _v1, _v2, _extents );

		}

		clampPoint( point, target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Box3: .clampPoint() target is now required' );
				target = new Vector3();

			}

			return target.copy( point ).clamp( this.min, this.max );

		}

		distanceToPoint( point ) {

			const clampedPoint = _vector$1.copy( point ).clamp( this.min, this.max );

			return clampedPoint.sub( point ).length();

		}

		getBoundingSphere( target ) {

			if ( target === undefined ) {

				console.error( 'THREE.Box3: .getBoundingSphere() target is now required' );
				//target = new Sphere(); // removed to avoid cyclic dependency

			}

			this.getCenter( target.center );

			target.radius = this.getSize( _vector$1 ).length() * 0.5;

			return target;

		}

		intersect( box ) {

			this.min.max( box.min );
			this.max.min( box.max );

			// ensure that if there is no overlap, the result is fully empty, not slightly empty with non-inf/+inf values that will cause subsequence intersects to erroneously return valid values.
			if ( this.isEmpty() ) this.makeEmpty();

			return this;

		}

		union( box ) {

			this.min.min( box.min );
			this.max.max( box.max );

			return this;

		}

		applyMatrix4( matrix ) {

			// transform of empty box is an empty box.
			if ( this.isEmpty() ) return this;

			// NOTE: I am using a binary pattern to specify all 2^3 combinations below
			_points[ 0 ].set( this.min.x, this.min.y, this.min.z ).applyMatrix4( matrix ); // 000
			_points[ 1 ].set( this.min.x, this.min.y, this.max.z ).applyMatrix4( matrix ); // 001
			_points[ 2 ].set( this.min.x, this.max.y, this.min.z ).applyMatrix4( matrix ); // 010
			_points[ 3 ].set( this.min.x, this.max.y, this.max.z ).applyMatrix4( matrix ); // 011
			_points[ 4 ].set( this.max.x, this.min.y, this.min.z ).applyMatrix4( matrix ); // 100
			_points[ 5 ].set( this.max.x, this.min.y, this.max.z ).applyMatrix4( matrix ); // 101
			_points[ 6 ].set( this.max.x, this.max.y, this.min.z ).applyMatrix4( matrix ); // 110
			_points[ 7 ].set( this.max.x, this.max.y, this.max.z ).applyMatrix4( matrix ); // 111

			this.setFromPoints( _points );

			return this;

		}

		translate( offset ) {

			this.min.add( offset );
			this.max.add( offset );

			return this;

		}

		equals( box ) {

			return box.min.equals( this.min ) && box.max.equals( this.max );

		}

	}

	function satForAxes( axes, v0, v1, v2, extents ) {

		for ( let i = 0, j = axes.length - 3; i <= j; i += 3 ) {

			_testAxis.fromArray( axes, i );
			// project the aabb onto the seperating axis
			const r = extents.x * Math.abs( _testAxis.x ) + extents.y * Math.abs( _testAxis.y ) + extents.z * Math.abs( _testAxis.z );
			// project all 3 vertices of the triangle onto the seperating axis
			const p0 = v0.dot( _testAxis );
			const p1 = v1.dot( _testAxis );
			const p2 = v2.dot( _testAxis );
			// actual test, basically see if either of the most extreme of the triangle points intersects r
			if ( Math.max( - Math.max( p0, p1, p2 ), Math.min( p0, p1, p2 ) ) > r ) {

				// points of the projected triangle are outside the projected half-length of the aabb
				// the axis is seperating and we can exit
				return false;

			}

		}

		return true;

	}

	const _points = [
		/*@__PURE__*/ new Vector3(),
		/*@__PURE__*/ new Vector3(),
		/*@__PURE__*/ new Vector3(),
		/*@__PURE__*/ new Vector3(),
		/*@__PURE__*/ new Vector3(),
		/*@__PURE__*/ new Vector3(),
		/*@__PURE__*/ new Vector3(),
		/*@__PURE__*/ new Vector3()
	];

	const _vector$1 = /*@__PURE__*/ new Vector3();

	const _box = /*@__PURE__*/ new Box3();

	// triangle centered vertices

	const _v0 = /*@__PURE__*/ new Vector3();
	const _v1 = /*@__PURE__*/ new Vector3();
	const _v2 = /*@__PURE__*/ new Vector3();

	// triangle edge vectors

	const _f0 = /*@__PURE__*/ new Vector3();
	const _f1 = /*@__PURE__*/ new Vector3();
	const _f2 = /*@__PURE__*/ new Vector3();

	const _center = /*@__PURE__*/ new Vector3();
	const _extents = /*@__PURE__*/ new Vector3();
	const _triangleNormal = /*@__PURE__*/ new Vector3();
	const _testAxis = /*@__PURE__*/ new Vector3();

	const _box$1 = /*@__PURE__*/ new Box3();

	class Sphere {

		constructor( center, radius ) {

			this.center = ( center !== undefined ) ? center : new Vector3();
			this.radius = ( radius !== undefined ) ? radius : - 1;

		}

		set( center, radius ) {

			this.center.copy( center );
			this.radius = radius;

			return this;

		}

		setFromPoints( points, optionalCenter ) {

			const center = this.center;

			if ( optionalCenter !== undefined ) {

				center.copy( optionalCenter );

			} else {

				_box$1.setFromPoints( points ).getCenter( center );

			}

			let maxRadiusSq = 0;

			for ( let i = 0, il = points.length; i < il; i ++ ) {

				maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( points[ i ] ) );

			}

			this.radius = Math.sqrt( maxRadiusSq );

			return this;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( sphere ) {

			this.center.copy( sphere.center );
			this.radius = sphere.radius;

			return this;

		}

		isEmpty() {

			return ( this.radius < 0 );

		}

		makeEmpty() {

			this.center.set( 0, 0, 0 );
			this.radius = - 1;

			return this;

		}

		containsPoint( point ) {

			return ( point.distanceToSquared( this.center ) <= ( this.radius * this.radius ) );

		}

		distanceToPoint( point ) {

			return ( point.distanceTo( this.center ) - this.radius );

		}

		intersectsSphere( sphere ) {

			const radiusSum = this.radius + sphere.radius;

			return sphere.center.distanceToSquared( this.center ) <= ( radiusSum * radiusSum );

		}

		intersectsBox( box ) {

			return box.intersectsSphere( this );

		}

		intersectsPlane( plane ) {

			return Math.abs( plane.distanceToPoint( this.center ) ) <= this.radius;

		}

		clampPoint( point, target ) {

			const deltaLengthSq = this.center.distanceToSquared( point );

			if ( target === undefined ) {

				console.warn( 'THREE.Sphere: .clampPoint() target is now required' );
				target = new Vector3();

			}

			target.copy( point );

			if ( deltaLengthSq > ( this.radius * this.radius ) ) {

				target.sub( this.center ).normalize();
				target.multiplyScalar( this.radius ).add( this.center );

			}

			return target;

		}

		getBoundingBox( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Sphere: .getBoundingBox() target is now required' );
				target = new Box3();

			}

			if ( this.isEmpty() ) {

				// Empty sphere produces empty bounding box
				target.makeEmpty();
				return target;

			}

			target.set( this.center, this.center );
			target.expandByScalar( this.radius );

			return target;

		}

		applyMatrix4( matrix ) {

			this.center.applyMatrix4( matrix );
			this.radius = this.radius * matrix.getMaxScaleOnAxis();

			return this;

		}

		translate( offset ) {

			this.center.add( offset );

			return this;

		}

		equals( sphere ) {

			return sphere.center.equals( this.center ) && ( sphere.radius === this.radius );

		}

	}

	const _vector$2 = /*@__PURE__*/ new Vector3();
	const _segCenter = /*@__PURE__*/ new Vector3();
	const _segDir = /*@__PURE__*/ new Vector3();
	const _diff = /*@__PURE__*/ new Vector3();

	const _edge1 = /*@__PURE__*/ new Vector3();
	const _edge2 = /*@__PURE__*/ new Vector3();
	const _normal = /*@__PURE__*/ new Vector3();

	class Ray {

		constructor( origin, direction ) {

			this.origin = ( origin !== undefined ) ? origin : new Vector3();
			this.direction = ( direction !== undefined ) ? direction : new Vector3( 0, 0, - 1 );

		}

		set( origin, direction ) {

			this.origin.copy( origin );
			this.direction.copy( direction );

			return this;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( ray ) {

			this.origin.copy( ray.origin );
			this.direction.copy( ray.direction );

			return this;

		}

		at( t, target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Ray: .at() target is now required' );
				target = new Vector3();

			}

			return target.copy( this.direction ).multiplyScalar( t ).add( this.origin );

		}

		lookAt( v ) {

			this.direction.copy( v ).sub( this.origin ).normalize();

			return this;

		}

		recast( t ) {

			this.origin.copy( this.at( t, _vector$2 ) );

			return this;

		}

		closestPointToPoint( point, target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Ray: .closestPointToPoint() target is now required' );
				target = new Vector3();

			}

			target.subVectors( point, this.origin );

			const directionDistance = target.dot( this.direction );

			if ( directionDistance < 0 ) {

				return target.copy( this.origin );

			}

			return target.copy( this.direction ).multiplyScalar( directionDistance ).add( this.origin );

		}

		distanceToPoint( point ) {

			return Math.sqrt( this.distanceSqToPoint( point ) );

		}

		distanceSqToPoint( point ) {

			const directionDistance = _vector$2.subVectors( point, this.origin ).dot( this.direction );

			// point behind the ray

			if ( directionDistance < 0 ) {

				return this.origin.distanceToSquared( point );

			}

			_vector$2.copy( this.direction ).multiplyScalar( directionDistance ).add( this.origin );

			return _vector$2.distanceToSquared( point );

		}

		distanceSqToSegment( v0, v1, optionalPointOnRay, optionalPointOnSegment ) {

			// from http://www.geometrictools.com/GTEngine/Include/Mathematics/GteDistRaySegment.h
			// It returns the min distance between the ray and the segment
			// defined by v0 and v1
			// It can also set two optional targets :
			// - The closest point on the ray
			// - The closest point on the segment

			_segCenter.copy( v0 ).add( v1 ).multiplyScalar( 0.5 );
			_segDir.copy( v1 ).sub( v0 ).normalize();
			_diff.copy( this.origin ).sub( _segCenter );

			const segExtent = v0.distanceTo( v1 ) * 0.5;
			const a01 = - this.direction.dot( _segDir );
			const b0 = _diff.dot( this.direction );
			const b1 = - _diff.dot( _segDir );
			const c = _diff.lengthSq();
			const det = Math.abs( 1 - a01 * a01 );
			let s0, s1, sqrDist, extDet;

			if ( det > 0 ) {

				// The ray and segment are not parallel.

				s0 = a01 * b1 - b0;
				s1 = a01 * b0 - b1;
				extDet = segExtent * det;

				if ( s0 >= 0 ) {

					if ( s1 >= - extDet ) {

						if ( s1 <= extDet ) {

							// region 0
							// Minimum at interior points of ray and segment.

							const invDet = 1 / det;
							s0 *= invDet;
							s1 *= invDet;
							sqrDist = s0 * ( s0 + a01 * s1 + 2 * b0 ) + s1 * ( a01 * s0 + s1 + 2 * b1 ) + c;

						} else {

							// region 1

							s1 = segExtent;
							s0 = Math.max( 0, - ( a01 * s1 + b0 ) );
							sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

						}

					} else {

						// region 5

						s1 = - segExtent;
						s0 = Math.max( 0, - ( a01 * s1 + b0 ) );
						sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

					}

				} else {

					if ( s1 <= - extDet ) {

						// region 4

						s0 = Math.max( 0, - ( - a01 * segExtent + b0 ) );
						s1 = ( s0 > 0 ) ? - segExtent : Math.min( Math.max( - segExtent, - b1 ), segExtent );
						sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

					} else if ( s1 <= extDet ) {

						// region 3

						s0 = 0;
						s1 = Math.min( Math.max( - segExtent, - b1 ), segExtent );
						sqrDist = s1 * ( s1 + 2 * b1 ) + c;

					} else {

						// region 2

						s0 = Math.max( 0, - ( a01 * segExtent + b0 ) );
						s1 = ( s0 > 0 ) ? segExtent : Math.min( Math.max( - segExtent, - b1 ), segExtent );
						sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

					}

				}

			} else {

				// Ray and segment are parallel.

				s1 = ( a01 > 0 ) ? - segExtent : segExtent;
				s0 = Math.max( 0, - ( a01 * s1 + b0 ) );
				sqrDist = - s0 * s0 + s1 * ( s1 + 2 * b1 ) + c;

			}

			if ( optionalPointOnRay ) {

				optionalPointOnRay.copy( this.direction ).multiplyScalar( s0 ).add( this.origin );

			}

			if ( optionalPointOnSegment ) {

				optionalPointOnSegment.copy( _segDir ).multiplyScalar( s1 ).add( _segCenter );

			}

			return sqrDist;

		}

		intersectSphere( sphere, target ) {

			_vector$2.subVectors( sphere.center, this.origin );
			const tca = _vector$2.dot( this.direction );
			const d2 = _vector$2.dot( _vector$2 ) - tca * tca;
			const radius2 = sphere.radius * sphere.radius;

			if ( d2 > radius2 ) return null;

			const thc = Math.sqrt( radius2 - d2 );

			// t0 = first intersect point - entrance on front of sphere
			const t0 = tca - thc;

			// t1 = second intersect point - exit point on back of sphere
			const t1 = tca + thc;

			// test to see if both t0 and t1 are behind the ray - if so, return null
			if ( t0 < 0 && t1 < 0 ) return null;

			// test to see if t0 is behind the ray:
			// if it is, the ray is inside the sphere, so return the second exit point scaled by t1,
			// in order to always return an intersect point that is in front of the ray.
			if ( t0 < 0 ) return this.at( t1, target );

			// else t0 is in front of the ray, so return the first collision point scaled by t0
			return this.at( t0, target );

		}

		intersectsSphere( sphere ) {

			return this.distanceSqToPoint( sphere.center ) <= ( sphere.radius * sphere.radius );

		}

		distanceToPlane( plane ) {

			const denominator = plane.normal.dot( this.direction );

			if ( denominator === 0 ) {

				// line is coplanar, return origin
				if ( plane.distanceToPoint( this.origin ) === 0 ) {

					return 0;

				}

				// Null is preferable to undefined since undefined means.... it is undefined

				return null;

			}

			const t = - ( this.origin.dot( plane.normal ) + plane.constant ) / denominator;

			// Return if the ray never intersects the plane

			return t >= 0 ? t : null;

		}

		intersectPlane( plane, target ) {

			const t = this.distanceToPlane( plane );

			if ( t === null ) {

				return null;

			}

			return this.at( t, target );

		}

		intersectsPlane( plane ) {

			// check if the ray lies on the plane first

			const distToPoint = plane.distanceToPoint( this.origin );

			if ( distToPoint === 0 ) {

				return true;

			}

			const denominator = plane.normal.dot( this.direction );

			if ( denominator * distToPoint < 0 ) {

				return true;

			}

			// ray origin is behind the plane (and is pointing behind it)

			return false;

		}

		intersectBox( box, target ) {

			let tmin, tmax, tymin, tymax, tzmin, tzmax;

			const invdirx = 1 / this.direction.x,
				invdiry = 1 / this.direction.y,
				invdirz = 1 / this.direction.z;

			const origin = this.origin;

			if ( invdirx >= 0 ) {

				tmin = ( box.min.x - origin.x ) * invdirx;
				tmax = ( box.max.x - origin.x ) * invdirx;

			} else {

				tmin = ( box.max.x - origin.x ) * invdirx;
				tmax = ( box.min.x - origin.x ) * invdirx;

			}

			if ( invdiry >= 0 ) {

				tymin = ( box.min.y - origin.y ) * invdiry;
				tymax = ( box.max.y - origin.y ) * invdiry;

			} else {

				tymin = ( box.max.y - origin.y ) * invdiry;
				tymax = ( box.min.y - origin.y ) * invdiry;

			}

			if ( ( tmin > tymax ) || ( tymin > tmax ) ) return null;

			// These lines also handle the case where tmin or tmax is NaN
			// (result of 0 * Infinity). x !== x returns true if x is NaN

			if ( tymin > tmin || tmin !== tmin ) tmin = tymin;

			if ( tymax < tmax || tmax !== tmax ) tmax = tymax;

			if ( invdirz >= 0 ) {

				tzmin = ( box.min.z - origin.z ) * invdirz;
				tzmax = ( box.max.z - origin.z ) * invdirz;

			} else {

				tzmin = ( box.max.z - origin.z ) * invdirz;
				tzmax = ( box.min.z - origin.z ) * invdirz;

			}

			if ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return null;

			if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin;

			if ( tzmax < tmax || tmax !== tmax ) tmax = tzmax;

			//return point closest to the ray (positive side)

			if ( tmax < 0 ) return null;

			return this.at( tmin >= 0 ? tmin : tmax, target );

		}

		intersectsBox( box ) {

			return this.intersectBox( box, _vector$2 ) !== null;

		}

		intersectTriangle( a, b, c, backfaceCulling, target ) {

			// Compute the offset origin, edges, and normal.

			// from http://www.geometrictools.com/GTEngine/Include/Mathematics/GteIntrRay3Triangle3.h

			_edge1.subVectors( b, a );
			_edge2.subVectors( c, a );
			_normal.crossVectors( _edge1, _edge2 );

			// Solve Q + t*D = b1*E1 + b2*E2 (Q = kDiff, D = ray direction,
			// E1 = kEdge1, E2 = kEdge2, N = Cross(E1,E2)) by
			//   |Dot(D,N)|*b1 = sign(Dot(D,N))*Dot(D,Cross(Q,E2))
			//   |Dot(D,N)|*b2 = sign(Dot(D,N))*Dot(D,Cross(E1,Q))
			//   |Dot(D,N)|*t = -sign(Dot(D,N))*Dot(Q,N)
			let DdN = this.direction.dot( _normal );
			let sign;

			if ( DdN > 0 ) {

				if ( backfaceCulling ) return null;
				sign = 1;

			} else if ( DdN < 0 ) {

				sign = - 1;
				DdN = - DdN;

			} else {

				return null;

			}

			_diff.subVectors( this.origin, a );
			const DdQxE2 = sign * this.direction.dot( _edge2.crossVectors( _diff, _edge2 ) );

			// b1 < 0, no intersection
			if ( DdQxE2 < 0 ) {

				return null;

			}

			const DdE1xQ = sign * this.direction.dot( _edge1.cross( _diff ) );

			// b2 < 0, no intersection
			if ( DdE1xQ < 0 ) {

				return null;

			}

			// b1+b2 > 1, no intersection
			if ( DdQxE2 + DdE1xQ > DdN ) {

				return null;

			}

			// Line intersects triangle, check if ray does.
			const QdN = - sign * _diff.dot( _normal );

			// t < 0, no intersection
			if ( QdN < 0 ) {

				return null;

			}

			// Ray intersects triangle.
			return this.at( QdN / DdN, target );

		}

		applyMatrix4( matrix4 ) {

			this.origin.applyMatrix4( matrix4 );
			this.direction.transformDirection( matrix4 );

			return this;

		}

		equals( ray ) {

			return ray.origin.equals( this.origin ) && ray.direction.equals( this.direction );

		}

	}

	class Matrix4 {

		constructor() {

			Object.defineProperty( this, 'isMatrix4', { value: true } );

			this.elements = [

				1, 0, 0, 0,
				0, 1, 0, 0,
				0, 0, 1, 0,
				0, 0, 0, 1

			];

			if ( arguments.length > 0 ) {

				console.error( 'THREE.Matrix4: the constructor no longer reads arguments. use .set() instead.' );

			}

		}

		set( n11, n12, n13, n14, n21, n22, n23, n24, n31, n32, n33, n34, n41, n42, n43, n44 ) {

			const te = this.elements;

			te[ 0 ] = n11; te[ 4 ] = n12; te[ 8 ] = n13; te[ 12 ] = n14;
			te[ 1 ] = n21; te[ 5 ] = n22; te[ 9 ] = n23; te[ 13 ] = n24;
			te[ 2 ] = n31; te[ 6 ] = n32; te[ 10 ] = n33; te[ 14 ] = n34;
			te[ 3 ] = n41; te[ 7 ] = n42; te[ 11 ] = n43; te[ 15 ] = n44;

			return this;

		}

		identity() {

			this.set(

				1, 0, 0, 0,
				0, 1, 0, 0,
				0, 0, 1, 0,
				0, 0, 0, 1

			);

			return this;

		}

		clone() {

			return new Matrix4().fromArray( this.elements );

		}

		copy( m ) {

			const te = this.elements;
			const me = m.elements;

			te[ 0 ] = me[ 0 ]; te[ 1 ] = me[ 1 ]; te[ 2 ] = me[ 2 ]; te[ 3 ] = me[ 3 ];
			te[ 4 ] = me[ 4 ]; te[ 5 ] = me[ 5 ]; te[ 6 ] = me[ 6 ]; te[ 7 ] = me[ 7 ];
			te[ 8 ] = me[ 8 ]; te[ 9 ] = me[ 9 ]; te[ 10 ] = me[ 10 ]; te[ 11 ] = me[ 11 ];
			te[ 12 ] = me[ 12 ]; te[ 13 ] = me[ 13 ]; te[ 14 ] = me[ 14 ]; te[ 15 ] = me[ 15 ];

			return this;

		}

		copyPosition( m ) {

			const te = this.elements, me = m.elements;

			te[ 12 ] = me[ 12 ];
			te[ 13 ] = me[ 13 ];
			te[ 14 ] = me[ 14 ];

			return this;

		}

		extractBasis( xAxis, yAxis, zAxis ) {

			xAxis.setFromMatrixColumn( this, 0 );
			yAxis.setFromMatrixColumn( this, 1 );
			zAxis.setFromMatrixColumn( this, 2 );

			return this;

		}

		makeBasis( xAxis, yAxis, zAxis ) {

			this.set(
				xAxis.x, yAxis.x, zAxis.x, 0,
				xAxis.y, yAxis.y, zAxis.y, 0,
				xAxis.z, yAxis.z, zAxis.z, 0,
				0, 0, 0, 1
			);

			return this;

		}

		extractRotation( m ) {

			// this method does not support reflection matrices

			const te = this.elements;
			const me = m.elements;

			const scaleX = 1 / _v1$1.setFromMatrixColumn( m, 0 ).length();
			const scaleY = 1 / _v1$1.setFromMatrixColumn( m, 1 ).length();
			const scaleZ = 1 / _v1$1.setFromMatrixColumn( m, 2 ).length();

			te[ 0 ] = me[ 0 ] * scaleX;
			te[ 1 ] = me[ 1 ] * scaleX;
			te[ 2 ] = me[ 2 ] * scaleX;
			te[ 3 ] = 0;

			te[ 4 ] = me[ 4 ] * scaleY;
			te[ 5 ] = me[ 5 ] * scaleY;
			te[ 6 ] = me[ 6 ] * scaleY;
			te[ 7 ] = 0;

			te[ 8 ] = me[ 8 ] * scaleZ;
			te[ 9 ] = me[ 9 ] * scaleZ;
			te[ 10 ] = me[ 10 ] * scaleZ;
			te[ 11 ] = 0;

			te[ 12 ] = 0;
			te[ 13 ] = 0;
			te[ 14 ] = 0;
			te[ 15 ] = 1;

			return this;

		}

		makeRotationFromEuler( euler ) {

			if ( ! ( euler && euler.isEuler ) ) {

				console.error( 'THREE.Matrix4: .makeRotationFromEuler() now expects a Euler rotation rather than a Vector3 and order.' );

			}

			const te = this.elements;

			const x = euler.x, y = euler.y, z = euler.z;
			const a = Math.cos( x ), b = Math.sin( x );
			const c = Math.cos( y ), d = Math.sin( y );
			const e = Math.cos( z ), f = Math.sin( z );

			if ( euler.order === 'XYZ' ) {

				const ae = a * e, af = a * f, be = b * e, bf = b * f;

				te[ 0 ] = c * e;
				te[ 4 ] = - c * f;
				te[ 8 ] = d;

				te[ 1 ] = af + be * d;
				te[ 5 ] = ae - bf * d;
				te[ 9 ] = - b * c;

				te[ 2 ] = bf - ae * d;
				te[ 6 ] = be + af * d;
				te[ 10 ] = a * c;

			} else if ( euler.order === 'YXZ' ) {

				const ce = c * e, cf = c * f, de = d * e, df = d * f;

				te[ 0 ] = ce + df * b;
				te[ 4 ] = de * b - cf;
				te[ 8 ] = a * d;

				te[ 1 ] = a * f;
				te[ 5 ] = a * e;
				te[ 9 ] = - b;

				te[ 2 ] = cf * b - de;
				te[ 6 ] = df + ce * b;
				te[ 10 ] = a * c;

			} else if ( euler.order === 'ZXY' ) {

				const ce = c * e, cf = c * f, de = d * e, df = d * f;

				te[ 0 ] = ce - df * b;
				te[ 4 ] = - a * f;
				te[ 8 ] = de + cf * b;

				te[ 1 ] = cf + de * b;
				te[ 5 ] = a * e;
				te[ 9 ] = df - ce * b;

				te[ 2 ] = - a * d;
				te[ 6 ] = b;
				te[ 10 ] = a * c;

			} else if ( euler.order === 'ZYX' ) {

				const ae = a * e, af = a * f, be = b * e, bf = b * f;

				te[ 0 ] = c * e;
				te[ 4 ] = be * d - af;
				te[ 8 ] = ae * d + bf;

				te[ 1 ] = c * f;
				te[ 5 ] = bf * d + ae;
				te[ 9 ] = af * d - be;

				te[ 2 ] = - d;
				te[ 6 ] = b * c;
				te[ 10 ] = a * c;

			} else if ( euler.order === 'YZX' ) {

				const ac = a * c, ad = a * d, bc = b * c, bd = b * d;

				te[ 0 ] = c * e;
				te[ 4 ] = bd - ac * f;
				te[ 8 ] = bc * f + ad;

				te[ 1 ] = f;
				te[ 5 ] = a * e;
				te[ 9 ] = - b * e;

				te[ 2 ] = - d * e;
				te[ 6 ] = ad * f + bc;
				te[ 10 ] = ac - bd * f;

			} else if ( euler.order === 'XZY' ) {

				const ac = a * c, ad = a * d, bc = b * c, bd = b * d;

				te[ 0 ] = c * e;
				te[ 4 ] = - f;
				te[ 8 ] = d * e;

				te[ 1 ] = ac * f + bd;
				te[ 5 ] = a * e;
				te[ 9 ] = ad * f - bc;

				te[ 2 ] = bc * f - ad;
				te[ 6 ] = b * e;
				te[ 10 ] = bd * f + ac;

			}

			// bottom row
			te[ 3 ] = 0;
			te[ 7 ] = 0;
			te[ 11 ] = 0;

			// last column
			te[ 12 ] = 0;
			te[ 13 ] = 0;
			te[ 14 ] = 0;
			te[ 15 ] = 1;

			return this;

		}

		makeRotationFromQuaternion( q ) {

			return this.compose( _zero, q, _one );

		}

		lookAt( eye, target, up ) {

			const te = this.elements;

			_z.subVectors( eye, target );

			if ( _z.lengthSq() === 0 ) {

				// eye and target are in the same position

				_z.z = 1;

			}

			_z.normalize();
			_x.crossVectors( up, _z );

			if ( _x.lengthSq() === 0 ) {

				// up and z are parallel

				if ( Math.abs( up.z ) === 1 ) {

					_z.x += 0.0001;

				} else {

					_z.z += 0.0001;

				}

				_z.normalize();
				_x.crossVectors( up, _z );

			}

			_x.normalize();
			_y.crossVectors( _z, _x );

			te[ 0 ] = _x.x; te[ 4 ] = _y.x; te[ 8 ] = _z.x;
			te[ 1 ] = _x.y; te[ 5 ] = _y.y; te[ 9 ] = _z.y;
			te[ 2 ] = _x.z; te[ 6 ] = _y.z; te[ 10 ] = _z.z;

			return this;

		}

		multiply( m, n ) {

			if ( n !== undefined ) {

				console.warn( 'THREE.Matrix4: .multiply() now only accepts one argument. Use .multiplyMatrices( a, b ) instead.' );
				return this.multiplyMatrices( m, n );

			}

			return this.multiplyMatrices( this, m );

		}

		premultiply( m ) {

			return this.multiplyMatrices( m, this );

		}

		multiplyMatrices( a, b ) {

			const ae = a.elements;
			const be = b.elements;
			const te = this.elements;

			const a11 = ae[ 0 ], a12 = ae[ 4 ], a13 = ae[ 8 ], a14 = ae[ 12 ];
			const a21 = ae[ 1 ], a22 = ae[ 5 ], a23 = ae[ 9 ], a24 = ae[ 13 ];
			const a31 = ae[ 2 ], a32 = ae[ 6 ], a33 = ae[ 10 ], a34 = ae[ 14 ];
			const a41 = ae[ 3 ], a42 = ae[ 7 ], a43 = ae[ 11 ], a44 = ae[ 15 ];

			const b11 = be[ 0 ], b12 = be[ 4 ], b13 = be[ 8 ], b14 = be[ 12 ];
			const b21 = be[ 1 ], b22 = be[ 5 ], b23 = be[ 9 ], b24 = be[ 13 ];
			const b31 = be[ 2 ], b32 = be[ 6 ], b33 = be[ 10 ], b34 = be[ 14 ];
			const b41 = be[ 3 ], b42 = be[ 7 ], b43 = be[ 11 ], b44 = be[ 15 ];

			te[ 0 ] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
			te[ 4 ] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
			te[ 8 ] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
			te[ 12 ] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;

			te[ 1 ] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
			te[ 5 ] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
			te[ 9 ] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
			te[ 13 ] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;

			te[ 2 ] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
			te[ 6 ] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
			te[ 10 ] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
			te[ 14 ] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;

			te[ 3 ] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
			te[ 7 ] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
			te[ 11 ] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
			te[ 15 ] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;

			return this;

		}

		multiplyScalar( s ) {

			const te = this.elements;

			te[ 0 ] *= s; te[ 4 ] *= s; te[ 8 ] *= s; te[ 12 ] *= s;
			te[ 1 ] *= s; te[ 5 ] *= s; te[ 9 ] *= s; te[ 13 ] *= s;
			te[ 2 ] *= s; te[ 6 ] *= s; te[ 10 ] *= s; te[ 14 ] *= s;
			te[ 3 ] *= s; te[ 7 ] *= s; te[ 11 ] *= s; te[ 15 ] *= s;

			return this;

		}

		determinant() {

			const te = this.elements;

			const n11 = te[ 0 ], n12 = te[ 4 ], n13 = te[ 8 ], n14 = te[ 12 ];
			const n21 = te[ 1 ], n22 = te[ 5 ], n23 = te[ 9 ], n24 = te[ 13 ];
			const n31 = te[ 2 ], n32 = te[ 6 ], n33 = te[ 10 ], n34 = te[ 14 ];
			const n41 = te[ 3 ], n42 = te[ 7 ], n43 = te[ 11 ], n44 = te[ 15 ];

			//TODO: make this more efficient
			//( based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm )

			return (
				n41 * (
					+ n14 * n23 * n32
					 - n13 * n24 * n32
					 - n14 * n22 * n33
					 + n12 * n24 * n33
					 + n13 * n22 * n34
					 - n12 * n23 * n34
				) +
				n42 * (
					+ n11 * n23 * n34
					 - n11 * n24 * n33
					 + n14 * n21 * n33
					 - n13 * n21 * n34
					 + n13 * n24 * n31
					 - n14 * n23 * n31
				) +
				n43 * (
					+ n11 * n24 * n32
					 - n11 * n22 * n34
					 - n14 * n21 * n32
					 + n12 * n21 * n34
					 + n14 * n22 * n31
					 - n12 * n24 * n31
				) +
				n44 * (
					- n13 * n22 * n31
					 - n11 * n23 * n32
					 + n11 * n22 * n33
					 + n13 * n21 * n32
					 - n12 * n21 * n33
					 + n12 * n23 * n31
				)

			);

		}

		transpose() {

			const te = this.elements;
			let tmp;

			tmp = te[ 1 ]; te[ 1 ] = te[ 4 ]; te[ 4 ] = tmp;
			tmp = te[ 2 ]; te[ 2 ] = te[ 8 ]; te[ 8 ] = tmp;
			tmp = te[ 6 ]; te[ 6 ] = te[ 9 ]; te[ 9 ] = tmp;

			tmp = te[ 3 ]; te[ 3 ] = te[ 12 ]; te[ 12 ] = tmp;
			tmp = te[ 7 ]; te[ 7 ] = te[ 13 ]; te[ 13 ] = tmp;
			tmp = te[ 11 ]; te[ 11 ] = te[ 14 ]; te[ 14 ] = tmp;

			return this;

		}

		setPosition( x, y, z ) {

			const te = this.elements;

			if ( x.isVector3 ) {

				te[ 12 ] = x.x;
				te[ 13 ] = x.y;
				te[ 14 ] = x.z;

			} else {

				te[ 12 ] = x;
				te[ 13 ] = y;
				te[ 14 ] = z;

			}

			return this;

		}

		invert() {

			// based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm
			const te = this.elements,

				n11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ], n41 = te[ 3 ],
				n12 = te[ 4 ], n22 = te[ 5 ], n32 = te[ 6 ], n42 = te[ 7 ],
				n13 = te[ 8 ], n23 = te[ 9 ], n33 = te[ 10 ], n43 = te[ 11 ],
				n14 = te[ 12 ], n24 = te[ 13 ], n34 = te[ 14 ], n44 = te[ 15 ],

				t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44,
				t12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44,
				t13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44,
				t14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34;

			const det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14;

			if ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 );

			const detInv = 1 / det;

			te[ 0 ] = t11 * detInv;
			te[ 1 ] = ( n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44 ) * detInv;
			te[ 2 ] = ( n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44 ) * detInv;
			te[ 3 ] = ( n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43 ) * detInv;

			te[ 4 ] = t12 * detInv;
			te[ 5 ] = ( n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44 ) * detInv;
			te[ 6 ] = ( n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44 ) * detInv;
			te[ 7 ] = ( n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43 ) * detInv;

			te[ 8 ] = t13 * detInv;
			te[ 9 ] = ( n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44 ) * detInv;
			te[ 10 ] = ( n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44 ) * detInv;
			te[ 11 ] = ( n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43 ) * detInv;

			te[ 12 ] = t14 * detInv;
			te[ 13 ] = ( n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34 ) * detInv;
			te[ 14 ] = ( n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34 ) * detInv;
			te[ 15 ] = ( n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33 ) * detInv;

			return this;

		}

		scale( v ) {

			const te = this.elements;
			const x = v.x, y = v.y, z = v.z;

			te[ 0 ] *= x; te[ 4 ] *= y; te[ 8 ] *= z;
			te[ 1 ] *= x; te[ 5 ] *= y; te[ 9 ] *= z;
			te[ 2 ] *= x; te[ 6 ] *= y; te[ 10 ] *= z;
			te[ 3 ] *= x; te[ 7 ] *= y; te[ 11 ] *= z;

			return this;

		}

		getMaxScaleOnAxis() {

			const te = this.elements;

			const scaleXSq = te[ 0 ] * te[ 0 ] + te[ 1 ] * te[ 1 ] + te[ 2 ] * te[ 2 ];
			const scaleYSq = te[ 4 ] * te[ 4 ] + te[ 5 ] * te[ 5 ] + te[ 6 ] * te[ 6 ];
			const scaleZSq = te[ 8 ] * te[ 8 ] + te[ 9 ] * te[ 9 ] + te[ 10 ] * te[ 10 ];

			return Math.sqrt( Math.max( scaleXSq, scaleYSq, scaleZSq ) );

		}

		makeTranslation( x, y, z ) {

			this.set(

				1, 0, 0, x,
				0, 1, 0, y,
				0, 0, 1, z,
				0, 0, 0, 1

			);

			return this;

		}

		makeRotationX( theta ) {

			const c = Math.cos( theta ), s = Math.sin( theta );

			this.set(

				1, 0, 0, 0,
				0, c, - s, 0,
				0, s, c, 0,
				0, 0, 0, 1

			);

			return this;

		}

		makeRotationY( theta ) {

			const c = Math.cos( theta ), s = Math.sin( theta );

			this.set(

				 c, 0, s, 0,
				 0, 1, 0, 0,
				- s, 0, c, 0,
				 0, 0, 0, 1

			);

			return this;

		}

		makeRotationZ( theta ) {

			const c = Math.cos( theta ), s = Math.sin( theta );

			this.set(

				c, - s, 0, 0,
				s, c, 0, 0,
				0, 0, 1, 0,
				0, 0, 0, 1

			);

			return this;

		}

		makeRotationAxis( axis, angle ) {

			// Based on http://www.gamedev.net/reference/articles/article1199.asp

			const c = Math.cos( angle );
			const s = Math.sin( angle );
			const t = 1 - c;
			const x = axis.x, y = axis.y, z = axis.z;
			const tx = t * x, ty = t * y;

			this.set(

				tx * x + c, tx * y - s * z, tx * z + s * y, 0,
				tx * y + s * z, ty * y + c, ty * z - s * x, 0,
				tx * z - s * y, ty * z + s * x, t * z * z + c, 0,
				0, 0, 0, 1

			);

			return this;

		}

		makeScale( x, y, z ) {

			this.set(

				x, 0, 0, 0,
				0, y, 0, 0,
				0, 0, z, 0,
				0, 0, 0, 1

			);

			return this;

		}

		makeShear( x, y, z ) {

			this.set(

				1, y, z, 0,
				x, 1, z, 0,
				x, y, 1, 0,
				0, 0, 0, 1

			);

			return this;

		}

		compose( position, quaternion, scale ) {

			const te = this.elements;

			const x = quaternion._x, y = quaternion._y, z = quaternion._z, w = quaternion._w;
			const x2 = x + x,	y2 = y + y, z2 = z + z;
			const xx = x * x2, xy = x * y2, xz = x * z2;
			const yy = y * y2, yz = y * z2, zz = z * z2;
			const wx = w * x2, wy = w * y2, wz = w * z2;

			const sx = scale.x, sy = scale.y, sz = scale.z;

			te[ 0 ] = ( 1 - ( yy + zz ) ) * sx;
			te[ 1 ] = ( xy + wz ) * sx;
			te[ 2 ] = ( xz - wy ) * sx;
			te[ 3 ] = 0;

			te[ 4 ] = ( xy - wz ) * sy;
			te[ 5 ] = ( 1 - ( xx + zz ) ) * sy;
			te[ 6 ] = ( yz + wx ) * sy;
			te[ 7 ] = 0;

			te[ 8 ] = ( xz + wy ) * sz;
			te[ 9 ] = ( yz - wx ) * sz;
			te[ 10 ] = ( 1 - ( xx + yy ) ) * sz;
			te[ 11 ] = 0;

			te[ 12 ] = position.x;
			te[ 13 ] = position.y;
			te[ 14 ] = position.z;
			te[ 15 ] = 1;

			return this;

		}

		decompose( position, quaternion, scale ) {

			const te = this.elements;

			let sx = _v1$1.set( te[ 0 ], te[ 1 ], te[ 2 ] ).length();
			const sy = _v1$1.set( te[ 4 ], te[ 5 ], te[ 6 ] ).length();
			const sz = _v1$1.set( te[ 8 ], te[ 9 ], te[ 10 ] ).length();

			// if determine is negative, we need to invert one scale
			const det = this.determinant();
			if ( det < 0 ) sx = - sx;

			position.x = te[ 12 ];
			position.y = te[ 13 ];
			position.z = te[ 14 ];

			// scale the rotation part
			_m1.copy( this );

			const invSX = 1 / sx;
			const invSY = 1 / sy;
			const invSZ = 1 / sz;

			_m1.elements[ 0 ] *= invSX;
			_m1.elements[ 1 ] *= invSX;
			_m1.elements[ 2 ] *= invSX;

			_m1.elements[ 4 ] *= invSY;
			_m1.elements[ 5 ] *= invSY;
			_m1.elements[ 6 ] *= invSY;

			_m1.elements[ 8 ] *= invSZ;
			_m1.elements[ 9 ] *= invSZ;
			_m1.elements[ 10 ] *= invSZ;

			quaternion.setFromRotationMatrix( _m1 );

			scale.x = sx;
			scale.y = sy;
			scale.z = sz;

			return this;

		}

		makePerspective( left, right, top, bottom, near, far ) {

			if ( far === undefined ) {

				console.warn( 'THREE.Matrix4: .makePerspective() has been redefined and has a new signature. Please check the docs.' );

			}

			const te = this.elements;
			const x = 2 * near / ( right - left );
			const y = 2 * near / ( top - bottom );

			const a = ( right + left ) / ( right - left );
			const b = ( top + bottom ) / ( top - bottom );
			const c = - ( far + near ) / ( far - near );
			const d = - 2 * far * near / ( far - near );

			te[ 0 ] = x;	te[ 4 ] = 0;	te[ 8 ] = a;	te[ 12 ] = 0;
			te[ 1 ] = 0;	te[ 5 ] = y;	te[ 9 ] = b;	te[ 13 ] = 0;
			te[ 2 ] = 0;	te[ 6 ] = 0;	te[ 10 ] = c;	te[ 14 ] = d;
			te[ 3 ] = 0;	te[ 7 ] = 0;	te[ 11 ] = - 1;	te[ 15 ] = 0;

			return this;

		}

		makeOrthographic( left, right, top, bottom, near, far ) {

			const te = this.elements;
			const w = 1.0 / ( right - left );
			const h = 1.0 / ( top - bottom );
			const p = 1.0 / ( far - near );

			const x = ( right + left ) * w;
			const y = ( top + bottom ) * h;
			const z = ( far + near ) * p;

			te[ 0 ] = 2 * w;	te[ 4 ] = 0;	te[ 8 ] = 0;	te[ 12 ] = - x;
			te[ 1 ] = 0;	te[ 5 ] = 2 * h;	te[ 9 ] = 0;	te[ 13 ] = - y;
			te[ 2 ] = 0;	te[ 6 ] = 0;	te[ 10 ] = - 2 * p;	te[ 14 ] = - z;
			te[ 3 ] = 0;	te[ 7 ] = 0;	te[ 11 ] = 0;	te[ 15 ] = 1;

			return this;

		}

		equals( matrix ) {

			const te = this.elements;
			const me = matrix.elements;

			for ( let i = 0; i < 16; i ++ ) {

				if ( te[ i ] !== me[ i ] ) return false;

			}

			return true;

		}

		fromArray( array, offset = 0 ) {

			for ( let i = 0; i < 16; i ++ ) {

				this.elements[ i ] = array[ i + offset ];

			}

			return this;

		}

		toArray( array = [], offset = 0 ) {

			const te = this.elements;

			array[ offset ] = te[ 0 ];
			array[ offset + 1 ] = te[ 1 ];
			array[ offset + 2 ] = te[ 2 ];
			array[ offset + 3 ] = te[ 3 ];

			array[ offset + 4 ] = te[ 4 ];
			array[ offset + 5 ] = te[ 5 ];
			array[ offset + 6 ] = te[ 6 ];
			array[ offset + 7 ] = te[ 7 ];

			array[ offset + 8 ] = te[ 8 ];
			array[ offset + 9 ] = te[ 9 ];
			array[ offset + 10 ] = te[ 10 ];
			array[ offset + 11 ] = te[ 11 ];

			array[ offset + 12 ] = te[ 12 ];
			array[ offset + 13 ] = te[ 13 ];
			array[ offset + 14 ] = te[ 14 ];
			array[ offset + 15 ] = te[ 15 ];

			return array;

		}

	}

	const _v1$1 = /*@__PURE__*/ new Vector3();
	const _m1 = /*@__PURE__*/ new Matrix4();
	const _zero = /*@__PURE__*/ new Vector3( 0, 0, 0 );
	const _one = /*@__PURE__*/ new Vector3( 1, 1, 1 );
	const _x = /*@__PURE__*/ new Vector3();
	const _y = /*@__PURE__*/ new Vector3();
	const _z = /*@__PURE__*/ new Vector3();

	class Euler {

		constructor( x = 0, y = 0, z = 0, order = Euler.DefaultOrder ) {

			Object.defineProperty( this, 'isEuler', { value: true } );

			this._x = x;
			this._y = y;
			this._z = z;
			this._order = order;

		}

		get x() {

			return this._x;

		}

		set x( value ) {

			this._x = value;
			this._onChangeCallback();

		}

		get y() {

			return this._y;

		}

		set y( value ) {

			this._y = value;
			this._onChangeCallback();

		}

		get z() {

			return this._z;

		}

		set z( value ) {

			this._z = value;
			this._onChangeCallback();

		}

		get order() {

			return this._order;

		}

		set order( value ) {

			this._order = value;
			this._onChangeCallback();

		}

		set( x, y, z, order ) {

			this._x = x;
			this._y = y;
			this._z = z;
			this._order = order || this._order;

			this._onChangeCallback();

			return this;

		}

		clone() {

			return new this.constructor( this._x, this._y, this._z, this._order );

		}

		copy( euler ) {

			this._x = euler._x;
			this._y = euler._y;
			this._z = euler._z;
			this._order = euler._order;

			this._onChangeCallback();

			return this;

		}

		setFromRotationMatrix( m, order, update ) {

			const clamp = MathUtils.clamp;

			// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)

			const te = m.elements;
			const m11 = te[ 0 ], m12 = te[ 4 ], m13 = te[ 8 ];
			const m21 = te[ 1 ], m22 = te[ 5 ], m23 = te[ 9 ];
			const m31 = te[ 2 ], m32 = te[ 6 ], m33 = te[ 10 ];

			order = order || this._order;

			switch ( order ) {

				case 'XYZ':

					this._y = Math.asin( clamp( m13, - 1, 1 ) );

					if ( Math.abs( m13 ) < 0.9999999 ) {

						this._x = Math.atan2( - m23, m33 );
						this._z = Math.atan2( - m12, m11 );

					} else {

						this._x = Math.atan2( m32, m22 );
						this._z = 0;

					}

					break;

				case 'YXZ':

					this._x = Math.asin( - clamp( m23, - 1, 1 ) );

					if ( Math.abs( m23 ) < 0.9999999 ) {

						this._y = Math.atan2( m13, m33 );
						this._z = Math.atan2( m21, m22 );

					} else {

						this._y = Math.atan2( - m31, m11 );
						this._z = 0;

					}

					break;

				case 'ZXY':

					this._x = Math.asin( clamp( m32, - 1, 1 ) );

					if ( Math.abs( m32 ) < 0.9999999 ) {

						this._y = Math.atan2( - m31, m33 );
						this._z = Math.atan2( - m12, m22 );

					} else {

						this._y = 0;
						this._z = Math.atan2( m21, m11 );

					}

					break;

				case 'ZYX':

					this._y = Math.asin( - clamp( m31, - 1, 1 ) );

					if ( Math.abs( m31 ) < 0.9999999 ) {

						this._x = Math.atan2( m32, m33 );
						this._z = Math.atan2( m21, m11 );

					} else {

						this._x = 0;
						this._z = Math.atan2( - m12, m22 );

					}

					break;

				case 'YZX':

					this._z = Math.asin( clamp( m21, - 1, 1 ) );

					if ( Math.abs( m21 ) < 0.9999999 ) {

						this._x = Math.atan2( - m23, m22 );
						this._y = Math.atan2( - m31, m11 );

					} else {

						this._x = 0;
						this._y = Math.atan2( m13, m33 );

					}

					break;

				case 'XZY':

					this._z = Math.asin( - clamp( m12, - 1, 1 ) );

					if ( Math.abs( m12 ) < 0.9999999 ) {

						this._x = Math.atan2( m32, m22 );
						this._y = Math.atan2( m13, m11 );

					} else {

						this._x = Math.atan2( - m23, m33 );
						this._y = 0;

					}

					break;

				default:

					console.warn( 'THREE.Euler: .setFromRotationMatrix() encountered an unknown order: ' + order );

			}

			this._order = order;

			if ( update !== false ) this._onChangeCallback();

			return this;

		}

		setFromQuaternion( q, order, update ) {

			_matrix.makeRotationFromQuaternion( q );

			return this.setFromRotationMatrix( _matrix, order, update );

		}

		setFromVector3( v, order ) {

			return this.set( v.x, v.y, v.z, order || this._order );

		}

		reorder( newOrder ) {

			// WARNING: this discards revolution information -bhouston

			_quaternion$1.setFromEuler( this );

			return this.setFromQuaternion( _quaternion$1, newOrder );

		}

		equals( euler ) {

			return ( euler._x === this._x ) && ( euler._y === this._y ) && ( euler._z === this._z ) && ( euler._order === this._order );

		}

		fromArray( array ) {

			this._x = array[ 0 ];
			this._y = array[ 1 ];
			this._z = array[ 2 ];
			if ( array[ 3 ] !== undefined ) this._order = array[ 3 ];

			this._onChangeCallback();

			return this;

		}

		toArray( array = [], offset = 0 ) {

			array[ offset ] = this._x;
			array[ offset + 1 ] = this._y;
			array[ offset + 2 ] = this._z;
			array[ offset + 3 ] = this._order;

			return array;

		}

		toVector3( optionalResult ) {

			if ( optionalResult ) {

				return optionalResult.set( this._x, this._y, this._z );

			} else {

				return new Vector3( this._x, this._y, this._z );

			}

		}

		_onChange( callback ) {

			this._onChangeCallback = callback;

			return this;

		}

		_onChangeCallback() {}

	}

	Euler.DefaultOrder = 'XYZ';
	Euler.RotationOrders = [ 'XYZ', 'YZX', 'ZXY', 'XZY', 'YXZ', 'ZYX' ];

	const _matrix = /*@__PURE__*/ new Matrix4();
	const _quaternion$1 = /*@__PURE__*/ new Quaternion();

	class Layers {

		constructor() {

			this.mask = 1 | 0;

		}

		set( channel ) {

			this.mask = 1 << channel | 0;

		}

		enable( channel ) {

			this.mask |= 1 << channel | 0;

		}

		enableAll() {

			this.mask = 0xffffffff | 0;

		}

		toggle( channel ) {

			this.mask ^= 1 << channel | 0;

		}

		disable( channel ) {

			this.mask &= ~ ( 1 << channel | 0 );

		}

		disableAll() {

			this.mask = 0;

		}

		test( layers ) {

			return ( this.mask & layers.mask ) !== 0;

		}

	}

	let _object3DId = 0;

	const _v1$2 = new Vector3();
	const _q1 = new Quaternion();
	const _m1$1 = new Matrix4();
	const _target = new Vector3();

	const _position = new Vector3();
	const _scale = new Vector3();
	const _quaternion$2 = new Quaternion();

	const _xAxis = new Vector3( 1, 0, 0 );
	const _yAxis = new Vector3( 0, 1, 0 );
	const _zAxis = new Vector3( 0, 0, 1 );

	const _addedEvent = { type: 'added' };
	const _removedEvent = { type: 'removed' };

	function Object3D() {

		Object.defineProperty( this, 'id', { value: _object3DId ++ } );

		this.uuid = MathUtils.generateUUID();

		this.name = '';
		this.type = 'Object3D';

		this.parent = null;
		this.children = [];

		this.up = Object3D.DefaultUp.clone();

		const position = new Vector3();
		const rotation = new Euler();
		const quaternion = new Quaternion();
		const scale = new Vector3( 1, 1, 1 );

		function onRotationChange() {

			quaternion.setFromEuler( rotation, false );

		}

		function onQuaternionChange() {

			rotation.setFromQuaternion( quaternion, undefined, false );

		}

		rotation._onChange( onRotationChange );
		quaternion._onChange( onQuaternionChange );

		Object.defineProperties( this, {
			position: {
				configurable: true,
				enumerable: true,
				value: position
			},
			rotation: {
				configurable: true,
				enumerable: true,
				value: rotation
			},
			quaternion: {
				configurable: true,
				enumerable: true,
				value: quaternion
			},
			scale: {
				configurable: true,
				enumerable: true,
				value: scale
			},
			modelViewMatrix: {
				value: new Matrix4()
			},
			normalMatrix: {
				value: new Matrix3()
			}
		} );

		this.matrix = new Matrix4();
		this.matrixWorld = new Matrix4();

		this.matrixAutoUpdate = Object3D.DefaultMatrixAutoUpdate;
		this.matrixWorldNeedsUpdate = false;

		this.layers = new Layers();
		this.visible = true;

		this.castShadow = false;
		this.receiveShadow = false;

		this.frustumCulled = true;
		this.renderOrder = 0;

		this.animations = [];

		this.userData = {};

	}

	Object3D.DefaultUp = new Vector3( 0, 1, 0 );
	Object3D.DefaultMatrixAutoUpdate = true;

	Object3D.prototype = Object.assign( Object.create( EventDispatcher$1.prototype ), {

		constructor: Object3D,

		isObject3D: true,

		onBeforeRender: function () {},
		onAfterRender: function () {},

		applyMatrix4: function ( matrix ) {

			if ( this.matrixAutoUpdate ) this.updateMatrix();

			this.matrix.premultiply( matrix );

			this.matrix.decompose( this.position, this.quaternion, this.scale );

		},

		applyQuaternion: function ( q ) {

			this.quaternion.premultiply( q );

			return this;

		},

		setRotationFromAxisAngle: function ( axis, angle ) {

			// assumes axis is normalized

			this.quaternion.setFromAxisAngle( axis, angle );

		},

		setRotationFromEuler: function ( euler ) {

			this.quaternion.setFromEuler( euler, true );

		},

		setRotationFromMatrix: function ( m ) {

			// assumes the upper 3x3 of m is a pure rotation matrix (i.e, unscaled)

			this.quaternion.setFromRotationMatrix( m );

		},

		setRotationFromQuaternion: function ( q ) {

			// assumes q is normalized

			this.quaternion.copy( q );

		},

		rotateOnAxis: function ( axis, angle ) {

			// rotate object on axis in object space
			// axis is assumed to be normalized

			_q1.setFromAxisAngle( axis, angle );

			this.quaternion.multiply( _q1 );

			return this;

		},

		rotateOnWorldAxis: function ( axis, angle ) {

			// rotate object on axis in world space
			// axis is assumed to be normalized
			// method assumes no rotated parent

			_q1.setFromAxisAngle( axis, angle );

			this.quaternion.premultiply( _q1 );

			return this;

		},

		rotateX: function ( angle ) {

			return this.rotateOnAxis( _xAxis, angle );

		},

		rotateY: function ( angle ) {

			return this.rotateOnAxis( _yAxis, angle );

		},

		rotateZ: function ( angle ) {

			return this.rotateOnAxis( _zAxis, angle );

		},

		translateOnAxis: function ( axis, distance ) {

			// translate object by distance along axis in object space
			// axis is assumed to be normalized

			_v1$2.copy( axis ).applyQuaternion( this.quaternion );

			this.position.add( _v1$2.multiplyScalar( distance ) );

			return this;

		},

		translateX: function ( distance ) {

			return this.translateOnAxis( _xAxis, distance );

		},

		translateY: function ( distance ) {

			return this.translateOnAxis( _yAxis, distance );

		},

		translateZ: function ( distance ) {

			return this.translateOnAxis( _zAxis, distance );

		},

		localToWorld: function ( vector ) {

			return vector.applyMatrix4( this.matrixWorld );

		},

		worldToLocal: function ( vector ) {

			return vector.applyMatrix4( _m1$1.copy( this.matrixWorld ).invert() );

		},

		lookAt: function ( x, y, z ) {

			// This method does not support objects having non-uniformly-scaled parent(s)

			if ( x.isVector3 ) {

				_target.copy( x );

			} else {

				_target.set( x, y, z );

			}

			const parent = this.parent;

			this.updateWorldMatrix( true, false );

			_position.setFromMatrixPosition( this.matrixWorld );

			if ( this.isCamera || this.isLight ) {

				_m1$1.lookAt( _position, _target, this.up );

			} else {

				_m1$1.lookAt( _target, _position, this.up );

			}

			this.quaternion.setFromRotationMatrix( _m1$1 );

			if ( parent ) {

				_m1$1.extractRotation( parent.matrixWorld );
				_q1.setFromRotationMatrix( _m1$1 );
				this.quaternion.premultiply( _q1.invert() );

			}

		},

		add: function ( object ) {

			if ( arguments.length > 1 ) {

				for ( let i = 0; i < arguments.length; i ++ ) {

					this.add( arguments[ i ] );

				}

				return this;

			}

			if ( object === this ) {

				console.error( 'THREE.Object3D.add: object can\'t be added as a child of itself.', object );
				return this;

			}

			if ( object && object.isObject3D ) {

				if ( object.parent !== null ) {

					object.parent.remove( object );

				}

				object.parent = this;
				this.children.push( object );

				object.dispatchEvent( _addedEvent );

			} else {

				console.error( 'THREE.Object3D.add: object not an instance of THREE.Object3D.', object );

			}

			return this;

		},

		remove: function ( object ) {

			if ( arguments.length > 1 ) {

				for ( let i = 0; i < arguments.length; i ++ ) {

					this.remove( arguments[ i ] );

				}

				return this;

			}

			const index = this.children.indexOf( object );

			if ( index !== - 1 ) {

				object.parent = null;
				this.children.splice( index, 1 );

				object.dispatchEvent( _removedEvent );

			}

			return this;

		},

		clear: function () {

			for ( let i = 0; i < this.children.length; i ++ ) {

				const object = this.children[ i ];

				object.parent = null;

				object.dispatchEvent( _removedEvent );

			}

			this.children.length = 0;

			return this;


		},

		attach: function ( object ) {

			// adds object as a child of this, while maintaining the object's world transform

			this.updateWorldMatrix( true, false );

			_m1$1.copy( this.matrixWorld ).invert();

			if ( object.parent !== null ) {

				object.parent.updateWorldMatrix( true, false );

				_m1$1.multiply( object.parent.matrixWorld );

			}

			object.applyMatrix4( _m1$1 );

			object.updateWorldMatrix( false, false );

			this.add( object );

			return this;

		},

		getObjectById: function ( id ) {

			return this.getObjectByProperty( 'id', id );

		},

		getObjectByName: function ( name ) {

			return this.getObjectByProperty( 'name', name );

		},

		getObjectByProperty: function ( name, value ) {

			if ( this[ name ] === value ) return this;

			for ( let i = 0, l = this.children.length; i < l; i ++ ) {

				const child = this.children[ i ];
				const object = child.getObjectByProperty( name, value );

				if ( object !== undefined ) {

					return object;

				}

			}

			return undefined;

		},

		getWorldPosition: function ( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Object3D: .getWorldPosition() target is now required' );
				target = new Vector3();

			}

			this.updateWorldMatrix( true, false );

			return target.setFromMatrixPosition( this.matrixWorld );

		},

		getWorldQuaternion: function ( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Object3D: .getWorldQuaternion() target is now required' );
				target = new Quaternion();

			}

			this.updateWorldMatrix( true, false );

			this.matrixWorld.decompose( _position, target, _scale );

			return target;

		},

		getWorldScale: function ( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Object3D: .getWorldScale() target is now required' );
				target = new Vector3();

			}

			this.updateWorldMatrix( true, false );

			this.matrixWorld.decompose( _position, _quaternion$2, target );

			return target;

		},

		getWorldDirection: function ( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Object3D: .getWorldDirection() target is now required' );
				target = new Vector3();

			}

			this.updateWorldMatrix( true, false );

			const e = this.matrixWorld.elements;

			return target.set( e[ 8 ], e[ 9 ], e[ 10 ] ).normalize();

		},

		raycast: function () {},

		traverse: function ( callback ) {

			callback( this );

			const children = this.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				children[ i ].traverse( callback );

			}

		},

		traverseVisible: function ( callback ) {

			if ( this.visible === false ) return;

			callback( this );

			const children = this.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				children[ i ].traverseVisible( callback );

			}

		},

		traverseAncestors: function ( callback ) {

			const parent = this.parent;

			if ( parent !== null ) {

				callback( parent );

				parent.traverseAncestors( callback );

			}

		},

		updateMatrix: function () {

			this.matrix.compose( this.position, this.quaternion, this.scale );

			this.matrixWorldNeedsUpdate = true;

		},

		updateMatrixWorld: function ( force ) {

			if ( this.matrixAutoUpdate ) this.updateMatrix();

			if ( this.matrixWorldNeedsUpdate || force ) {

				if ( this.parent === null ) {

					this.matrixWorld.copy( this.matrix );

				} else {

					this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );

				}

				this.matrixWorldNeedsUpdate = false;

				force = true;

			}

			// update children

			const children = this.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				children[ i ].updateMatrixWorld( force );

			}

		},

		updateWorldMatrix: function ( updateParents, updateChildren ) {

			const parent = this.parent;

			if ( updateParents === true && parent !== null ) {

				parent.updateWorldMatrix( true, false );

			}

			if ( this.matrixAutoUpdate ) this.updateMatrix();

			if ( this.parent === null ) {

				this.matrixWorld.copy( this.matrix );

			} else {

				this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );

			}

			// update children

			if ( updateChildren === true ) {

				const children = this.children;

				for ( let i = 0, l = children.length; i < l; i ++ ) {

					children[ i ].updateWorldMatrix( false, true );

				}

			}

		},

		toJSON: function ( meta ) {

			// meta is a string when called from JSON.stringify
			const isRootObject = ( meta === undefined || typeof meta === 'string' );

			const output = {};

			// meta is a hash used to collect geometries, materials.
			// not providing it implies that this is the root object
			// being serialized.
			if ( isRootObject ) {

				// initialize meta obj
				meta = {
					geometries: {},
					materials: {},
					textures: {},
					images: {},
					shapes: {},
					skeletons: {},
					animations: {}
				};

				output.metadata = {
					version: 4.5,
					type: 'Object',
					generator: 'Object3D.toJSON'
				};

			}

			// standard Object3D serialization

			const object = {};

			object.uuid = this.uuid;
			object.type = this.type;

			if ( this.name !== '' ) object.name = this.name;
			if ( this.castShadow === true ) object.castShadow = true;
			if ( this.receiveShadow === true ) object.receiveShadow = true;
			if ( this.visible === false ) object.visible = false;
			if ( this.frustumCulled === false ) object.frustumCulled = false;
			if ( this.renderOrder !== 0 ) object.renderOrder = this.renderOrder;
			if ( JSON.stringify( this.userData ) !== '{}' ) object.userData = this.userData;

			object.layers = this.layers.mask;
			object.matrix = this.matrix.toArray();

			if ( this.matrixAutoUpdate === false ) object.matrixAutoUpdate = false;

			// object specific properties

			if ( this.isInstancedMesh ) {

				object.type = 'InstancedMesh';
				object.count = this.count;
				object.instanceMatrix = this.instanceMatrix.toJSON();

			}

			//

			function serialize( library, element ) {

				if ( library[ element.uuid ] === undefined ) {

					library[ element.uuid ] = element.toJSON( meta );

				}

				return element.uuid;

			}

			if ( this.isMesh || this.isLine || this.isPoints ) {

				object.geometry = serialize( meta.geometries, this.geometry );

				const parameters = this.geometry.parameters;

				if ( parameters !== undefined && parameters.shapes !== undefined ) {

					const shapes = parameters.shapes;

					if ( Array.isArray( shapes ) ) {

						for ( let i = 0, l = shapes.length; i < l; i ++ ) {

							const shape = shapes[ i ];

							serialize( meta.shapes, shape );

						}

					} else {

						serialize( meta.shapes, shapes );

					}

				}

			}

			if ( this.isSkinnedMesh ) {

				object.bindMode = this.bindMode;
				object.bindMatrix = this.bindMatrix.toArray();

				if ( this.skeleton !== undefined ) {

					serialize( meta.skeletons, this.skeleton );

					object.skeleton = this.skeleton.uuid;

				}

			}

			if ( this.material !== undefined ) {

				if ( Array.isArray( this.material ) ) {

					const uuids = [];

					for ( let i = 0, l = this.material.length; i < l; i ++ ) {

						uuids.push( serialize( meta.materials, this.material[ i ] ) );

					}

					object.material = uuids;

				} else {

					object.material = serialize( meta.materials, this.material );

				}

			}

			//

			if ( this.children.length > 0 ) {

				object.children = [];

				for ( let i = 0; i < this.children.length; i ++ ) {

					object.children.push( this.children[ i ].toJSON( meta ).object );

				}

			}

			//

			if ( this.animations.length > 0 ) {

				object.animations = [];

				for ( let i = 0; i < this.animations.length; i ++ ) {

					const animation = this.animations[ i ];

					object.animations.push( serialize( meta.animations, animation ) );

				}

			}

			if ( isRootObject ) {

				const geometries = extractFromCache( meta.geometries );
				const materials = extractFromCache( meta.materials );
				const textures = extractFromCache( meta.textures );
				const images = extractFromCache( meta.images );
				const shapes = extractFromCache( meta.shapes );
				const skeletons = extractFromCache( meta.skeletons );
				const animations = extractFromCache( meta.animations );

				if ( geometries.length > 0 ) output.geometries = geometries;
				if ( materials.length > 0 ) output.materials = materials;
				if ( textures.length > 0 ) output.textures = textures;
				if ( images.length > 0 ) output.images = images;
				if ( shapes.length > 0 ) output.shapes = shapes;
				if ( skeletons.length > 0 ) output.skeletons = skeletons;
				if ( animations.length > 0 ) output.animations = animations;

			}

			output.object = object;

			return output;

			// extract data from the cache hash
			// remove metadata on each item
			// and return as array
			function extractFromCache( cache ) {

				const values = [];
				for ( const key in cache ) {

					const data = cache[ key ];
					delete data.metadata;
					values.push( data );

				}

				return values;

			}

		},

		clone: function ( recursive ) {

			return new this.constructor().copy( this, recursive );

		},

		copy: function ( source, recursive = true ) {

			this.name = source.name;

			this.up.copy( source.up );

			this.position.copy( source.position );
			this.rotation.order = source.rotation.order;
			this.quaternion.copy( source.quaternion );
			this.scale.copy( source.scale );

			this.matrix.copy( source.matrix );
			this.matrixWorld.copy( source.matrixWorld );

			this.matrixAutoUpdate = source.matrixAutoUpdate;
			this.matrixWorldNeedsUpdate = source.matrixWorldNeedsUpdate;

			this.layers.mask = source.layers.mask;
			this.visible = source.visible;

			this.castShadow = source.castShadow;
			this.receiveShadow = source.receiveShadow;

			this.frustumCulled = source.frustumCulled;
			this.renderOrder = source.renderOrder;

			this.userData = JSON.parse( JSON.stringify( source.userData ) );

			if ( recursive === true ) {

				for ( let i = 0; i < source.children.length; i ++ ) {

					const child = source.children[ i ];
					this.add( child.clone() );

				}

			}

			return this;

		}

	} );

	const _vector1 = /*@__PURE__*/ new Vector3();
	const _vector2 = /*@__PURE__*/ new Vector3();
	const _normalMatrix = /*@__PURE__*/ new Matrix3();

	class Plane {

		constructor( normal, constant ) {

			Object.defineProperty( this, 'isPlane', { value: true } );

			// normal is assumed to be normalized

			this.normal = ( normal !== undefined ) ? normal : new Vector3( 1, 0, 0 );
			this.constant = ( constant !== undefined ) ? constant : 0;

		}

		set( normal, constant ) {

			this.normal.copy( normal );
			this.constant = constant;

			return this;

		}

		setComponents( x, y, z, w ) {

			this.normal.set( x, y, z );
			this.constant = w;

			return this;

		}

		setFromNormalAndCoplanarPoint( normal, point ) {

			this.normal.copy( normal );
			this.constant = - point.dot( this.normal );

			return this;

		}

		setFromCoplanarPoints( a, b, c ) {

			const normal = _vector1.subVectors( c, b ).cross( _vector2.subVectors( a, b ) ).normalize();

			// Q: should an error be thrown if normal is zero (e.g. degenerate plane)?

			this.setFromNormalAndCoplanarPoint( normal, a );

			return this;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( plane ) {

			this.normal.copy( plane.normal );
			this.constant = plane.constant;

			return this;

		}

		normalize() {

			// Note: will lead to a divide by zero if the plane is invalid.

			const inverseNormalLength = 1.0 / this.normal.length();
			this.normal.multiplyScalar( inverseNormalLength );
			this.constant *= inverseNormalLength;

			return this;

		}

		negate() {

			this.constant *= - 1;
			this.normal.negate();

			return this;

		}

		distanceToPoint( point ) {

			return this.normal.dot( point ) + this.constant;

		}

		distanceToSphere( sphere ) {

			return this.distanceToPoint( sphere.center ) - sphere.radius;

		}

		projectPoint( point, target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Plane: .projectPoint() target is now required' );
				target = new Vector3();

			}

			return target.copy( this.normal ).multiplyScalar( - this.distanceToPoint( point ) ).add( point );

		}

		intersectLine( line, target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Plane: .intersectLine() target is now required' );
				target = new Vector3();

			}

			const direction = line.delta( _vector1 );

			const denominator = this.normal.dot( direction );

			if ( denominator === 0 ) {

				// line is coplanar, return origin
				if ( this.distanceToPoint( line.start ) === 0 ) {

					return target.copy( line.start );

				}

				// Unsure if this is the correct method to handle this case.
				return undefined;

			}

			const t = - ( line.start.dot( this.normal ) + this.constant ) / denominator;

			if ( t < 0 || t > 1 ) {

				return undefined;

			}

			return target.copy( direction ).multiplyScalar( t ).add( line.start );

		}

		intersectsLine( line ) {

			// Note: this tests if a line intersects the plane, not whether it (or its end-points) are coplanar with it.

			const startSign = this.distanceToPoint( line.start );
			const endSign = this.distanceToPoint( line.end );

			return ( startSign < 0 && endSign > 0 ) || ( endSign < 0 && startSign > 0 );

		}

		intersectsBox( box ) {

			return box.intersectsPlane( this );

		}

		intersectsSphere( sphere ) {

			return sphere.intersectsPlane( this );

		}

		coplanarPoint( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Plane: .coplanarPoint() target is now required' );
				target = new Vector3();

			}

			return target.copy( this.normal ).multiplyScalar( - this.constant );

		}

		applyMatrix4( matrix, optionalNormalMatrix ) {

			const normalMatrix = optionalNormalMatrix || _normalMatrix.getNormalMatrix( matrix );

			const referencePoint = this.coplanarPoint( _vector1 ).applyMatrix4( matrix );

			const normal = this.normal.applyMatrix3( normalMatrix ).normalize();

			this.constant = - referencePoint.dot( normal );

			return this;

		}

		translate( offset ) {

			this.constant -= offset.dot( this.normal );

			return this;

		}

		equals( plane ) {

			return plane.normal.equals( this.normal ) && ( plane.constant === this.constant );

		}

	}

	const _v0$1 = /*@__PURE__*/ new Vector3();
	const _v1$3 = /*@__PURE__*/ new Vector3();
	const _v2$1 = /*@__PURE__*/ new Vector3();
	const _v3 = /*@__PURE__*/ new Vector3();

	const _vab = /*@__PURE__*/ new Vector3();
	const _vac = /*@__PURE__*/ new Vector3();
	const _vbc = /*@__PURE__*/ new Vector3();
	const _vap = /*@__PURE__*/ new Vector3();
	const _vbp = /*@__PURE__*/ new Vector3();
	const _vcp = /*@__PURE__*/ new Vector3();

	class Triangle {

		constructor( a, b, c ) {

			this.a = ( a !== undefined ) ? a : new Vector3();
			this.b = ( b !== undefined ) ? b : new Vector3();
			this.c = ( c !== undefined ) ? c : new Vector3();

		}

		static getNormal( a, b, c, target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Triangle: .getNormal() target is now required' );
				target = new Vector3();

			}

			target.subVectors( c, b );
			_v0$1.subVectors( a, b );
			target.cross( _v0$1 );

			const targetLengthSq = target.lengthSq();
			if ( targetLengthSq > 0 ) {

				return target.multiplyScalar( 1 / Math.sqrt( targetLengthSq ) );

			}

			return target.set( 0, 0, 0 );

		}

		// static/instance method to calculate barycentric coordinates
		// based on: http://www.blackpawn.com/texts/pointinpoly/default.html
		static getBarycoord( point, a, b, c, target ) {

			_v0$1.subVectors( c, a );
			_v1$3.subVectors( b, a );
			_v2$1.subVectors( point, a );

			const dot00 = _v0$1.dot( _v0$1 );
			const dot01 = _v0$1.dot( _v1$3 );
			const dot02 = _v0$1.dot( _v2$1 );
			const dot11 = _v1$3.dot( _v1$3 );
			const dot12 = _v1$3.dot( _v2$1 );

			const denom = ( dot00 * dot11 - dot01 * dot01 );

			if ( target === undefined ) {

				console.warn( 'THREE.Triangle: .getBarycoord() target is now required' );
				target = new Vector3();

			}

			// collinear or singular triangle
			if ( denom === 0 ) {

				// arbitrary location outside of triangle?
				// not sure if this is the best idea, maybe should be returning undefined
				return target.set( - 2, - 1, - 1 );

			}

			const invDenom = 1 / denom;
			const u = ( dot11 * dot02 - dot01 * dot12 ) * invDenom;
			const v = ( dot00 * dot12 - dot01 * dot02 ) * invDenom;

			// barycentric coordinates must always sum to 1
			return target.set( 1 - u - v, v, u );

		}

		static containsPoint( point, a, b, c ) {

			this.getBarycoord( point, a, b, c, _v3 );

			return ( _v3.x >= 0 ) && ( _v3.y >= 0 ) && ( ( _v3.x + _v3.y ) <= 1 );

		}

		static getUV( point, p1, p2, p3, uv1, uv2, uv3, target ) {

			this.getBarycoord( point, p1, p2, p3, _v3 );

			target.set( 0, 0 );
			target.addScaledVector( uv1, _v3.x );
			target.addScaledVector( uv2, _v3.y );
			target.addScaledVector( uv3, _v3.z );

			return target;

		}

		static isFrontFacing( a, b, c, direction ) {

			_v0$1.subVectors( c, b );
			_v1$3.subVectors( a, b );

			// strictly front facing
			return ( _v0$1.cross( _v1$3 ).dot( direction ) < 0 ) ? true : false;

		}

		set( a, b, c ) {

			this.a.copy( a );
			this.b.copy( b );
			this.c.copy( c );

			return this;

		}

		setFromPointsAndIndices( points, i0, i1, i2 ) {

			this.a.copy( points[ i0 ] );
			this.b.copy( points[ i1 ] );
			this.c.copy( points[ i2 ] );

			return this;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( triangle ) {

			this.a.copy( triangle.a );
			this.b.copy( triangle.b );
			this.c.copy( triangle.c );

			return this;

		}

		getArea() {

			_v0$1.subVectors( this.c, this.b );
			_v1$3.subVectors( this.a, this.b );

			return _v0$1.cross( _v1$3 ).length() * 0.5;

		}

		getMidpoint( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Triangle: .getMidpoint() target is now required' );
				target = new Vector3();

			}

			return target.addVectors( this.a, this.b ).add( this.c ).multiplyScalar( 1 / 3 );

		}

		getNormal( target ) {

			return Triangle.getNormal( this.a, this.b, this.c, target );

		}

		getPlane( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Triangle: .getPlane() target is now required' );
				target = new Plane();

			}

			return target.setFromCoplanarPoints( this.a, this.b, this.c );

		}

		getBarycoord( point, target ) {

			return Triangle.getBarycoord( point, this.a, this.b, this.c, target );

		}

		getUV( point, uv1, uv2, uv3, target ) {

			return Triangle.getUV( point, this.a, this.b, this.c, uv1, uv2, uv3, target );

		}

		containsPoint( point ) {

			return Triangle.containsPoint( point, this.a, this.b, this.c );

		}

		isFrontFacing( direction ) {

			return Triangle.isFrontFacing( this.a, this.b, this.c, direction );

		}

		intersectsBox( box ) {

			return box.intersectsTriangle( this );

		}

		closestPointToPoint( p, target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Triangle: .closestPointToPoint() target is now required' );
				target = new Vector3();

			}

			const a = this.a, b = this.b, c = this.c;
			let v, w;

			// algorithm thanks to Real-Time Collision Detection by Christer Ericson,
			// published by Morgan Kaufmann Publishers, (c) 2005 Elsevier Inc.,
			// under the accompanying license; see chapter 5.1.5 for detailed explanation.
			// basically, we're distinguishing which of the voronoi regions of the triangle
			// the point lies in with the minimum amount of redundant computation.

			_vab.subVectors( b, a );
			_vac.subVectors( c, a );
			_vap.subVectors( p, a );
			const d1 = _vab.dot( _vap );
			const d2 = _vac.dot( _vap );
			if ( d1 <= 0 && d2 <= 0 ) {

				// vertex region of A; barycentric coords (1, 0, 0)
				return target.copy( a );

			}

			_vbp.subVectors( p, b );
			const d3 = _vab.dot( _vbp );
			const d4 = _vac.dot( _vbp );
			if ( d3 >= 0 && d4 <= d3 ) {

				// vertex region of B; barycentric coords (0, 1, 0)
				return target.copy( b );

			}

			const vc = d1 * d4 - d3 * d2;
			if ( vc <= 0 && d1 >= 0 && d3 <= 0 ) {

				v = d1 / ( d1 - d3 );
				// edge region of AB; barycentric coords (1-v, v, 0)
				return target.copy( a ).addScaledVector( _vab, v );

			}

			_vcp.subVectors( p, c );
			const d5 = _vab.dot( _vcp );
			const d6 = _vac.dot( _vcp );
			if ( d6 >= 0 && d5 <= d6 ) {

				// vertex region of C; barycentric coords (0, 0, 1)
				return target.copy( c );

			}

			const vb = d5 * d2 - d1 * d6;
			if ( vb <= 0 && d2 >= 0 && d6 <= 0 ) {

				w = d2 / ( d2 - d6 );
				// edge region of AC; barycentric coords (1-w, 0, w)
				return target.copy( a ).addScaledVector( _vac, w );

			}

			const va = d3 * d6 - d5 * d4;
			if ( va <= 0 && ( d4 - d3 ) >= 0 && ( d5 - d6 ) >= 0 ) {

				_vbc.subVectors( c, b );
				w = ( d4 - d3 ) / ( ( d4 - d3 ) + ( d5 - d6 ) );
				// edge region of BC; barycentric coords (0, 1-w, w)
				return target.copy( b ).addScaledVector( _vbc, w ); // edge region of BC

			}

			// face region
			const denom = 1 / ( va + vb + vc );
			// u = va * denom
			v = vb * denom;
			w = vc * denom;

			return target.copy( a ).addScaledVector( _vab, v ).addScaledVector( _vac, w );

		}

		equals( triangle ) {

			return triangle.a.equals( this.a ) && triangle.b.equals( this.b ) && triangle.c.equals( this.c );

		}

	}

	const _colorKeywords = { 'aliceblue': 0xF0F8FF, 'antiquewhite': 0xFAEBD7, 'aqua': 0x00FFFF, 'aquamarine': 0x7FFFD4, 'azure': 0xF0FFFF,
		'beige': 0xF5F5DC, 'bisque': 0xFFE4C4, 'black': 0x000000, 'blanchedalmond': 0xFFEBCD, 'blue': 0x0000FF, 'blueviolet': 0x8A2BE2,
		'brown': 0xA52A2A, 'burlywood': 0xDEB887, 'cadetblue': 0x5F9EA0, 'chartreuse': 0x7FFF00, 'chocolate': 0xD2691E, 'coral': 0xFF7F50,
		'cornflowerblue': 0x6495ED, 'cornsilk': 0xFFF8DC, 'crimson': 0xDC143C, 'cyan': 0x00FFFF, 'darkblue': 0x00008B, 'darkcyan': 0x008B8B,
		'darkgoldenrod': 0xB8860B, 'darkgray': 0xA9A9A9, 'darkgreen': 0x006400, 'darkgrey': 0xA9A9A9, 'darkkhaki': 0xBDB76B, 'darkmagenta': 0x8B008B,
		'darkolivegreen': 0x556B2F, 'darkorange': 0xFF8C00, 'darkorchid': 0x9932CC, 'darkred': 0x8B0000, 'darksalmon': 0xE9967A, 'darkseagreen': 0x8FBC8F,
		'darkslateblue': 0x483D8B, 'darkslategray': 0x2F4F4F, 'darkslategrey': 0x2F4F4F, 'darkturquoise': 0x00CED1, 'darkviolet': 0x9400D3,
		'deeppink': 0xFF1493, 'deepskyblue': 0x00BFFF, 'dimgray': 0x696969, 'dimgrey': 0x696969, 'dodgerblue': 0x1E90FF, 'firebrick': 0xB22222,
		'floralwhite': 0xFFFAF0, 'forestgreen': 0x228B22, 'fuchsia': 0xFF00FF, 'gainsboro': 0xDCDCDC, 'ghostwhite': 0xF8F8FF, 'gold': 0xFFD700,
		'goldenrod': 0xDAA520, 'gray': 0x808080, 'green': 0x008000, 'greenyellow': 0xADFF2F, 'grey': 0x808080, 'honeydew': 0xF0FFF0, 'hotpink': 0xFF69B4,
		'indianred': 0xCD5C5C, 'indigo': 0x4B0082, 'ivory': 0xFFFFF0, 'khaki': 0xF0E68C, 'lavender': 0xE6E6FA, 'lavenderblush': 0xFFF0F5, 'lawngreen': 0x7CFC00,
		'lemonchiffon': 0xFFFACD, 'lightblue': 0xADD8E6, 'lightcoral': 0xF08080, 'lightcyan': 0xE0FFFF, 'lightgoldenrodyellow': 0xFAFAD2, 'lightgray': 0xD3D3D3,
		'lightgreen': 0x90EE90, 'lightgrey': 0xD3D3D3, 'lightpink': 0xFFB6C1, 'lightsalmon': 0xFFA07A, 'lightseagreen': 0x20B2AA, 'lightskyblue': 0x87CEFA,
		'lightslategray': 0x778899, 'lightslategrey': 0x778899, 'lightsteelblue': 0xB0C4DE, 'lightyellow': 0xFFFFE0, 'lime': 0x00FF00, 'limegreen': 0x32CD32,
		'linen': 0xFAF0E6, 'magenta': 0xFF00FF, 'maroon': 0x800000, 'mediumaquamarine': 0x66CDAA, 'mediumblue': 0x0000CD, 'mediumorchid': 0xBA55D3,
		'mediumpurple': 0x9370DB, 'mediumseagreen': 0x3CB371, 'mediumslateblue': 0x7B68EE, 'mediumspringgreen': 0x00FA9A, 'mediumturquoise': 0x48D1CC,
		'mediumvioletred': 0xC71585, 'midnightblue': 0x191970, 'mintcream': 0xF5FFFA, 'mistyrose': 0xFFE4E1, 'moccasin': 0xFFE4B5, 'navajowhite': 0xFFDEAD,
		'navy': 0x000080, 'oldlace': 0xFDF5E6, 'olive': 0x808000, 'olivedrab': 0x6B8E23, 'orange': 0xFFA500, 'orangered': 0xFF4500, 'orchid': 0xDA70D6,
		'palegoldenrod': 0xEEE8AA, 'palegreen': 0x98FB98, 'paleturquoise': 0xAFEEEE, 'palevioletred': 0xDB7093, 'papayawhip': 0xFFEFD5, 'peachpuff': 0xFFDAB9,
		'peru': 0xCD853F, 'pink': 0xFFC0CB, 'plum': 0xDDA0DD, 'powderblue': 0xB0E0E6, 'purple': 0x800080, 'rebeccapurple': 0x663399, 'red': 0xFF0000, 'rosybrown': 0xBC8F8F,
		'royalblue': 0x4169E1, 'saddlebrown': 0x8B4513, 'salmon': 0xFA8072, 'sandybrown': 0xF4A460, 'seagreen': 0x2E8B57, 'seashell': 0xFFF5EE,
		'sienna': 0xA0522D, 'silver': 0xC0C0C0, 'skyblue': 0x87CEEB, 'slateblue': 0x6A5ACD, 'slategray': 0x708090, 'slategrey': 0x708090, 'snow': 0xFFFAFA,
		'springgreen': 0x00FF7F, 'steelblue': 0x4682B4, 'tan': 0xD2B48C, 'teal': 0x008080, 'thistle': 0xD8BFD8, 'tomato': 0xFF6347, 'turquoise': 0x40E0D0,
		'violet': 0xEE82EE, 'wheat': 0xF5DEB3, 'white': 0xFFFFFF, 'whitesmoke': 0xF5F5F5, 'yellow': 0xFFFF00, 'yellowgreen': 0x9ACD32 };

	const _hslA = { h: 0, s: 0, l: 0 };
	const _hslB = { h: 0, s: 0, l: 0 };

	function hue2rgb( p, q, t ) {

		if ( t < 0 ) t += 1;
		if ( t > 1 ) t -= 1;
		if ( t < 1 / 6 ) return p + ( q - p ) * 6 * t;
		if ( t < 1 / 2 ) return q;
		if ( t < 2 / 3 ) return p + ( q - p ) * 6 * ( 2 / 3 - t );
		return p;

	}

	function SRGBToLinear( c ) {

		return ( c < 0.04045 ) ? c * 0.0773993808 : Math.pow( c * 0.9478672986 + 0.0521327014, 2.4 );

	}

	function LinearToSRGB( c ) {

		return ( c < 0.0031308 ) ? c * 12.92 : 1.055 * ( Math.pow( c, 0.41666 ) ) - 0.055;

	}

	class Color {

		constructor( r, g, b ) {

			Object.defineProperty( this, 'isColor', { value: true } );

			if ( g === undefined && b === undefined ) {

				// r is THREE.Color, hex or string
				return this.set( r );

			}

			return this.setRGB( r, g, b );

		}

		set( value ) {

			if ( value && value.isColor ) {

				this.copy( value );

			} else if ( typeof value === 'number' ) {

				this.setHex( value );

			} else if ( typeof value === 'string' ) {

				this.setStyle( value );

			}

			return this;

		}

		setScalar( scalar ) {

			this.r = scalar;
			this.g = scalar;
			this.b = scalar;

			return this;

		}

		setHex( hex ) {

			hex = Math.floor( hex );

			this.r = ( hex >> 16 & 255 ) / 255;
			this.g = ( hex >> 8 & 255 ) / 255;
			this.b = ( hex & 255 ) / 255;

			return this;

		}

		setRGB( r, g, b ) {

			this.r = r;
			this.g = g;
			this.b = b;

			return this;

		}

		setHSL( h, s, l ) {

			// h,s,l ranges are in 0.0 - 1.0
			h = MathUtils.euclideanModulo( h, 1 );
			s = MathUtils.clamp( s, 0, 1 );
			l = MathUtils.clamp( l, 0, 1 );

			if ( s === 0 ) {

				this.r = this.g = this.b = l;

			} else {

				const p = l <= 0.5 ? l * ( 1 + s ) : l + s - ( l * s );
				const q = ( 2 * l ) - p;

				this.r = hue2rgb( q, p, h + 1 / 3 );
				this.g = hue2rgb( q, p, h );
				this.b = hue2rgb( q, p, h - 1 / 3 );

			}

			return this;

		}

		setStyle( style ) {

			function handleAlpha( string ) {

				if ( string === undefined ) return;

				if ( parseFloat( string ) < 1 ) {

					console.warn( 'THREE.Color: Alpha component of ' + style + ' will be ignored.' );

				}

			}


			let m;

			if ( m = /^((?:rgb|hsl)a?)\(\s*([^\)]*)\)/.exec( style ) ) {

				// rgb / hsl

				let color;
				const name = m[ 1 ];
				const components = m[ 2 ];

				switch ( name ) {

					case 'rgb':
					case 'rgba':

						if ( color = /^(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) {

							// rgb(255,0,0) rgba(255,0,0,0.5)
							this.r = Math.min( 255, parseInt( color[ 1 ], 10 ) ) / 255;
							this.g = Math.min( 255, parseInt( color[ 2 ], 10 ) ) / 255;
							this.b = Math.min( 255, parseInt( color[ 3 ], 10 ) ) / 255;

							handleAlpha( color[ 4 ] );

							return this;

						}

						if ( color = /^(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) {

							// rgb(100%,0%,0%) rgba(100%,0%,0%,0.5)
							this.r = Math.min( 100, parseInt( color[ 1 ], 10 ) ) / 100;
							this.g = Math.min( 100, parseInt( color[ 2 ], 10 ) ) / 100;
							this.b = Math.min( 100, parseInt( color[ 3 ], 10 ) ) / 100;

							handleAlpha( color[ 4 ] );

							return this;

						}

						break;

					case 'hsl':
					case 'hsla':

						if ( color = /^(\d*\.?\d+)\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec( components ) ) {

							// hsl(120,50%,50%) hsla(120,50%,50%,0.5)
							const h = parseFloat( color[ 1 ] ) / 360;
							const s = parseInt( color[ 2 ], 10 ) / 100;
							const l = parseInt( color[ 3 ], 10 ) / 100;

							handleAlpha( color[ 4 ] );

							return this.setHSL( h, s, l );

						}

						break;

				}

			} else if ( m = /^\#([A-Fa-f\d]+)$/.exec( style ) ) {

				// hex color

				const hex = m[ 1 ];
				const size = hex.length;

				if ( size === 3 ) {

					// #ff0
					this.r = parseInt( hex.charAt( 0 ) + hex.charAt( 0 ), 16 ) / 255;
					this.g = parseInt( hex.charAt( 1 ) + hex.charAt( 1 ), 16 ) / 255;
					this.b = parseInt( hex.charAt( 2 ) + hex.charAt( 2 ), 16 ) / 255;

					return this;

				} else if ( size === 6 ) {

					// #ff0000
					this.r = parseInt( hex.charAt( 0 ) + hex.charAt( 1 ), 16 ) / 255;
					this.g = parseInt( hex.charAt( 2 ) + hex.charAt( 3 ), 16 ) / 255;
					this.b = parseInt( hex.charAt( 4 ) + hex.charAt( 5 ), 16 ) / 255;

					return this;

				}

			}

			if ( style && style.length > 0 ) {

				return this.setColorName( style );

			}

			return this;

		}

		setColorName( style ) {

			// color keywords
			const hex = _colorKeywords[ style ];

			if ( hex !== undefined ) {

				// red
				this.setHex( hex );

			} else {

				// unknown color
				console.warn( 'THREE.Color: Unknown color ' + style );

			}

			return this;

		}

		clone() {

			return new this.constructor( this.r, this.g, this.b );

		}

		copy( color ) {

			this.r = color.r;
			this.g = color.g;
			this.b = color.b;

			return this;

		}

		copyGammaToLinear( color, gammaFactor = 2.0 ) {

			this.r = Math.pow( color.r, gammaFactor );
			this.g = Math.pow( color.g, gammaFactor );
			this.b = Math.pow( color.b, gammaFactor );

			return this;

		}

		copyLinearToGamma( color, gammaFactor = 2.0 ) {

			const safeInverse = ( gammaFactor > 0 ) ? ( 1.0 / gammaFactor ) : 1.0;

			this.r = Math.pow( color.r, safeInverse );
			this.g = Math.pow( color.g, safeInverse );
			this.b = Math.pow( color.b, safeInverse );

			return this;

		}

		convertGammaToLinear( gammaFactor ) {

			this.copyGammaToLinear( this, gammaFactor );

			return this;

		}

		convertLinearToGamma( gammaFactor ) {

			this.copyLinearToGamma( this, gammaFactor );

			return this;

		}

		copySRGBToLinear( color ) {

			this.r = SRGBToLinear( color.r );
			this.g = SRGBToLinear( color.g );
			this.b = SRGBToLinear( color.b );

			return this;

		}

		copyLinearToSRGB( color ) {

			this.r = LinearToSRGB( color.r );
			this.g = LinearToSRGB( color.g );
			this.b = LinearToSRGB( color.b );

			return this;

		}

		convertSRGBToLinear() {

			this.copySRGBToLinear( this );

			return this;

		}

		convertLinearToSRGB() {

			this.copyLinearToSRGB( this );

			return this;

		}

		getHex() {

			return ( this.r * 255 ) << 16 ^ ( this.g * 255 ) << 8 ^ ( this.b * 255 ) << 0;

		}

		getHexString() {

			return ( '000000' + this.getHex().toString( 16 ) ).slice( - 6 );

		}

		getHSL( target ) {

			// h,s,l ranges are in 0.0 - 1.0

			if ( target === undefined ) {

				console.warn( 'THREE.Color: .getHSL() target is now required' );
				target = { h: 0, s: 0, l: 0 };

			}

			const r = this.r, g = this.g, b = this.b;

			const max = Math.max( r, g, b );
			const min = Math.min( r, g, b );

			let hue, saturation;
			const lightness = ( min + max ) / 2.0;

			if ( min === max ) {

				hue = 0;
				saturation = 0;

			} else {

				const delta = max - min;

				saturation = lightness <= 0.5 ? delta / ( max + min ) : delta / ( 2 - max - min );

				switch ( max ) {

					case r: hue = ( g - b ) / delta + ( g < b ? 6 : 0 ); break;
					case g: hue = ( b - r ) / delta + 2; break;
					case b: hue = ( r - g ) / delta + 4; break;

				}

				hue /= 6;

			}

			target.h = hue;
			target.s = saturation;
			target.l = lightness;

			return target;

		}

		getStyle() {

			return 'rgb(' + ( ( this.r * 255 ) | 0 ) + ',' + ( ( this.g * 255 ) | 0 ) + ',' + ( ( this.b * 255 ) | 0 ) + ')';

		}

		offsetHSL( h, s, l ) {

			this.getHSL( _hslA );

			_hslA.h += h; _hslA.s += s; _hslA.l += l;

			this.setHSL( _hslA.h, _hslA.s, _hslA.l );

			return this;

		}

		add( color ) {

			this.r += color.r;
			this.g += color.g;
			this.b += color.b;

			return this;

		}

		addColors( color1, color2 ) {

			this.r = color1.r + color2.r;
			this.g = color1.g + color2.g;
			this.b = color1.b + color2.b;

			return this;

		}

		addScalar( s ) {

			this.r += s;
			this.g += s;
			this.b += s;

			return this;

		}

		sub( color ) {

			this.r = Math.max( 0, this.r - color.r );
			this.g = Math.max( 0, this.g - color.g );
			this.b = Math.max( 0, this.b - color.b );

			return this;

		}

		multiply( color ) {

			this.r *= color.r;
			this.g *= color.g;
			this.b *= color.b;

			return this;

		}

		multiplyScalar( s ) {

			this.r *= s;
			this.g *= s;
			this.b *= s;

			return this;

		}

		lerp( color, alpha ) {

			this.r += ( color.r - this.r ) * alpha;
			this.g += ( color.g - this.g ) * alpha;
			this.b += ( color.b - this.b ) * alpha;

			return this;

		}

		lerpHSL( color, alpha ) {

			this.getHSL( _hslA );
			color.getHSL( _hslB );

			const h = MathUtils.lerp( _hslA.h, _hslB.h, alpha );
			const s = MathUtils.lerp( _hslA.s, _hslB.s, alpha );
			const l = MathUtils.lerp( _hslA.l, _hslB.l, alpha );

			this.setHSL( h, s, l );

			return this;

		}

		equals( c ) {

			return ( c.r === this.r ) && ( c.g === this.g ) && ( c.b === this.b );

		}

		fromArray( array, offset = 0 ) {

			this.r = array[ offset ];
			this.g = array[ offset + 1 ];
			this.b = array[ offset + 2 ];

			return this;

		}

		toArray( array = [], offset = 0 ) {

			array[ offset ] = this.r;
			array[ offset + 1 ] = this.g;
			array[ offset + 2 ] = this.b;

			return array;

		}

		fromBufferAttribute( attribute, index ) {

			this.r = attribute.getX( index );
			this.g = attribute.getY( index );
			this.b = attribute.getZ( index );

			if ( attribute.normalized === true ) {

				// assuming Uint8Array

				this.r /= 255;
				this.g /= 255;
				this.b /= 255;

			}

			return this;

		}

		toJSON() {

			return this.getHex();

		}

	}

	Color.NAMES = _colorKeywords;
	Color.prototype.r = 1;
	Color.prototype.g = 1;
	Color.prototype.b = 1;

	class Face3 {

		constructor( a, b, c, normal, color, materialIndex = 0 ) {

			this.a = a;
			this.b = b;
			this.c = c;

			this.normal = ( normal && normal.isVector3 ) ? normal : new Vector3();
			this.vertexNormals = Array.isArray( normal ) ? normal : [];

			this.color = ( color && color.isColor ) ? color : new Color();
			this.vertexColors = Array.isArray( color ) ? color : [];

			this.materialIndex = materialIndex;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( source ) {

			this.a = source.a;
			this.b = source.b;
			this.c = source.c;

			this.normal.copy( source.normal );
			this.color.copy( source.color );

			this.materialIndex = source.materialIndex;

			for ( let i = 0, il = source.vertexNormals.length; i < il; i ++ ) {

				this.vertexNormals[ i ] = source.vertexNormals[ i ].clone();

			}

			for ( let i = 0, il = source.vertexColors.length; i < il; i ++ ) {

				this.vertexColors[ i ] = source.vertexColors[ i ].clone();

			}

			return this;

		}

	}

	let materialId = 0;

	function Material() {

		Object.defineProperty( this, 'id', { value: materialId ++ } );

		this.uuid = MathUtils.generateUUID();

		this.name = '';
		this.type = 'Material';

		this.fog = true;

		this.blending = NormalBlending;
		this.side = FrontSide;
		this.flatShading = false;
		this.vertexColors = false;

		this.opacity = 1;
		this.transparent = false;

		this.blendSrc = SrcAlphaFactor;
		this.blendDst = OneMinusSrcAlphaFactor;
		this.blendEquation = AddEquation;
		this.blendSrcAlpha = null;
		this.blendDstAlpha = null;
		this.blendEquationAlpha = null;

		this.depthFunc = LessEqualDepth;
		this.depthTest = true;
		this.depthWrite = true;

		this.stencilWriteMask = 0xff;
		this.stencilFunc = AlwaysStencilFunc;
		this.stencilRef = 0;
		this.stencilFuncMask = 0xff;
		this.stencilFail = KeepStencilOp;
		this.stencilZFail = KeepStencilOp;
		this.stencilZPass = KeepStencilOp;
		this.stencilWrite = false;

		this.clippingPlanes = null;
		this.clipIntersection = false;
		this.clipShadows = false;

		this.shadowSide = null;

		this.colorWrite = true;

		this.precision = null; // override the renderer's default precision for this material

		this.polygonOffset = false;
		this.polygonOffsetFactor = 0;
		this.polygonOffsetUnits = 0;

		this.dithering = false;

		this.alphaTest = 0;
		this.premultipliedAlpha = false;

		this.visible = true;

		this.toneMapped = true;

		this.userData = {};

		this.version = 0;

	}

	Material.prototype = Object.assign( Object.create( EventDispatcher$1.prototype ), {

		constructor: Material,

		isMaterial: true,

		onBeforeCompile: function ( /* shaderobject, renderer */ ) {},

		customProgramCacheKey: function () {

			return this.onBeforeCompile.toString();

		},

		setValues: function ( values ) {

			if ( values === undefined ) return;

			for ( const key in values ) {

				const newValue = values[ key ];

				if ( newValue === undefined ) {

					console.warn( 'THREE.Material: \'' + key + '\' parameter is undefined.' );
					continue;

				}

				// for backward compatability if shading is set in the constructor
				if ( key === 'shading' ) {

					console.warn( 'THREE.' + this.type + ': .shading has been removed. Use the boolean .flatShading instead.' );
					this.flatShading = ( newValue === FlatShading ) ? true : false;
					continue;

				}

				const currentValue = this[ key ];

				if ( currentValue === undefined ) {

					console.warn( 'THREE.' + this.type + ': \'' + key + '\' is not a property of this material.' );
					continue;

				}

				if ( currentValue && currentValue.isColor ) {

					currentValue.set( newValue );

				} else if ( ( currentValue && currentValue.isVector3 ) && ( newValue && newValue.isVector3 ) ) {

					currentValue.copy( newValue );

				} else {

					this[ key ] = newValue;

				}

			}

		},

		toJSON: function ( meta ) {

			const isRoot = ( meta === undefined || typeof meta === 'string' );

			if ( isRoot ) {

				meta = {
					textures: {},
					images: {}
				};

			}

			const data = {
				metadata: {
					version: 4.5,
					type: 'Material',
					generator: 'Material.toJSON'
				}
			};

			// standard Material serialization
			data.uuid = this.uuid;
			data.type = this.type;

			if ( this.name !== '' ) data.name = this.name;

			if ( this.color && this.color.isColor ) data.color = this.color.getHex();

			if ( this.roughness !== undefined ) data.roughness = this.roughness;
			if ( this.metalness !== undefined ) data.metalness = this.metalness;

			if ( this.sheen && this.sheen.isColor ) data.sheen = this.sheen.getHex();
			if ( this.emissive && this.emissive.isColor ) data.emissive = this.emissive.getHex();
			if ( this.emissiveIntensity && this.emissiveIntensity !== 1 ) data.emissiveIntensity = this.emissiveIntensity;

			if ( this.specular && this.specular.isColor ) data.specular = this.specular.getHex();
			if ( this.shininess !== undefined ) data.shininess = this.shininess;
			if ( this.clearcoat !== undefined ) data.clearcoat = this.clearcoat;
			if ( this.clearcoatRoughness !== undefined ) data.clearcoatRoughness = this.clearcoatRoughness;

			if ( this.clearcoatMap && this.clearcoatMap.isTexture ) {

				data.clearcoatMap = this.clearcoatMap.toJSON( meta ).uuid;

			}

			if ( this.clearcoatRoughnessMap && this.clearcoatRoughnessMap.isTexture ) {

				data.clearcoatRoughnessMap = this.clearcoatRoughnessMap.toJSON( meta ).uuid;

			}

			if ( this.clearcoatNormalMap && this.clearcoatNormalMap.isTexture ) {

				data.clearcoatNormalMap = this.clearcoatNormalMap.toJSON( meta ).uuid;
				data.clearcoatNormalScale = this.clearcoatNormalScale.toArray();

			}

			if ( this.map && this.map.isTexture ) data.map = this.map.toJSON( meta ).uuid;
			if ( this.matcap && this.matcap.isTexture ) data.matcap = this.matcap.toJSON( meta ).uuid;
			if ( this.alphaMap && this.alphaMap.isTexture ) data.alphaMap = this.alphaMap.toJSON( meta ).uuid;
			if ( this.lightMap && this.lightMap.isTexture ) data.lightMap = this.lightMap.toJSON( meta ).uuid;

			if ( this.aoMap && this.aoMap.isTexture ) {

				data.aoMap = this.aoMap.toJSON( meta ).uuid;
				data.aoMapIntensity = this.aoMapIntensity;

			}

			if ( this.bumpMap && this.bumpMap.isTexture ) {

				data.bumpMap = this.bumpMap.toJSON( meta ).uuid;
				data.bumpScale = this.bumpScale;

			}

			if ( this.normalMap && this.normalMap.isTexture ) {

				data.normalMap = this.normalMap.toJSON( meta ).uuid;
				data.normalMapType = this.normalMapType;
				data.normalScale = this.normalScale.toArray();

			}

			if ( this.displacementMap && this.displacementMap.isTexture ) {

				data.displacementMap = this.displacementMap.toJSON( meta ).uuid;
				data.displacementScale = this.displacementScale;
				data.displacementBias = this.displacementBias;

			}

			if ( this.roughnessMap && this.roughnessMap.isTexture ) data.roughnessMap = this.roughnessMap.toJSON( meta ).uuid;
			if ( this.metalnessMap && this.metalnessMap.isTexture ) data.metalnessMap = this.metalnessMap.toJSON( meta ).uuid;

			if ( this.emissiveMap && this.emissiveMap.isTexture ) data.emissiveMap = this.emissiveMap.toJSON( meta ).uuid;
			if ( this.specularMap && this.specularMap.isTexture ) data.specularMap = this.specularMap.toJSON( meta ).uuid;

			if ( this.envMap && this.envMap.isTexture ) {

				data.envMap = this.envMap.toJSON( meta ).uuid;
				data.reflectivity = this.reflectivity; // Scale behind envMap
				data.refractionRatio = this.refractionRatio;

				if ( this.combine !== undefined ) data.combine = this.combine;
				if ( this.envMapIntensity !== undefined ) data.envMapIntensity = this.envMapIntensity;

			}

			if ( this.gradientMap && this.gradientMap.isTexture ) {

				data.gradientMap = this.gradientMap.toJSON( meta ).uuid;

			}

			if ( this.size !== undefined ) data.size = this.size;
			if ( this.sizeAttenuation !== undefined ) data.sizeAttenuation = this.sizeAttenuation;

			if ( this.blending !== NormalBlending ) data.blending = this.blending;
			if ( this.flatShading === true ) data.flatShading = this.flatShading;
			if ( this.side !== FrontSide ) data.side = this.side;
			if ( this.vertexColors ) data.vertexColors = true;

			if ( this.opacity < 1 ) data.opacity = this.opacity;
			if ( this.transparent === true ) data.transparent = this.transparent;

			data.depthFunc = this.depthFunc;
			data.depthTest = this.depthTest;
			data.depthWrite = this.depthWrite;

			data.stencilWrite = this.stencilWrite;
			data.stencilWriteMask = this.stencilWriteMask;
			data.stencilFunc = this.stencilFunc;
			data.stencilRef = this.stencilRef;
			data.stencilFuncMask = this.stencilFuncMask;
			data.stencilFail = this.stencilFail;
			data.stencilZFail = this.stencilZFail;
			data.stencilZPass = this.stencilZPass;

			// rotation (SpriteMaterial)
			if ( this.rotation && this.rotation !== 0 ) data.rotation = this.rotation;

			if ( this.polygonOffset === true ) data.polygonOffset = true;
			if ( this.polygonOffsetFactor !== 0 ) data.polygonOffsetFactor = this.polygonOffsetFactor;
			if ( this.polygonOffsetUnits !== 0 ) data.polygonOffsetUnits = this.polygonOffsetUnits;

			if ( this.linewidth && this.linewidth !== 1 ) data.linewidth = this.linewidth;
			if ( this.dashSize !== undefined ) data.dashSize = this.dashSize;
			if ( this.gapSize !== undefined ) data.gapSize = this.gapSize;
			if ( this.scale !== undefined ) data.scale = this.scale;

			if ( this.dithering === true ) data.dithering = true;

			if ( this.alphaTest > 0 ) data.alphaTest = this.alphaTest;
			if ( this.premultipliedAlpha === true ) data.premultipliedAlpha = this.premultipliedAlpha;

			if ( this.wireframe === true ) data.wireframe = this.wireframe;
			if ( this.wireframeLinewidth > 1 ) data.wireframeLinewidth = this.wireframeLinewidth;
			if ( this.wireframeLinecap !== 'round' ) data.wireframeLinecap = this.wireframeLinecap;
			if ( this.wireframeLinejoin !== 'round' ) data.wireframeLinejoin = this.wireframeLinejoin;

			if ( this.morphTargets === true ) data.morphTargets = true;
			if ( this.morphNormals === true ) data.morphNormals = true;
			if ( this.skinning === true ) data.skinning = true;

			if ( this.visible === false ) data.visible = false;

			if ( this.toneMapped === false ) data.toneMapped = false;

			if ( JSON.stringify( this.userData ) !== '{}' ) data.userData = this.userData;

			// TODO: Copied from Object3D.toJSON

			function extractFromCache( cache ) {

				const values = [];

				for ( const key in cache ) {

					const data = cache[ key ];
					delete data.metadata;
					values.push( data );

				}

				return values;

			}

			if ( isRoot ) {

				const textures = extractFromCache( meta.textures );
				const images = extractFromCache( meta.images );

				if ( textures.length > 0 ) data.textures = textures;
				if ( images.length > 0 ) data.images = images;

			}

			return data;

		},

		clone: function () {

			return new this.constructor().copy( this );

		},

		copy: function ( source ) {

			this.name = source.name;

			this.fog = source.fog;

			this.blending = source.blending;
			this.side = source.side;
			this.flatShading = source.flatShading;
			this.vertexColors = source.vertexColors;

			this.opacity = source.opacity;
			this.transparent = source.transparent;

			this.blendSrc = source.blendSrc;
			this.blendDst = source.blendDst;
			this.blendEquation = source.blendEquation;
			this.blendSrcAlpha = source.blendSrcAlpha;
			this.blendDstAlpha = source.blendDstAlpha;
			this.blendEquationAlpha = source.blendEquationAlpha;

			this.depthFunc = source.depthFunc;
			this.depthTest = source.depthTest;
			this.depthWrite = source.depthWrite;

			this.stencilWriteMask = source.stencilWriteMask;
			this.stencilFunc = source.stencilFunc;
			this.stencilRef = source.stencilRef;
			this.stencilFuncMask = source.stencilFuncMask;
			this.stencilFail = source.stencilFail;
			this.stencilZFail = source.stencilZFail;
			this.stencilZPass = source.stencilZPass;
			this.stencilWrite = source.stencilWrite;

			const srcPlanes = source.clippingPlanes;
			let dstPlanes = null;

			if ( srcPlanes !== null ) {

				const n = srcPlanes.length;
				dstPlanes = new Array( n );

				for ( let i = 0; i !== n; ++ i ) {

					dstPlanes[ i ] = srcPlanes[ i ].clone();

				}

			}

			this.clippingPlanes = dstPlanes;
			this.clipIntersection = source.clipIntersection;
			this.clipShadows = source.clipShadows;

			this.shadowSide = source.shadowSide;

			this.colorWrite = source.colorWrite;

			this.precision = source.precision;

			this.polygonOffset = source.polygonOffset;
			this.polygonOffsetFactor = source.polygonOffsetFactor;
			this.polygonOffsetUnits = source.polygonOffsetUnits;

			this.dithering = source.dithering;

			this.alphaTest = source.alphaTest;
			this.premultipliedAlpha = source.premultipliedAlpha;

			this.visible = source.visible;

			this.toneMapped = source.toneMapped;

			this.userData = JSON.parse( JSON.stringify( source.userData ) );

			return this;

		},

		dispose: function () {

			this.dispatchEvent( { type: 'dispose' } );

		}

	} );

	Object.defineProperty( Material.prototype, 'needsUpdate', {

		set: function ( value ) {

			if ( value === true ) this.version ++;

		}

	} );

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  opacity: <float>,
	 *  map: new THREE.Texture( <Image> ),
	 *
	 *  lightMap: new THREE.Texture( <Image> ),
	 *  lightMapIntensity: <float>
	 *
	 *  aoMap: new THREE.Texture( <Image> ),
	 *  aoMapIntensity: <float>
	 *
	 *  specularMap: new THREE.Texture( <Image> ),
	 *
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *
	 *  envMap: new THREE.CubeTexture( [posx, negx, posy, negy, posz, negz] ),
	 *  combine: THREE.Multiply,
	 *  reflectivity: <float>,
	 *  refractionRatio: <float>,
	 *
	 *  depthTest: <bool>,
	 *  depthWrite: <bool>,
	 *
	 *  wireframe: <boolean>,
	 *  wireframeLinewidth: <float>,
	 *
	 *  skinning: <bool>,
	 *  morphTargets: <bool>
	 * }
	 */

	function MeshBasicMaterial( parameters ) {

		Material.call( this );

		this.type = 'MeshBasicMaterial';

		this.color = new Color( 0xffffff ); // emissive

		this.map = null;

		this.lightMap = null;
		this.lightMapIntensity = 1.0;

		this.aoMap = null;
		this.aoMapIntensity = 1.0;

		this.specularMap = null;

		this.alphaMap = null;

		this.envMap = null;
		this.combine = MultiplyOperation;
		this.reflectivity = 1;
		this.refractionRatio = 0.98;

		this.wireframe = false;
		this.wireframeLinewidth = 1;
		this.wireframeLinecap = 'round';
		this.wireframeLinejoin = 'round';

		this.skinning = false;
		this.morphTargets = false;

		this.setValues( parameters );

	}

	MeshBasicMaterial.prototype = Object.create( Material.prototype );
	MeshBasicMaterial.prototype.constructor = MeshBasicMaterial;

	MeshBasicMaterial.prototype.isMeshBasicMaterial = true;

	MeshBasicMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.color.copy( source.color );

		this.map = source.map;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.specularMap = source.specularMap;

		this.alphaMap = source.alphaMap;

		this.envMap = source.envMap;
		this.combine = source.combine;
		this.reflectivity = source.reflectivity;
		this.refractionRatio = source.refractionRatio;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.skinning = source.skinning;
		this.morphTargets = source.morphTargets;

		return this;

	};

	const _vector$3 = new Vector3();
	const _vector2$1 = new Vector2();

	function BufferAttribute( array, itemSize, normalized ) {

		if ( Array.isArray( array ) ) {

			throw new TypeError( 'THREE.BufferAttribute: array should be a Typed Array.' );

		}

		this.name = '';

		this.array = array;
		this.itemSize = itemSize;
		this.count = array !== undefined ? array.length / itemSize : 0;
		this.normalized = normalized === true;

		this.usage = StaticDrawUsage;
		this.updateRange = { offset: 0, count: - 1 };

		this.version = 0;

	}

	Object.defineProperty( BufferAttribute.prototype, 'needsUpdate', {

		set: function ( value ) {

			if ( value === true ) this.version ++;

		}

	} );

	Object.assign( BufferAttribute.prototype, {

		isBufferAttribute: true,

		onUploadCallback: function () {},

		setUsage: function ( value ) {

			this.usage = value;

			return this;

		},

		copy: function ( source ) {

			this.name = source.name;
			this.array = new source.array.constructor( source.array );
			this.itemSize = source.itemSize;
			this.count = source.count;
			this.normalized = source.normalized;

			this.usage = source.usage;

			return this;

		},

		copyAt: function ( index1, attribute, index2 ) {

			index1 *= this.itemSize;
			index2 *= attribute.itemSize;

			for ( let i = 0, l = this.itemSize; i < l; i ++ ) {

				this.array[ index1 + i ] = attribute.array[ index2 + i ];

			}

			return this;

		},

		copyArray: function ( array ) {

			this.array.set( array );

			return this;

		},

		copyColorsArray: function ( colors ) {

			const array = this.array;
			let offset = 0;

			for ( let i = 0, l = colors.length; i < l; i ++ ) {

				let color = colors[ i ];

				if ( color === undefined ) {

					console.warn( 'THREE.BufferAttribute.copyColorsArray(): color is undefined', i );
					color = new Color();

				}

				array[ offset ++ ] = color.r;
				array[ offset ++ ] = color.g;
				array[ offset ++ ] = color.b;

			}

			return this;

		},

		copyVector2sArray: function ( vectors ) {

			const array = this.array;
			let offset = 0;

			for ( let i = 0, l = vectors.length; i < l; i ++ ) {

				let vector = vectors[ i ];

				if ( vector === undefined ) {

					console.warn( 'THREE.BufferAttribute.copyVector2sArray(): vector is undefined', i );
					vector = new Vector2();

				}

				array[ offset ++ ] = vector.x;
				array[ offset ++ ] = vector.y;

			}

			return this;

		},

		copyVector3sArray: function ( vectors ) {

			const array = this.array;
			let offset = 0;

			for ( let i = 0, l = vectors.length; i < l; i ++ ) {

				let vector = vectors[ i ];

				if ( vector === undefined ) {

					console.warn( 'THREE.BufferAttribute.copyVector3sArray(): vector is undefined', i );
					vector = new Vector3();

				}

				array[ offset ++ ] = vector.x;
				array[ offset ++ ] = vector.y;
				array[ offset ++ ] = vector.z;

			}

			return this;

		},

		copyVector4sArray: function ( vectors ) {

			const array = this.array;
			let offset = 0;

			for ( let i = 0, l = vectors.length; i < l; i ++ ) {

				let vector = vectors[ i ];

				if ( vector === undefined ) {

					console.warn( 'THREE.BufferAttribute.copyVector4sArray(): vector is undefined', i );
					vector = new Vector4();

				}

				array[ offset ++ ] = vector.x;
				array[ offset ++ ] = vector.y;
				array[ offset ++ ] = vector.z;
				array[ offset ++ ] = vector.w;

			}

			return this;

		},

		applyMatrix3: function ( m ) {

			if ( this.itemSize === 2 ) {

				for ( let i = 0, l = this.count; i < l; i ++ ) {

					_vector2$1.fromBufferAttribute( this, i );
					_vector2$1.applyMatrix3( m );

					this.setXY( i, _vector2$1.x, _vector2$1.y );

				}

			} else if ( this.itemSize === 3 ) {

				for ( let i = 0, l = this.count; i < l; i ++ ) {

					_vector$3.fromBufferAttribute( this, i );
					_vector$3.applyMatrix3( m );

					this.setXYZ( i, _vector$3.x, _vector$3.y, _vector$3.z );

				}

			}

			return this;

		},

		applyMatrix4: function ( m ) {

			for ( let i = 0, l = this.count; i < l; i ++ ) {

				_vector$3.x = this.getX( i );
				_vector$3.y = this.getY( i );
				_vector$3.z = this.getZ( i );

				_vector$3.applyMatrix4( m );

				this.setXYZ( i, _vector$3.x, _vector$3.y, _vector$3.z );

			}

			return this;

		},

		applyNormalMatrix: function ( m ) {

			for ( let i = 0, l = this.count; i < l; i ++ ) {

				_vector$3.x = this.getX( i );
				_vector$3.y = this.getY( i );
				_vector$3.z = this.getZ( i );

				_vector$3.applyNormalMatrix( m );

				this.setXYZ( i, _vector$3.x, _vector$3.y, _vector$3.z );

			}

			return this;

		},

		transformDirection: function ( m ) {

			for ( let i = 0, l = this.count; i < l; i ++ ) {

				_vector$3.x = this.getX( i );
				_vector$3.y = this.getY( i );
				_vector$3.z = this.getZ( i );

				_vector$3.transformDirection( m );

				this.setXYZ( i, _vector$3.x, _vector$3.y, _vector$3.z );

			}

			return this;

		},

		set: function ( value, offset = 0 ) {

			this.array.set( value, offset );

			return this;

		},

		getX: function ( index ) {

			return this.array[ index * this.itemSize ];

		},

		setX: function ( index, x ) {

			this.array[ index * this.itemSize ] = x;

			return this;

		},

		getY: function ( index ) {

			return this.array[ index * this.itemSize + 1 ];

		},

		setY: function ( index, y ) {

			this.array[ index * this.itemSize + 1 ] = y;

			return this;

		},

		getZ: function ( index ) {

			return this.array[ index * this.itemSize + 2 ];

		},

		setZ: function ( index, z ) {

			this.array[ index * this.itemSize + 2 ] = z;

			return this;

		},

		getW: function ( index ) {

			return this.array[ index * this.itemSize + 3 ];

		},

		setW: function ( index, w ) {

			this.array[ index * this.itemSize + 3 ] = w;

			return this;

		},

		setXY: function ( index, x, y ) {

			index *= this.itemSize;

			this.array[ index + 0 ] = x;
			this.array[ index + 1 ] = y;

			return this;

		},

		setXYZ: function ( index, x, y, z ) {

			index *= this.itemSize;

			this.array[ index + 0 ] = x;
			this.array[ index + 1 ] = y;
			this.array[ index + 2 ] = z;

			return this;

		},

		setXYZW: function ( index, x, y, z, w ) {

			index *= this.itemSize;

			this.array[ index + 0 ] = x;
			this.array[ index + 1 ] = y;
			this.array[ index + 2 ] = z;
			this.array[ index + 3 ] = w;

			return this;

		},

		onUpload: function ( callback ) {

			this.onUploadCallback = callback;

			return this;

		},

		clone: function () {

			return new this.constructor( this.array, this.itemSize ).copy( this );

		},

		toJSON: function () {

			return {
				itemSize: this.itemSize,
				type: this.array.constructor.name,
				array: Array.prototype.slice.call( this.array ),
				normalized: this.normalized
			};

		}

	} );

	//

	function Int8BufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Int8Array( array ), itemSize, normalized );

	}

	Int8BufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Int8BufferAttribute.prototype.constructor = Int8BufferAttribute;


	function Uint8BufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Uint8Array( array ), itemSize, normalized );

	}

	Uint8BufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Uint8BufferAttribute.prototype.constructor = Uint8BufferAttribute;


	function Uint8ClampedBufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Uint8ClampedArray( array ), itemSize, normalized );

	}

	Uint8ClampedBufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Uint8ClampedBufferAttribute.prototype.constructor = Uint8ClampedBufferAttribute;


	function Int16BufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Int16Array( array ), itemSize, normalized );

	}

	Int16BufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Int16BufferAttribute.prototype.constructor = Int16BufferAttribute;


	function Uint16BufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Uint16Array( array ), itemSize, normalized );

	}

	Uint16BufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Uint16BufferAttribute.prototype.constructor = Uint16BufferAttribute;


	function Int32BufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Int32Array( array ), itemSize, normalized );

	}

	Int32BufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Int32BufferAttribute.prototype.constructor = Int32BufferAttribute;


	function Uint32BufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Uint32Array( array ), itemSize, normalized );

	}

	Uint32BufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Uint32BufferAttribute.prototype.constructor = Uint32BufferAttribute;

	function Float16BufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Uint16Array( array ), itemSize, normalized );

	}

	Float16BufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Float16BufferAttribute.prototype.constructor = Float16BufferAttribute;
	Float16BufferAttribute.prototype.isFloat16BufferAttribute = true;

	function Float32BufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Float32Array( array ), itemSize, normalized );

	}

	Float32BufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Float32BufferAttribute.prototype.constructor = Float32BufferAttribute;


	function Float64BufferAttribute( array, itemSize, normalized ) {

		BufferAttribute.call( this, new Float64Array( array ), itemSize, normalized );

	}

	Float64BufferAttribute.prototype = Object.create( BufferAttribute.prototype );
	Float64BufferAttribute.prototype.constructor = Float64BufferAttribute;

	class DirectGeometry {

		constructor() {

			this.vertices = [];
			this.normals = [];
			this.colors = [];
			this.uvs = [];
			this.uvs2 = [];

			this.groups = [];

			this.morphTargets = {};

			this.skinWeights = [];
			this.skinIndices = [];

			// this.lineDistances = [];

			this.boundingBox = null;
			this.boundingSphere = null;

			// update flags

			this.verticesNeedUpdate = false;
			this.normalsNeedUpdate = false;
			this.colorsNeedUpdate = false;
			this.uvsNeedUpdate = false;
			this.groupsNeedUpdate = false;

		}

		computeGroups( geometry ) {

			const groups = [];

			let group, i;
			let materialIndex = undefined;

			const faces = geometry.faces;

			for ( i = 0; i < faces.length; i ++ ) {

				const face = faces[ i ];

				// materials

				if ( face.materialIndex !== materialIndex ) {

					materialIndex = face.materialIndex;

					if ( group !== undefined ) {

						group.count = ( i * 3 ) - group.start;
						groups.push( group );

					}

					group = {
						start: i * 3,
						materialIndex: materialIndex
					};

				}

			}

			if ( group !== undefined ) {

				group.count = ( i * 3 ) - group.start;
				groups.push( group );

			}

			this.groups = groups;

		}

		fromGeometry( geometry ) {

			const faces = geometry.faces;
			const vertices = geometry.vertices;
			const faceVertexUvs = geometry.faceVertexUvs;

			const hasFaceVertexUv = faceVertexUvs[ 0 ] && faceVertexUvs[ 0 ].length > 0;
			const hasFaceVertexUv2 = faceVertexUvs[ 1 ] && faceVertexUvs[ 1 ].length > 0;

			// morphs

			const morphTargets = geometry.morphTargets;
			const morphTargetsLength = morphTargets.length;

			let morphTargetsPosition;

			if ( morphTargetsLength > 0 ) {

				morphTargetsPosition = [];

				for ( let i = 0; i < morphTargetsLength; i ++ ) {

					morphTargetsPosition[ i ] = {
						name: morphTargets[ i ].name,
					 	data: []
					};

				}

				this.morphTargets.position = morphTargetsPosition;

			}

			const morphNormals = geometry.morphNormals;
			const morphNormalsLength = morphNormals.length;

			let morphTargetsNormal;

			if ( morphNormalsLength > 0 ) {

				morphTargetsNormal = [];

				for ( let i = 0; i < morphNormalsLength; i ++ ) {

					morphTargetsNormal[ i ] = {
						name: morphNormals[ i ].name,
					 	data: []
					};

				}

				this.morphTargets.normal = morphTargetsNormal;

			}

			// skins

			const skinIndices = geometry.skinIndices;
			const skinWeights = geometry.skinWeights;

			const hasSkinIndices = skinIndices.length === vertices.length;
			const hasSkinWeights = skinWeights.length === vertices.length;

			//

			if ( vertices.length > 0 && faces.length === 0 ) {

				console.error( 'THREE.DirectGeometry: Faceless geometries are not supported.' );

			}

			for ( let i = 0; i < faces.length; i ++ ) {

				const face = faces[ i ];

				this.vertices.push( vertices[ face.a ], vertices[ face.b ], vertices[ face.c ] );

				const vertexNormals = face.vertexNormals;

				if ( vertexNormals.length === 3 ) {

					this.normals.push( vertexNormals[ 0 ], vertexNormals[ 1 ], vertexNormals[ 2 ] );

				} else {

					const normal = face.normal;

					this.normals.push( normal, normal, normal );

				}

				const vertexColors = face.vertexColors;

				if ( vertexColors.length === 3 ) {

					this.colors.push( vertexColors[ 0 ], vertexColors[ 1 ], vertexColors[ 2 ] );

				} else {

					const color = face.color;

					this.colors.push( color, color, color );

				}

				if ( hasFaceVertexUv === true ) {

					const vertexUvs = faceVertexUvs[ 0 ][ i ];

					if ( vertexUvs !== undefined ) {

						this.uvs.push( vertexUvs[ 0 ], vertexUvs[ 1 ], vertexUvs[ 2 ] );

					} else {

						console.warn( 'THREE.DirectGeometry.fromGeometry(): Undefined vertexUv ', i );

						this.uvs.push( new Vector2(), new Vector2(), new Vector2() );

					}

				}

				if ( hasFaceVertexUv2 === true ) {

					const vertexUvs = faceVertexUvs[ 1 ][ i ];

					if ( vertexUvs !== undefined ) {

						this.uvs2.push( vertexUvs[ 0 ], vertexUvs[ 1 ], vertexUvs[ 2 ] );

					} else {

						console.warn( 'THREE.DirectGeometry.fromGeometry(): Undefined vertexUv2 ', i );

						this.uvs2.push( new Vector2(), new Vector2(), new Vector2() );

					}

				}

				// morphs

				for ( let j = 0; j < morphTargetsLength; j ++ ) {

					const morphTarget = morphTargets[ j ].vertices;

					morphTargetsPosition[ j ].data.push( morphTarget[ face.a ], morphTarget[ face.b ], morphTarget[ face.c ] );

				}

				for ( let j = 0; j < morphNormalsLength; j ++ ) {

					const morphNormal = morphNormals[ j ].vertexNormals[ i ];

					morphTargetsNormal[ j ].data.push( morphNormal.a, morphNormal.b, morphNormal.c );

				}

				// skins

				if ( hasSkinIndices ) {

					this.skinIndices.push( skinIndices[ face.a ], skinIndices[ face.b ], skinIndices[ face.c ] );

				}

				if ( hasSkinWeights ) {

					this.skinWeights.push( skinWeights[ face.a ], skinWeights[ face.b ], skinWeights[ face.c ] );

				}

			}

			this.computeGroups( geometry );

			this.verticesNeedUpdate = geometry.verticesNeedUpdate;
			this.normalsNeedUpdate = geometry.normalsNeedUpdate;
			this.colorsNeedUpdate = geometry.colorsNeedUpdate;
			this.uvsNeedUpdate = geometry.uvsNeedUpdate;
			this.groupsNeedUpdate = geometry.groupsNeedUpdate;

			if ( geometry.boundingSphere !== null ) {

				this.boundingSphere = geometry.boundingSphere.clone();

			}

			if ( geometry.boundingBox !== null ) {

				this.boundingBox = geometry.boundingBox.clone();

			}

			return this;

		}

	}

	function arrayMax( array ) {

		if ( array.length === 0 ) return - Infinity;

		let max = array[ 0 ];

		for ( let i = 1, l = array.length; i < l; ++ i ) {

			if ( array[ i ] > max ) max = array[ i ];

		}

		return max;

	}

	const TYPED_ARRAYS = {
		Int8Array: Int8Array,
		Uint8Array: Uint8Array,
		// Workaround for IE11 pre KB2929437. See #11440
		Uint8ClampedArray: typeof Uint8ClampedArray !== 'undefined' ? Uint8ClampedArray : Uint8Array,
		Int16Array: Int16Array,
		Uint16Array: Uint16Array,
		Int32Array: Int32Array,
		Uint32Array: Uint32Array,
		Float32Array: Float32Array,
		Float64Array: Float64Array
	};

	function getTypedArray( type, buffer ) {

		return new TYPED_ARRAYS[ type ]( buffer );

	}

	let _bufferGeometryId = 1; // BufferGeometry uses odd numbers as Id

	const _m1$2 = new Matrix4();
	const _obj = new Object3D();
	const _offset = new Vector3();
	const _box$2 = new Box3();
	const _boxMorphTargets = new Box3();
	const _vector$4 = new Vector3();

	function BufferGeometry() {

		Object.defineProperty( this, 'id', { value: _bufferGeometryId += 2 } );

		this.uuid = MathUtils.generateUUID();

		this.name = '';
		this.type = 'BufferGeometry';

		this.index = null;
		this.attributes = {};

		this.morphAttributes = {};
		this.morphTargetsRelative = false;

		this.groups = [];

		this.boundingBox = null;
		this.boundingSphere = null;

		this.drawRange = { start: 0, count: Infinity };

		this.userData = {};

	}

	BufferGeometry.prototype = Object.assign( Object.create( EventDispatcher$1.prototype ), {

		constructor: BufferGeometry,

		isBufferGeometry: true,

		getIndex: function () {

			return this.index;

		},

		setIndex: function ( index ) {

			if ( Array.isArray( index ) ) {

				this.index = new ( arrayMax( index ) > 65535 ? Uint32BufferAttribute : Uint16BufferAttribute )( index, 1 );

			} else {

				this.index = index;

			}

			return this;

		},

		getAttribute: function ( name ) {

			return this.attributes[ name ];

		},

		setAttribute: function ( name, attribute ) {

			this.attributes[ name ] = attribute;

			return this;

		},

		deleteAttribute: function ( name ) {

			delete this.attributes[ name ];

			return this;

		},

		hasAttribute: function ( name ) {

			return this.attributes[ name ] !== undefined;

		},

		addGroup: function ( start, count, materialIndex = 0 ) {

			this.groups.push( {

				start: start,
				count: count,
				materialIndex: materialIndex

			} );

		},

		clearGroups: function () {

			this.groups = [];

		},

		setDrawRange: function ( start, count ) {

			this.drawRange.start = start;
			this.drawRange.count = count;

		},

		applyMatrix4: function ( matrix ) {

			const position = this.attributes.position;

			if ( position !== undefined ) {

				position.applyMatrix4( matrix );

				position.needsUpdate = true;

			}

			const normal = this.attributes.normal;

			if ( normal !== undefined ) {

				const normalMatrix = new Matrix3().getNormalMatrix( matrix );

				normal.applyNormalMatrix( normalMatrix );

				normal.needsUpdate = true;

			}

			const tangent = this.attributes.tangent;

			if ( tangent !== undefined ) {

				tangent.transformDirection( matrix );

				tangent.needsUpdate = true;

			}

			if ( this.boundingBox !== null ) {

				this.computeBoundingBox();

			}

			if ( this.boundingSphere !== null ) {

				this.computeBoundingSphere();

			}

			return this;

		},

		rotateX: function ( angle ) {

			// rotate geometry around world x-axis

			_m1$2.makeRotationX( angle );

			this.applyMatrix4( _m1$2 );

			return this;

		},

		rotateY: function ( angle ) {

			// rotate geometry around world y-axis

			_m1$2.makeRotationY( angle );

			this.applyMatrix4( _m1$2 );

			return this;

		},

		rotateZ: function ( angle ) {

			// rotate geometry around world z-axis

			_m1$2.makeRotationZ( angle );

			this.applyMatrix4( _m1$2 );

			return this;

		},

		translate: function ( x, y, z ) {

			// translate geometry

			_m1$2.makeTranslation( x, y, z );

			this.applyMatrix4( _m1$2 );

			return this;

		},

		scale: function ( x, y, z ) {

			// scale geometry

			_m1$2.makeScale( x, y, z );

			this.applyMatrix4( _m1$2 );

			return this;

		},

		lookAt: function ( vector ) {

			_obj.lookAt( vector );

			_obj.updateMatrix();

			this.applyMatrix4( _obj.matrix );

			return this;

		},

		center: function () {

			this.computeBoundingBox();

			this.boundingBox.getCenter( _offset ).negate();

			this.translate( _offset.x, _offset.y, _offset.z );

			return this;

		},

		setFromObject: function ( object ) {

			// console.log( 'THREE.BufferGeometry.setFromObject(). Converting', object, this );

			const geometry = object.geometry;

			if ( object.isPoints || object.isLine ) {

				const positions = new Float32BufferAttribute( geometry.vertices.length * 3, 3 );
				const colors = new Float32BufferAttribute( geometry.colors.length * 3, 3 );

				this.setAttribute( 'position', positions.copyVector3sArray( geometry.vertices ) );
				this.setAttribute( 'color', colors.copyColorsArray( geometry.colors ) );

				if ( geometry.lineDistances && geometry.lineDistances.length === geometry.vertices.length ) {

					const lineDistances = new Float32BufferAttribute( geometry.lineDistances.length, 1 );

					this.setAttribute( 'lineDistance', lineDistances.copyArray( geometry.lineDistances ) );

				}

				if ( geometry.boundingSphere !== null ) {

					this.boundingSphere = geometry.boundingSphere.clone();

				}

				if ( geometry.boundingBox !== null ) {

					this.boundingBox = geometry.boundingBox.clone();

				}

			} else if ( object.isMesh ) {

				if ( geometry && geometry.isGeometry ) {

					this.fromGeometry( geometry );

				}

			}

			return this;

		},

		setFromPoints: function ( points ) {

			const position = [];

			for ( let i = 0, l = points.length; i < l; i ++ ) {

				const point = points[ i ];
				position.push( point.x, point.y, point.z || 0 );

			}

			this.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );

			return this;

		},

		updateFromObject: function ( object ) {

			let geometry = object.geometry;

			if ( object.isMesh ) {

				let direct = geometry.__directGeometry;

				if ( geometry.elementsNeedUpdate === true ) {

					direct = undefined;
					geometry.elementsNeedUpdate = false;

				}

				if ( direct === undefined ) {

					return this.fromGeometry( geometry );

				}

				direct.verticesNeedUpdate = geometry.verticesNeedUpdate;
				direct.normalsNeedUpdate = geometry.normalsNeedUpdate;
				direct.colorsNeedUpdate = geometry.colorsNeedUpdate;
				direct.uvsNeedUpdate = geometry.uvsNeedUpdate;
				direct.groupsNeedUpdate = geometry.groupsNeedUpdate;

				geometry.verticesNeedUpdate = false;
				geometry.normalsNeedUpdate = false;
				geometry.colorsNeedUpdate = false;
				geometry.uvsNeedUpdate = false;
				geometry.groupsNeedUpdate = false;

				geometry = direct;

			}

			if ( geometry.verticesNeedUpdate === true ) {

				const attribute = this.attributes.position;

				if ( attribute !== undefined ) {

					attribute.copyVector3sArray( geometry.vertices );
					attribute.needsUpdate = true;

				}

				geometry.verticesNeedUpdate = false;

			}

			if ( geometry.normalsNeedUpdate === true ) {

				const attribute = this.attributes.normal;

				if ( attribute !== undefined ) {

					attribute.copyVector3sArray( geometry.normals );
					attribute.needsUpdate = true;

				}

				geometry.normalsNeedUpdate = false;

			}

			if ( geometry.colorsNeedUpdate === true ) {

				const attribute = this.attributes.color;

				if ( attribute !== undefined ) {

					attribute.copyColorsArray( geometry.colors );
					attribute.needsUpdate = true;

				}

				geometry.colorsNeedUpdate = false;

			}

			if ( geometry.uvsNeedUpdate ) {

				const attribute = this.attributes.uv;

				if ( attribute !== undefined ) {

					attribute.copyVector2sArray( geometry.uvs );
					attribute.needsUpdate = true;

				}

				geometry.uvsNeedUpdate = false;

			}

			if ( geometry.lineDistancesNeedUpdate ) {

				const attribute = this.attributes.lineDistance;

				if ( attribute !== undefined ) {

					attribute.copyArray( geometry.lineDistances );
					attribute.needsUpdate = true;

				}

				geometry.lineDistancesNeedUpdate = false;

			}

			if ( geometry.groupsNeedUpdate ) {

				geometry.computeGroups( object.geometry );
				this.groups = geometry.groups;

				geometry.groupsNeedUpdate = false;

			}

			return this;

		},

		fromGeometry: function ( geometry ) {

			geometry.__directGeometry = new DirectGeometry().fromGeometry( geometry );

			return this.fromDirectGeometry( geometry.__directGeometry );

		},

		fromDirectGeometry: function ( geometry ) {

			const positions = new Float32Array( geometry.vertices.length * 3 );
			this.setAttribute( 'position', new BufferAttribute( positions, 3 ).copyVector3sArray( geometry.vertices ) );

			if ( geometry.normals.length > 0 ) {

				const normals = new Float32Array( geometry.normals.length * 3 );
				this.setAttribute( 'normal', new BufferAttribute( normals, 3 ).copyVector3sArray( geometry.normals ) );

			}

			if ( geometry.colors.length > 0 ) {

				const colors = new Float32Array( geometry.colors.length * 3 );
				this.setAttribute( 'color', new BufferAttribute( colors, 3 ).copyColorsArray( geometry.colors ) );

			}

			if ( geometry.uvs.length > 0 ) {

				const uvs = new Float32Array( geometry.uvs.length * 2 );
				this.setAttribute( 'uv', new BufferAttribute( uvs, 2 ).copyVector2sArray( geometry.uvs ) );

			}

			if ( geometry.uvs2.length > 0 ) {

				const uvs2 = new Float32Array( geometry.uvs2.length * 2 );
				this.setAttribute( 'uv2', new BufferAttribute( uvs2, 2 ).copyVector2sArray( geometry.uvs2 ) );

			}

			// groups

			this.groups = geometry.groups;

			// morphs

			for ( const name in geometry.morphTargets ) {

				const array = [];
				const morphTargets = geometry.morphTargets[ name ];

				for ( let i = 0, l = morphTargets.length; i < l; i ++ ) {

					const morphTarget = morphTargets[ i ];

					const attribute = new Float32BufferAttribute( morphTarget.data.length * 3, 3 );
					attribute.name = morphTarget.name;

					array.push( attribute.copyVector3sArray( morphTarget.data ) );

				}

				this.morphAttributes[ name ] = array;

			}

			// skinning

			if ( geometry.skinIndices.length > 0 ) {

				const skinIndices = new Float32BufferAttribute( geometry.skinIndices.length * 4, 4 );
				this.setAttribute( 'skinIndex', skinIndices.copyVector4sArray( geometry.skinIndices ) );

			}

			if ( geometry.skinWeights.length > 0 ) {

				const skinWeights = new Float32BufferAttribute( geometry.skinWeights.length * 4, 4 );
				this.setAttribute( 'skinWeight', skinWeights.copyVector4sArray( geometry.skinWeights ) );

			}

			//

			if ( geometry.boundingSphere !== null ) {

				this.boundingSphere = geometry.boundingSphere.clone();

			}

			if ( geometry.boundingBox !== null ) {

				this.boundingBox = geometry.boundingBox.clone();

			}

			return this;

		},

		computeBoundingBox: function () {

			if ( this.boundingBox === null ) {

				this.boundingBox = new Box3();

			}

			const position = this.attributes.position;
			const morphAttributesPosition = this.morphAttributes.position;

			if ( position && position.isGLBufferAttribute ) {

				console.error( 'THREE.BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box. Alternatively set "mesh.frustumCulled" to "false".', this );

				this.boundingBox.set(
					new Vector3( - Infinity, - Infinity, - Infinity ),
					new Vector3( + Infinity, + Infinity, + Infinity )
				);

				return;

			}

			if ( position !== undefined ) {

				this.boundingBox.setFromBufferAttribute( position );

				// process morph attributes if present

				if ( morphAttributesPosition ) {

					for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {

						const morphAttribute = morphAttributesPosition[ i ];
						_box$2.setFromBufferAttribute( morphAttribute );

						if ( this.morphTargetsRelative ) {

							_vector$4.addVectors( this.boundingBox.min, _box$2.min );
							this.boundingBox.expandByPoint( _vector$4 );

							_vector$4.addVectors( this.boundingBox.max, _box$2.max );
							this.boundingBox.expandByPoint( _vector$4 );

						} else {

							this.boundingBox.expandByPoint( _box$2.min );
							this.boundingBox.expandByPoint( _box$2.max );

						}

					}

				}

			} else {

				this.boundingBox.makeEmpty();

			}

			if ( isNaN( this.boundingBox.min.x ) || isNaN( this.boundingBox.min.y ) || isNaN( this.boundingBox.min.z ) ) {

				console.error( 'THREE.BufferGeometry.computeBoundingBox(): Computed min/max have NaN values. The "position" attribute is likely to have NaN values.', this );

			}

		},

		computeBoundingSphere: function () {

			if ( this.boundingSphere === null ) {

				this.boundingSphere = new Sphere();

			}

			const position = this.attributes.position;
			const morphAttributesPosition = this.morphAttributes.position;

			if ( position && position.isGLBufferAttribute ) {

				console.error( 'THREE.BufferGeometry.computeBoundingSphere(): GLBufferAttribute requires a manual bounding sphere. Alternatively set "mesh.frustumCulled" to "false".', this );

				this.boundingSphere.set( new Vector3(), Infinity );

				return;

			}

			if ( position ) {

				// first, find the center of the bounding sphere

				const center = this.boundingSphere.center;

				_box$2.setFromBufferAttribute( position );

				// process morph attributes if present

				if ( morphAttributesPosition ) {

					for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {

						const morphAttribute = morphAttributesPosition[ i ];
						_boxMorphTargets.setFromBufferAttribute( morphAttribute );

						if ( this.morphTargetsRelative ) {

							_vector$4.addVectors( _box$2.min, _boxMorphTargets.min );
							_box$2.expandByPoint( _vector$4 );

							_vector$4.addVectors( _box$2.max, _boxMorphTargets.max );
							_box$2.expandByPoint( _vector$4 );

						} else {

							_box$2.expandByPoint( _boxMorphTargets.min );
							_box$2.expandByPoint( _boxMorphTargets.max );

						}

					}

				}

				_box$2.getCenter( center );

				// second, try to find a boundingSphere with a radius smaller than the
				// boundingSphere of the boundingBox: sqrt(3) smaller in the best case

				let maxRadiusSq = 0;

				for ( let i = 0, il = position.count; i < il; i ++ ) {

					_vector$4.fromBufferAttribute( position, i );

					maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector$4 ) );

				}

				// process morph attributes if present

				if ( morphAttributesPosition ) {

					for ( let i = 0, il = morphAttributesPosition.length; i < il; i ++ ) {

						const morphAttribute = morphAttributesPosition[ i ];
						const morphTargetsRelative = this.morphTargetsRelative;

						for ( let j = 0, jl = morphAttribute.count; j < jl; j ++ ) {

							_vector$4.fromBufferAttribute( morphAttribute, j );

							if ( morphTargetsRelative ) {

								_offset.fromBufferAttribute( position, j );
								_vector$4.add( _offset );

							}

							maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector$4 ) );

						}

					}

				}

				this.boundingSphere.radius = Math.sqrt( maxRadiusSq );

				if ( isNaN( this.boundingSphere.radius ) ) {

					console.error( 'THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values.', this );

				}

			}

		},

		computeFaceNormals: function () {

			// backwards compatibility

		},

		computeVertexNormals: function () {

			const index = this.index;
			const positionAttribute = this.getAttribute( 'position' );

			if ( positionAttribute !== undefined ) {

				let normalAttribute = this.getAttribute( 'normal' );

				if ( normalAttribute === undefined ) {

					normalAttribute = new BufferAttribute( new Float32Array( positionAttribute.count * 3 ), 3 );
					this.setAttribute( 'normal', normalAttribute );

				} else {

					// reset existing normals to zero

					for ( let i = 0, il = normalAttribute.count; i < il; i ++ ) {

						normalAttribute.setXYZ( i, 0, 0, 0 );

					}

				}

				const pA = new Vector3(), pB = new Vector3(), pC = new Vector3();
				const nA = new Vector3(), nB = new Vector3(), nC = new Vector3();
				const cb = new Vector3(), ab = new Vector3();

				// indexed elements

				if ( index ) {

					for ( let i = 0, il = index.count; i < il; i += 3 ) {

						const vA = index.getX( i + 0 );
						const vB = index.getX( i + 1 );
						const vC = index.getX( i + 2 );

						pA.fromBufferAttribute( positionAttribute, vA );
						pB.fromBufferAttribute( positionAttribute, vB );
						pC.fromBufferAttribute( positionAttribute, vC );

						cb.subVectors( pC, pB );
						ab.subVectors( pA, pB );
						cb.cross( ab );

						nA.fromBufferAttribute( normalAttribute, vA );
						nB.fromBufferAttribute( normalAttribute, vB );
						nC.fromBufferAttribute( normalAttribute, vC );

						nA.add( cb );
						nB.add( cb );
						nC.add( cb );

						normalAttribute.setXYZ( vA, nA.x, nA.y, nA.z );
						normalAttribute.setXYZ( vB, nB.x, nB.y, nB.z );
						normalAttribute.setXYZ( vC, nC.x, nC.y, nC.z );

					}

				} else {

					// non-indexed elements (unconnected triangle soup)

					for ( let i = 0, il = positionAttribute.count; i < il; i += 3 ) {

						pA.fromBufferAttribute( positionAttribute, i + 0 );
						pB.fromBufferAttribute( positionAttribute, i + 1 );
						pC.fromBufferAttribute( positionAttribute, i + 2 );

						cb.subVectors( pC, pB );
						ab.subVectors( pA, pB );
						cb.cross( ab );

						normalAttribute.setXYZ( i + 0, cb.x, cb.y, cb.z );
						normalAttribute.setXYZ( i + 1, cb.x, cb.y, cb.z );
						normalAttribute.setXYZ( i + 2, cb.x, cb.y, cb.z );

					}

				}

				this.normalizeNormals();

				normalAttribute.needsUpdate = true;

			}

		},

		merge: function ( geometry, offset ) {

			if ( ! ( geometry && geometry.isBufferGeometry ) ) {

				console.error( 'THREE.BufferGeometry.merge(): geometry not an instance of THREE.BufferGeometry.', geometry );
				return;

			}

			if ( offset === undefined ) {

				offset = 0;

				console.warn(
					'THREE.BufferGeometry.merge(): Overwriting original geometry, starting at offset=0. '
					+ 'Use BufferGeometryUtils.mergeBufferGeometries() for lossless merge.'
				);

			}

			const attributes = this.attributes;

			for ( const key in attributes ) {

				if ( geometry.attributes[ key ] === undefined ) continue;

				const attribute1 = attributes[ key ];
				const attributeArray1 = attribute1.array;

				const attribute2 = geometry.attributes[ key ];
				const attributeArray2 = attribute2.array;

				const attributeOffset = attribute2.itemSize * offset;
				const length = Math.min( attributeArray2.length, attributeArray1.length - attributeOffset );

				for ( let i = 0, j = attributeOffset; i < length; i ++, j ++ ) {

					attributeArray1[ j ] = attributeArray2[ i ];

				}

			}

			return this;

		},

		normalizeNormals: function () {

			const normals = this.attributes.normal;

			for ( let i = 0, il = normals.count; i < il; i ++ ) {

				_vector$4.fromBufferAttribute( normals, i );

				_vector$4.normalize();

				normals.setXYZ( i, _vector$4.x, _vector$4.y, _vector$4.z );

			}

		},

		toNonIndexed: function () {

			function convertBufferAttribute( attribute, indices ) {

				const array = attribute.array;
				const itemSize = attribute.itemSize;
				const normalized = attribute.normalized;

				const array2 = new array.constructor( indices.length * itemSize );

				let index = 0, index2 = 0;

				for ( let i = 0, l = indices.length; i < l; i ++ ) {

					index = indices[ i ] * itemSize;

					for ( let j = 0; j < itemSize; j ++ ) {

						array2[ index2 ++ ] = array[ index ++ ];

					}

				}

				return new BufferAttribute( array2, itemSize, normalized );

			}

			//

			if ( this.index === null ) {

				console.warn( 'THREE.BufferGeometry.toNonIndexed(): Geometry is already non-indexed.' );
				return this;

			}

			const geometry2 = new BufferGeometry();

			const indices = this.index.array;
			const attributes = this.attributes;

			// attributes

			for ( const name in attributes ) {

				const attribute = attributes[ name ];

				const newAttribute = convertBufferAttribute( attribute, indices );

				geometry2.setAttribute( name, newAttribute );

			}

			// morph attributes

			const morphAttributes = this.morphAttributes;

			for ( const name in morphAttributes ) {

				const morphArray = [];
				const morphAttribute = morphAttributes[ name ]; // morphAttribute: array of Float32BufferAttributes

				for ( let i = 0, il = morphAttribute.length; i < il; i ++ ) {

					const attribute = morphAttribute[ i ];

					const newAttribute = convertBufferAttribute( attribute, indices );

					morphArray.push( newAttribute );

				}

				geometry2.morphAttributes[ name ] = morphArray;

			}

			geometry2.morphTargetsRelative = this.morphTargetsRelative;

			// groups

			const groups = this.groups;

			for ( let i = 0, l = groups.length; i < l; i ++ ) {

				const group = groups[ i ];
				geometry2.addGroup( group.start, group.count, group.materialIndex );

			}

			return geometry2;

		},

		toJSON: function () {

			const data = {
				metadata: {
					version: 4.5,
					type: 'BufferGeometry',
					generator: 'BufferGeometry.toJSON'
				}
			};

			// standard BufferGeometry serialization

			data.uuid = this.uuid;
			data.type = this.type;
			if ( this.name !== '' ) data.name = this.name;
			if ( Object.keys( this.userData ).length > 0 ) data.userData = this.userData;

			if ( this.parameters !== undefined ) {

				const parameters = this.parameters;

				for ( const key in parameters ) {

					if ( parameters[ key ] !== undefined ) data[ key ] = parameters[ key ];

				}

				return data;

			}

			data.data = { attributes: {} };

			const index = this.index;

			if ( index !== null ) {

				data.data.index = {
					type: index.array.constructor.name,
					array: Array.prototype.slice.call( index.array )
				};

			}

			const attributes = this.attributes;

			for ( const key in attributes ) {

				const attribute = attributes[ key ];

				const attributeData = attribute.toJSON( data.data );

				if ( attribute.name !== '' ) attributeData.name = attribute.name;

				data.data.attributes[ key ] = attributeData;

			}

			const morphAttributes = {};
			let hasMorphAttributes = false;

			for ( const key in this.morphAttributes ) {

				const attributeArray = this.morphAttributes[ key ];

				const array = [];

				for ( let i = 0, il = attributeArray.length; i < il; i ++ ) {

					const attribute = attributeArray[ i ];

					const attributeData = attribute.toJSON( data.data );

					if ( attribute.name !== '' ) attributeData.name = attribute.name;

					array.push( attributeData );

				}

				if ( array.length > 0 ) {

					morphAttributes[ key ] = array;

					hasMorphAttributes = true;

				}

			}

			if ( hasMorphAttributes ) {

				data.data.morphAttributes = morphAttributes;
				data.data.morphTargetsRelative = this.morphTargetsRelative;

			}

			const groups = this.groups;

			if ( groups.length > 0 ) {

				data.data.groups = JSON.parse( JSON.stringify( groups ) );

			}

			const boundingSphere = this.boundingSphere;

			if ( boundingSphere !== null ) {

				data.data.boundingSphere = {
					center: boundingSphere.center.toArray(),
					radius: boundingSphere.radius
				};

			}

			return data;

		},

		clone: function () {

			/*
			 // Handle primitives

			 const parameters = this.parameters;

			 if ( parameters !== undefined ) {

			 const values = [];

			 for ( const key in parameters ) {

			 values.push( parameters[ key ] );

			 }

			 const geometry = Object.create( this.constructor.prototype );
			 this.constructor.apply( geometry, values );
			 return geometry;

			 }

			 return new this.constructor().copy( this );
			 */

			return new BufferGeometry().copy( this );

		},

		copy: function ( source ) {

			// reset

			this.index = null;
			this.attributes = {};
			this.morphAttributes = {};
			this.groups = [];
			this.boundingBox = null;
			this.boundingSphere = null;

			// used for storing cloned, shared data

			const data = {};

			// name

			this.name = source.name;

			// index

			const index = source.index;

			if ( index !== null ) {

				this.setIndex( index.clone( data ) );

			}

			// attributes

			const attributes = source.attributes;

			for ( const name in attributes ) {

				const attribute = attributes[ name ];
				this.setAttribute( name, attribute.clone( data ) );

			}

			// morph attributes

			const morphAttributes = source.morphAttributes;

			for ( const name in morphAttributes ) {

				const array = [];
				const morphAttribute = morphAttributes[ name ]; // morphAttribute: array of Float32BufferAttributes

				for ( let i = 0, l = morphAttribute.length; i < l; i ++ ) {

					array.push( morphAttribute[ i ].clone( data ) );

				}

				this.morphAttributes[ name ] = array;

			}

			this.morphTargetsRelative = source.morphTargetsRelative;

			// groups

			const groups = source.groups;

			for ( let i = 0, l = groups.length; i < l; i ++ ) {

				const group = groups[ i ];
				this.addGroup( group.start, group.count, group.materialIndex );

			}

			// bounding box

			const boundingBox = source.boundingBox;

			if ( boundingBox !== null ) {

				this.boundingBox = boundingBox.clone();

			}

			// bounding sphere

			const boundingSphere = source.boundingSphere;

			if ( boundingSphere !== null ) {

				this.boundingSphere = boundingSphere.clone();

			}

			// draw range

			this.drawRange.start = source.drawRange.start;
			this.drawRange.count = source.drawRange.count;

			// user data

			this.userData = source.userData;

			return this;

		},

		dispose: function () {

			this.dispatchEvent( { type: 'dispose' } );

		}

	} );

	const _inverseMatrix = new Matrix4();
	const _ray = new Ray();
	const _sphere = new Sphere();

	const _vA = new Vector3();
	const _vB = new Vector3();
	const _vC = new Vector3();

	const _tempA = new Vector3();
	const _tempB = new Vector3();
	const _tempC = new Vector3();

	const _morphA = new Vector3();
	const _morphB = new Vector3();
	const _morphC = new Vector3();

	const _uvA = new Vector2();
	const _uvB = new Vector2();
	const _uvC = new Vector2();

	const _intersectionPoint = new Vector3();
	const _intersectionPointWorld = new Vector3();

	function Mesh( geometry = new BufferGeometry(), material = new MeshBasicMaterial() ) {

		Object3D.call( this );

		this.type = 'Mesh';

		this.geometry = geometry;
		this.material = material;

		this.updateMorphTargets();

	}

	Mesh.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: Mesh,

		isMesh: true,

		copy: function ( source ) {

			Object3D.prototype.copy.call( this, source );

			if ( source.morphTargetInfluences !== undefined ) {

				this.morphTargetInfluences = source.morphTargetInfluences.slice();

			}

			if ( source.morphTargetDictionary !== undefined ) {

				this.morphTargetDictionary = Object.assign( {}, source.morphTargetDictionary );

			}

			this.material = source.material;
			this.geometry = source.geometry;

			return this;

		},

		updateMorphTargets: function () {

			const geometry = this.geometry;

			if ( geometry.isBufferGeometry ) {

				const morphAttributes = geometry.morphAttributes;
				const keys = Object.keys( morphAttributes );

				if ( keys.length > 0 ) {

					const morphAttribute = morphAttributes[ keys[ 0 ] ];

					if ( morphAttribute !== undefined ) {

						this.morphTargetInfluences = [];
						this.morphTargetDictionary = {};

						for ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {

							const name = morphAttribute[ m ].name || String( m );

							this.morphTargetInfluences.push( 0 );
							this.morphTargetDictionary[ name ] = m;

						}

					}

				}

			} else {

				const morphTargets = geometry.morphTargets;

				if ( morphTargets !== undefined && morphTargets.length > 0 ) {

					console.error( 'THREE.Mesh.updateMorphTargets() no longer supports THREE.Geometry. Use THREE.BufferGeometry instead.' );

				}

			}

		},

		raycast: function ( raycaster, intersects ) {

			const geometry = this.geometry;
			const material = this.material;
			const matrixWorld = this.matrixWorld;

			if ( material === undefined ) return;

			// Checking boundingSphere distance to ray

			if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();

			_sphere.copy( geometry.boundingSphere );
			_sphere.applyMatrix4( matrixWorld );

			if ( raycaster.ray.intersectsSphere( _sphere ) === false ) return;

			//

			_inverseMatrix.copy( matrixWorld ).invert();
			_ray.copy( raycaster.ray ).applyMatrix4( _inverseMatrix );

			// Check boundingBox before continuing

			if ( geometry.boundingBox !== null ) {

				if ( _ray.intersectsBox( geometry.boundingBox ) === false ) return;

			}

			let intersection;

			if ( geometry.isBufferGeometry ) {

				const index = geometry.index;
				const position = geometry.attributes.position;
				const morphPosition = geometry.morphAttributes.position;
				const morphTargetsRelative = geometry.morphTargetsRelative;
				const uv = geometry.attributes.uv;
				const uv2 = geometry.attributes.uv2;
				const groups = geometry.groups;
				const drawRange = geometry.drawRange;

				if ( index !== null ) {

					// indexed buffer geometry

					if ( Array.isArray( material ) ) {

						for ( let i = 0, il = groups.length; i < il; i ++ ) {

							const group = groups[ i ];
							const groupMaterial = material[ group.materialIndex ];

							const start = Math.max( group.start, drawRange.start );
							const end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) );

							for ( let j = start, jl = end; j < jl; j += 3 ) {

								const a = index.getX( j );
								const b = index.getX( j + 1 );
								const c = index.getX( j + 2 );

								intersection = checkBufferGeometryIntersection( this, groupMaterial, raycaster, _ray, position, morphPosition, morphTargetsRelative, uv, uv2, a, b, c );

								if ( intersection ) {

									intersection.faceIndex = Math.floor( j / 3 ); // triangle number in indexed buffer semantics
									intersection.face.materialIndex = group.materialIndex;
									intersects.push( intersection );

								}

							}

						}

					} else {

						const start = Math.max( 0, drawRange.start );
						const end = Math.min( index.count, ( drawRange.start + drawRange.count ) );

						for ( let i = start, il = end; i < il; i += 3 ) {

							const a = index.getX( i );
							const b = index.getX( i + 1 );
							const c = index.getX( i + 2 );

							intersection = checkBufferGeometryIntersection( this, material, raycaster, _ray, position, morphPosition, morphTargetsRelative, uv, uv2, a, b, c );

							if ( intersection ) {

								intersection.faceIndex = Math.floor( i / 3 ); // triangle number in indexed buffer semantics
								intersects.push( intersection );

							}

						}

					}

				} else if ( position !== undefined ) {

					// non-indexed buffer geometry

					if ( Array.isArray( material ) ) {

						for ( let i = 0, il = groups.length; i < il; i ++ ) {

							const group = groups[ i ];
							const groupMaterial = material[ group.materialIndex ];

							const start = Math.max( group.start, drawRange.start );
							const end = Math.min( ( group.start + group.count ), ( drawRange.start + drawRange.count ) );

							for ( let j = start, jl = end; j < jl; j += 3 ) {

								const a = j;
								const b = j + 1;
								const c = j + 2;

								intersection = checkBufferGeometryIntersection( this, groupMaterial, raycaster, _ray, position, morphPosition, morphTargetsRelative, uv, uv2, a, b, c );

								if ( intersection ) {

									intersection.faceIndex = Math.floor( j / 3 ); // triangle number in non-indexed buffer semantics
									intersection.face.materialIndex = group.materialIndex;
									intersects.push( intersection );

								}

							}

						}

					} else {

						const start = Math.max( 0, drawRange.start );
						const end = Math.min( position.count, ( drawRange.start + drawRange.count ) );

						for ( let i = start, il = end; i < il; i += 3 ) {

							const a = i;
							const b = i + 1;
							const c = i + 2;

							intersection = checkBufferGeometryIntersection( this, material, raycaster, _ray, position, morphPosition, morphTargetsRelative, uv, uv2, a, b, c );

							if ( intersection ) {

								intersection.faceIndex = Math.floor( i / 3 ); // triangle number in non-indexed buffer semantics
								intersects.push( intersection );

							}

						}

					}

				}

			} else if ( geometry.isGeometry ) {

				const isMultiMaterial = Array.isArray( material );

				const vertices = geometry.vertices;
				const faces = geometry.faces;
				let uvs;

				const faceVertexUvs = geometry.faceVertexUvs[ 0 ];
				if ( faceVertexUvs.length > 0 ) uvs = faceVertexUvs;

				for ( let f = 0, fl = faces.length; f < fl; f ++ ) {

					const face = faces[ f ];
					const faceMaterial = isMultiMaterial ? material[ face.materialIndex ] : material;

					if ( faceMaterial === undefined ) continue;

					const fvA = vertices[ face.a ];
					const fvB = vertices[ face.b ];
					const fvC = vertices[ face.c ];

					intersection = checkIntersection( this, faceMaterial, raycaster, _ray, fvA, fvB, fvC, _intersectionPoint );

					if ( intersection ) {

						if ( uvs && uvs[ f ] ) {

							const uvs_f = uvs[ f ];
							_uvA.copy( uvs_f[ 0 ] );
							_uvB.copy( uvs_f[ 1 ] );
							_uvC.copy( uvs_f[ 2 ] );

							intersection.uv = Triangle.getUV( _intersectionPoint, fvA, fvB, fvC, _uvA, _uvB, _uvC, new Vector2() );

						}

						intersection.face = face;
						intersection.faceIndex = f;
						intersects.push( intersection );

					}

				}

			}

		}

	} );

	function checkIntersection( object, material, raycaster, ray, pA, pB, pC, point ) {

		let intersect;

		if ( material.side === BackSide ) {

			intersect = ray.intersectTriangle( pC, pB, pA, true, point );

		} else {

			intersect = ray.intersectTriangle( pA, pB, pC, material.side !== DoubleSide, point );

		}

		if ( intersect === null ) return null;

		_intersectionPointWorld.copy( point );
		_intersectionPointWorld.applyMatrix4( object.matrixWorld );

		const distance = raycaster.ray.origin.distanceTo( _intersectionPointWorld );

		if ( distance < raycaster.near || distance > raycaster.far ) return null;

		return {
			distance: distance,
			point: _intersectionPointWorld.clone(),
			object: object
		};

	}

	function checkBufferGeometryIntersection( object, material, raycaster, ray, position, morphPosition, morphTargetsRelative, uv, uv2, a, b, c ) {

		_vA.fromBufferAttribute( position, a );
		_vB.fromBufferAttribute( position, b );
		_vC.fromBufferAttribute( position, c );

		const morphInfluences = object.morphTargetInfluences;

		if ( material.morphTargets && morphPosition && morphInfluences ) {

			_morphA.set( 0, 0, 0 );
			_morphB.set( 0, 0, 0 );
			_morphC.set( 0, 0, 0 );

			for ( let i = 0, il = morphPosition.length; i < il; i ++ ) {

				const influence = morphInfluences[ i ];
				const morphAttribute = morphPosition[ i ];

				if ( influence === 0 ) continue;

				_tempA.fromBufferAttribute( morphAttribute, a );
				_tempB.fromBufferAttribute( morphAttribute, b );
				_tempC.fromBufferAttribute( morphAttribute, c );

				if ( morphTargetsRelative ) {

					_morphA.addScaledVector( _tempA, influence );
					_morphB.addScaledVector( _tempB, influence );
					_morphC.addScaledVector( _tempC, influence );

				} else {

					_morphA.addScaledVector( _tempA.sub( _vA ), influence );
					_morphB.addScaledVector( _tempB.sub( _vB ), influence );
					_morphC.addScaledVector( _tempC.sub( _vC ), influence );

				}

			}

			_vA.add( _morphA );
			_vB.add( _morphB );
			_vC.add( _morphC );

		}

		if ( object.isSkinnedMesh ) {

			object.boneTransform( a, _vA );
			object.boneTransform( b, _vB );
			object.boneTransform( c, _vC );

		}

		const intersection = checkIntersection( object, material, raycaster, ray, _vA, _vB, _vC, _intersectionPoint );

		if ( intersection ) {

			if ( uv ) {

				_uvA.fromBufferAttribute( uv, a );
				_uvB.fromBufferAttribute( uv, b );
				_uvC.fromBufferAttribute( uv, c );

				intersection.uv = Triangle.getUV( _intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2() );

			}

			if ( uv2 ) {

				_uvA.fromBufferAttribute( uv2, a );
				_uvB.fromBufferAttribute( uv2, b );
				_uvC.fromBufferAttribute( uv2, c );

				intersection.uv2 = Triangle.getUV( _intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2() );

			}

			const face = new Face3( a, b, c );
			Triangle.getNormal( _vA, _vB, _vC, face.normal );

			intersection.face = face;

		}

		return intersection;

	}

	class BoxBufferGeometry extends BufferGeometry {

		constructor( width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 ) {

			super();

			this.type = 'BoxBufferGeometry';

			this.parameters = {
				width: width,
				height: height,
				depth: depth,
				widthSegments: widthSegments,
				heightSegments: heightSegments,
				depthSegments: depthSegments
			};

			const scope = this;

			// segments

			widthSegments = Math.floor( widthSegments );
			heightSegments = Math.floor( heightSegments );
			depthSegments = Math.floor( depthSegments );

			// buffers

			const indices = [];
			const vertices = [];
			const normals = [];
			const uvs = [];

			// helper variables

			let numberOfVertices = 0;
			let groupStart = 0;

			// build each side of the box geometry

			buildPlane( 'z', 'y', 'x', - 1, - 1, depth, height, width, depthSegments, heightSegments, 0 ); // px
			buildPlane( 'z', 'y', 'x', 1, - 1, depth, height, - width, depthSegments, heightSegments, 1 ); // nx
			buildPlane( 'x', 'z', 'y', 1, 1, width, depth, height, widthSegments, depthSegments, 2 ); // py
			buildPlane( 'x', 'z', 'y', 1, - 1, width, depth, - height, widthSegments, depthSegments, 3 ); // ny
			buildPlane( 'x', 'y', 'z', 1, - 1, width, height, depth, widthSegments, heightSegments, 4 ); // pz
			buildPlane( 'x', 'y', 'z', - 1, - 1, width, height, - depth, widthSegments, heightSegments, 5 ); // nz

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

			function buildPlane( u, v, w, udir, vdir, width, height, depth, gridX, gridY, materialIndex ) {

				const segmentWidth = width / gridX;
				const segmentHeight = height / gridY;

				const widthHalf = width / 2;
				const heightHalf = height / 2;
				const depthHalf = depth / 2;

				const gridX1 = gridX + 1;
				const gridY1 = gridY + 1;

				let vertexCounter = 0;
				let groupCount = 0;

				const vector = new Vector3();

				// generate vertices, normals and uvs

				for ( let iy = 0; iy < gridY1; iy ++ ) {

					const y = iy * segmentHeight - heightHalf;

					for ( let ix = 0; ix < gridX1; ix ++ ) {

						const x = ix * segmentWidth - widthHalf;

						// set values to correct vector component

						vector[ u ] = x * udir;
						vector[ v ] = y * vdir;
						vector[ w ] = depthHalf;

						// now apply vector to vertex buffer

						vertices.push( vector.x, vector.y, vector.z );

						// set values to correct vector component

						vector[ u ] = 0;
						vector[ v ] = 0;
						vector[ w ] = depth > 0 ? 1 : - 1;

						// now apply vector to normal buffer

						normals.push( vector.x, vector.y, vector.z );

						// uvs

						uvs.push( ix / gridX );
						uvs.push( 1 - ( iy / gridY ) );

						// counters

						vertexCounter += 1;

					}

				}

				// indices

				// 1. you need three indices to draw a single face
				// 2. a single segment consists of two faces
				// 3. so we need to generate six (2*3) indices per segment

				for ( let iy = 0; iy < gridY; iy ++ ) {

					for ( let ix = 0; ix < gridX; ix ++ ) {

						const a = numberOfVertices + ix + gridX1 * iy;
						const b = numberOfVertices + ix + gridX1 * ( iy + 1 );
						const c = numberOfVertices + ( ix + 1 ) + gridX1 * ( iy + 1 );
						const d = numberOfVertices + ( ix + 1 ) + gridX1 * iy;

						// faces

						indices.push( a, b, d );
						indices.push( b, c, d );

						// increase counter

						groupCount += 6;

					}

				}

				// add a group to the geometry. this will ensure multi material support

				scope.addGroup( groupStart, groupCount, materialIndex );

				// calculate new start value for groups

				groupStart += groupCount;

				// update total number of vertices

				numberOfVertices += vertexCounter;

			}

		}

	}

	/**
	 * Uniform Utilities
	 */

	function cloneUniforms( src ) {

		const dst = {};

		for ( const u in src ) {

			dst[ u ] = {};

			for ( const p in src[ u ] ) {

				const property = src[ u ][ p ];

				if ( property && ( property.isColor ||
					property.isMatrix3 || property.isMatrix4 ||
					property.isVector2 || property.isVector3 || property.isVector4 ||
					property.isTexture ) ) {

					dst[ u ][ p ] = property.clone();

				} else if ( Array.isArray( property ) ) {

					dst[ u ][ p ] = property.slice();

				} else {

					dst[ u ][ p ] = property;

				}

			}

		}

		return dst;

	}

	function mergeUniforms( uniforms ) {

		const merged = {};

		for ( let u = 0; u < uniforms.length; u ++ ) {

			const tmp = cloneUniforms( uniforms[ u ] );

			for ( const p in tmp ) {

				merged[ p ] = tmp[ p ];

			}

		}

		return merged;

	}

	// Legacy

	const UniformsUtils = { clone: cloneUniforms, merge: mergeUniforms };

	var default_vertex = "void main() {\n\tgl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}";

	var default_fragment = "void main() {\n\tgl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 );\n}";

	/**
	 * parameters = {
	 *  defines: { "label" : "value" },
	 *  uniforms: { "parameter1": { value: 1.0 }, "parameter2": { value2: 2 } },
	 *
	 *  fragmentShader: <string>,
	 *  vertexShader: <string>,
	 *
	 *  wireframe: <boolean>,
	 *  wireframeLinewidth: <float>,
	 *
	 *  lights: <bool>,
	 *
	 *  skinning: <bool>,
	 *  morphTargets: <bool>,
	 *  morphNormals: <bool>
	 * }
	 */

	function ShaderMaterial( parameters ) {

		Material.call( this );

		this.type = 'ShaderMaterial';

		this.defines = {};
		this.uniforms = {};

		this.vertexShader = default_vertex;
		this.fragmentShader = default_fragment;

		this.linewidth = 1;

		this.wireframe = false;
		this.wireframeLinewidth = 1;

		this.fog = false; // set to use scene fog
		this.lights = false; // set to use scene lights
		this.clipping = false; // set to use user-defined clipping planes

		this.skinning = false; // set to use skinning attribute streams
		this.morphTargets = false; // set to use morph targets
		this.morphNormals = false; // set to use morph normals

		this.extensions = {
			derivatives: false, // set to use derivatives
			fragDepth: false, // set to use fragment depth values
			drawBuffers: false, // set to use draw buffers
			shaderTextureLOD: false // set to use shader texture LOD
		};

		// When rendered geometry doesn't include these attributes but the material does,
		// use these default values in WebGL. This avoids errors when buffer data is missing.
		this.defaultAttributeValues = {
			'color': [ 1, 1, 1 ],
			'uv': [ 0, 0 ],
			'uv2': [ 0, 0 ]
		};

		this.index0AttributeName = undefined;
		this.uniformsNeedUpdate = false;

		this.glslVersion = null;

		if ( parameters !== undefined ) {

			if ( parameters.attributes !== undefined ) {

				console.error( 'THREE.ShaderMaterial: attributes should now be defined in THREE.BufferGeometry instead.' );

			}

			this.setValues( parameters );

		}

	}

	ShaderMaterial.prototype = Object.create( Material.prototype );
	ShaderMaterial.prototype.constructor = ShaderMaterial;

	ShaderMaterial.prototype.isShaderMaterial = true;

	ShaderMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.fragmentShader = source.fragmentShader;
		this.vertexShader = source.vertexShader;

		this.uniforms = cloneUniforms( source.uniforms );

		this.defines = Object.assign( {}, source.defines );

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;

		this.lights = source.lights;
		this.clipping = source.clipping;

		this.skinning = source.skinning;

		this.morphTargets = source.morphTargets;
		this.morphNormals = source.morphNormals;

		this.extensions = Object.assign( {}, source.extensions );

		this.glslVersion = source.glslVersion;

		return this;

	};

	ShaderMaterial.prototype.toJSON = function ( meta ) {

		const data = Material.prototype.toJSON.call( this, meta );

		data.glslVersion = this.glslVersion;
		data.uniforms = {};

		for ( const name in this.uniforms ) {

			const uniform = this.uniforms[ name ];
			const value = uniform.value;

			if ( value && value.isTexture ) {

				data.uniforms[ name ] = {
					type: 't',
					value: value.toJSON( meta ).uuid
				};

			} else if ( value && value.isColor ) {

				data.uniforms[ name ] = {
					type: 'c',
					value: value.getHex()
				};

			} else if ( value && value.isVector2 ) {

				data.uniforms[ name ] = {
					type: 'v2',
					value: value.toArray()
				};

			} else if ( value && value.isVector3 ) {

				data.uniforms[ name ] = {
					type: 'v3',
					value: value.toArray()
				};

			} else if ( value && value.isVector4 ) {

				data.uniforms[ name ] = {
					type: 'v4',
					value: value.toArray()
				};

			} else if ( value && value.isMatrix3 ) {

				data.uniforms[ name ] = {
					type: 'm3',
					value: value.toArray()
				};

			} else if ( value && value.isMatrix4 ) {

				data.uniforms[ name ] = {
					type: 'm4',
					value: value.toArray()
				};

			} else {

				data.uniforms[ name ] = {
					value: value
				};

				// note: the array variants v2v, v3v, v4v, m4v and tv are not supported so far

			}

		}

		if ( Object.keys( this.defines ).length > 0 ) data.defines = this.defines;

		data.vertexShader = this.vertexShader;
		data.fragmentShader = this.fragmentShader;

		const extensions = {};

		for ( const key in this.extensions ) {

			if ( this.extensions[ key ] === true ) extensions[ key ] = true;

		}

		if ( Object.keys( extensions ).length > 0 ) data.extensions = extensions;

		return data;

	};

	function Camera() {

		Object3D.call( this );

		this.type = 'Camera';

		this.matrixWorldInverse = new Matrix4();

		this.projectionMatrix = new Matrix4();
		this.projectionMatrixInverse = new Matrix4();

	}

	Camera.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: Camera,

		isCamera: true,

		copy: function ( source, recursive ) {

			Object3D.prototype.copy.call( this, source, recursive );

			this.matrixWorldInverse.copy( source.matrixWorldInverse );

			this.projectionMatrix.copy( source.projectionMatrix );
			this.projectionMatrixInverse.copy( source.projectionMatrixInverse );

			return this;

		},

		getWorldDirection: function ( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Camera: .getWorldDirection() target is now required' );
				target = new Vector3();

			}

			this.updateWorldMatrix( true, false );

			const e = this.matrixWorld.elements;

			return target.set( - e[ 8 ], - e[ 9 ], - e[ 10 ] ).normalize();

		},

		updateMatrixWorld: function ( force ) {

			Object3D.prototype.updateMatrixWorld.call( this, force );

			this.matrixWorldInverse.copy( this.matrixWorld ).invert();

		},

		updateWorldMatrix: function ( updateParents, updateChildren ) {

			Object3D.prototype.updateWorldMatrix.call( this, updateParents, updateChildren );

			this.matrixWorldInverse.copy( this.matrixWorld ).invert();

		},

		clone: function () {

			return new this.constructor().copy( this );

		}

	} );

	function PerspectiveCamera( fov = 50, aspect = 1, near = 0.1, far = 2000 ) {

		Camera.call( this );

		this.type = 'PerspectiveCamera';

		this.fov = fov;
		this.zoom = 1;

		this.near = near;
		this.far = far;
		this.focus = 10;

		this.aspect = aspect;
		this.view = null;

		this.filmGauge = 35;	// width of the film (default in millimeters)
		this.filmOffset = 0;	// horizontal film offset (same unit as gauge)

		this.updateProjectionMatrix();

	}

	PerspectiveCamera.prototype = Object.assign( Object.create( Camera.prototype ), {

		constructor: PerspectiveCamera,

		isPerspectiveCamera: true,

		copy: function ( source, recursive ) {

			Camera.prototype.copy.call( this, source, recursive );

			this.fov = source.fov;
			this.zoom = source.zoom;

			this.near = source.near;
			this.far = source.far;
			this.focus = source.focus;

			this.aspect = source.aspect;
			this.view = source.view === null ? null : Object.assign( {}, source.view );

			this.filmGauge = source.filmGauge;
			this.filmOffset = source.filmOffset;

			return this;

		},

		/**
		 * Sets the FOV by focal length in respect to the current .filmGauge.
		 *
		 * The default film gauge is 35, so that the focal length can be specified for
		 * a 35mm (full frame) camera.
		 *
		 * Values for focal length and film gauge must have the same unit.
		 */
		setFocalLength: function ( focalLength ) {

			// see http://www.bobatkins.com/photography/technical/field_of_view.html
			const vExtentSlope = 0.5 * this.getFilmHeight() / focalLength;

			this.fov = MathUtils.RAD2DEG * 2 * Math.atan( vExtentSlope );
			this.updateProjectionMatrix();

		},

		/**
		 * Calculates the focal length from the current .fov and .filmGauge.
		 */
		getFocalLength: function () {

			const vExtentSlope = Math.tan( MathUtils.DEG2RAD * 0.5 * this.fov );

			return 0.5 * this.getFilmHeight() / vExtentSlope;

		},

		getEffectiveFOV: function () {

			return MathUtils.RAD2DEG * 2 * Math.atan(
				Math.tan( MathUtils.DEG2RAD * 0.5 * this.fov ) / this.zoom );

		},

		getFilmWidth: function () {

			// film not completely covered in portrait format (aspect < 1)
			return this.filmGauge * Math.min( this.aspect, 1 );

		},

		getFilmHeight: function () {

			// film not completely covered in landscape format (aspect > 1)
			return this.filmGauge / Math.max( this.aspect, 1 );

		},

		/**
		 * Sets an offset in a larger frustum. This is useful for multi-window or
		 * multi-monitor/multi-machine setups.
		 *
		 * For example, if you have 3x2 monitors and each monitor is 1920x1080 and
		 * the monitors are in grid like this
		 *
		 *   +---+---+---+
		 *   | A | B | C |
		 *   +---+---+---+
		 *   | D | E | F |
		 *   +---+---+---+
		 *
		 * then for each monitor you would call it like this
		 *
		 *   const w = 1920;
		 *   const h = 1080;
		 *   const fullWidth = w * 3;
		 *   const fullHeight = h * 2;
		 *
		 *   --A--
		 *   camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 0, w, h );
		 *   --B--
		 *   camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 0, w, h );
		 *   --C--
		 *   camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 0, w, h );
		 *   --D--
		 *   camera.setViewOffset( fullWidth, fullHeight, w * 0, h * 1, w, h );
		 *   --E--
		 *   camera.setViewOffset( fullWidth, fullHeight, w * 1, h * 1, w, h );
		 *   --F--
		 *   camera.setViewOffset( fullWidth, fullHeight, w * 2, h * 1, w, h );
		 *
		 *   Note there is no reason monitors have to be the same size or in a grid.
		 */
		setViewOffset: function ( fullWidth, fullHeight, x, y, width, height ) {

			this.aspect = fullWidth / fullHeight;

			if ( this.view === null ) {

				this.view = {
					enabled: true,
					fullWidth: 1,
					fullHeight: 1,
					offsetX: 0,
					offsetY: 0,
					width: 1,
					height: 1
				};

			}

			this.view.enabled = true;
			this.view.fullWidth = fullWidth;
			this.view.fullHeight = fullHeight;
			this.view.offsetX = x;
			this.view.offsetY = y;
			this.view.width = width;
			this.view.height = height;

			this.updateProjectionMatrix();

		},

		clearViewOffset: function () {

			if ( this.view !== null ) {

				this.view.enabled = false;

			}

			this.updateProjectionMatrix();

		},

		updateProjectionMatrix: function () {

			const near = this.near;
			let top = near * Math.tan( MathUtils.DEG2RAD * 0.5 * this.fov ) / this.zoom;
			let height = 2 * top;
			let width = this.aspect * height;
			let left = - 0.5 * width;
			const view = this.view;

			if ( this.view !== null && this.view.enabled ) {

				const fullWidth = view.fullWidth,
					fullHeight = view.fullHeight;

				left += view.offsetX * width / fullWidth;
				top -= view.offsetY * height / fullHeight;
				width *= view.width / fullWidth;
				height *= view.height / fullHeight;

			}

			const skew = this.filmOffset;
			if ( skew !== 0 ) left += near * skew / this.getFilmWidth();

			this.projectionMatrix.makePerspective( left, left + width, top, top - height, near, this.far );

			this.projectionMatrixInverse.copy( this.projectionMatrix ).invert();

		},

		toJSON: function ( meta ) {

			const data = Object3D.prototype.toJSON.call( this, meta );

			data.object.fov = this.fov;
			data.object.zoom = this.zoom;

			data.object.near = this.near;
			data.object.far = this.far;
			data.object.focus = this.focus;

			data.object.aspect = this.aspect;

			if ( this.view !== null ) data.object.view = Object.assign( {}, this.view );

			data.object.filmGauge = this.filmGauge;
			data.object.filmOffset = this.filmOffset;

			return data;

		}

	} );

	const fov = 90, aspect = 1;

	function CubeCamera( near, far, renderTarget ) {

		Object3D.call( this );

		this.type = 'CubeCamera';

		if ( renderTarget.isWebGLCubeRenderTarget !== true ) {

			console.error( 'THREE.CubeCamera: The constructor now expects an instance of WebGLCubeRenderTarget as third parameter.' );
			return;

		}

		this.renderTarget = renderTarget;

		const cameraPX = new PerspectiveCamera( fov, aspect, near, far );
		cameraPX.layers = this.layers;
		cameraPX.up.set( 0, - 1, 0 );
		cameraPX.lookAt( new Vector3( 1, 0, 0 ) );
		this.add( cameraPX );

		const cameraNX = new PerspectiveCamera( fov, aspect, near, far );
		cameraNX.layers = this.layers;
		cameraNX.up.set( 0, - 1, 0 );
		cameraNX.lookAt( new Vector3( - 1, 0, 0 ) );
		this.add( cameraNX );

		const cameraPY = new PerspectiveCamera( fov, aspect, near, far );
		cameraPY.layers = this.layers;
		cameraPY.up.set( 0, 0, 1 );
		cameraPY.lookAt( new Vector3( 0, 1, 0 ) );
		this.add( cameraPY );

		const cameraNY = new PerspectiveCamera( fov, aspect, near, far );
		cameraNY.layers = this.layers;
		cameraNY.up.set( 0, 0, - 1 );
		cameraNY.lookAt( new Vector3( 0, - 1, 0 ) );
		this.add( cameraNY );

		const cameraPZ = new PerspectiveCamera( fov, aspect, near, far );
		cameraPZ.layers = this.layers;
		cameraPZ.up.set( 0, - 1, 0 );
		cameraPZ.lookAt( new Vector3( 0, 0, 1 ) );
		this.add( cameraPZ );

		const cameraNZ = new PerspectiveCamera( fov, aspect, near, far );
		cameraNZ.layers = this.layers;
		cameraNZ.up.set( 0, - 1, 0 );
		cameraNZ.lookAt( new Vector3( 0, 0, - 1 ) );
		this.add( cameraNZ );

		this.update = function ( renderer, scene ) {

			if ( this.parent === null ) this.updateMatrixWorld();

			const currentXrEnabled = renderer.xr.enabled;
			const currentRenderTarget = renderer.getRenderTarget();

			renderer.xr.enabled = false;

			const generateMipmaps = renderTarget.texture.generateMipmaps;

			renderTarget.texture.generateMipmaps = false;

			renderer.setRenderTarget( renderTarget, 0 );
			renderer.render( scene, cameraPX );

			renderer.setRenderTarget( renderTarget, 1 );
			renderer.render( scene, cameraNX );

			renderer.setRenderTarget( renderTarget, 2 );
			renderer.render( scene, cameraPY );

			renderer.setRenderTarget( renderTarget, 3 );
			renderer.render( scene, cameraNY );

			renderer.setRenderTarget( renderTarget, 4 );
			renderer.render( scene, cameraPZ );

			renderTarget.texture.generateMipmaps = generateMipmaps;

			renderer.setRenderTarget( renderTarget, 5 );
			renderer.render( scene, cameraNZ );

			renderer.setRenderTarget( currentRenderTarget );

			renderer.xr.enabled = currentXrEnabled;

		};

	}

	CubeCamera.prototype = Object.create( Object3D.prototype );
	CubeCamera.prototype.constructor = CubeCamera;

	function CubeTexture( images, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, encoding ) {

		images = images !== undefined ? images : [];
		mapping = mapping !== undefined ? mapping : CubeReflectionMapping;
		format = format !== undefined ? format : RGBFormat;

		Texture.call( this, images, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, encoding );

		this.flipY = false;

		// Why CubeTexture._needsFlipEnvMap is necessary:
		//
		// By convention -- likely based on the RenderMan spec from the 1990's -- cube maps are specified by WebGL (and three.js)
		// in a coordinate system in which positive-x is to the right when looking up the positive-z axis -- in other words,
		// in a left-handed coordinate system. By continuing this convention, preexisting cube maps continued to render correctly.

		// three.js uses a right-handed coordinate system. So environment maps used in three.js appear to have px and nx swapped
		// and the flag _needsFlipEnvMap controls this conversion. The flip is not required (and thus _needsFlipEnvMap is set to false)
		// when using WebGLCubeRenderTarget.texture as a cube texture.

		this._needsFlipEnvMap = true;

	}

	CubeTexture.prototype = Object.create( Texture.prototype );
	CubeTexture.prototype.constructor = CubeTexture;

	CubeTexture.prototype.isCubeTexture = true;

	Object.defineProperty( CubeTexture.prototype, 'images', {

		get: function () {

			return this.image;

		},

		set: function ( value ) {

			this.image = value;

		}

	} );

	function WebGLCubeRenderTarget( size, options, dummy ) {

		if ( Number.isInteger( options ) ) {

			console.warn( 'THREE.WebGLCubeRenderTarget: constructor signature is now WebGLCubeRenderTarget( size, options )' );

			options = dummy;

		}

		WebGLRenderTarget.call( this, size, size, options );

		options = options || {};

		this.texture = new CubeTexture( undefined, options.mapping, options.wrapS, options.wrapT, options.magFilter, options.minFilter, options.format, options.type, options.anisotropy, options.encoding );

		this.texture._needsFlipEnvMap = false;

	}

	WebGLCubeRenderTarget.prototype = Object.create( WebGLRenderTarget.prototype );
	WebGLCubeRenderTarget.prototype.constructor = WebGLCubeRenderTarget;

	WebGLCubeRenderTarget.prototype.isWebGLCubeRenderTarget = true;

	WebGLCubeRenderTarget.prototype.fromEquirectangularTexture = function ( renderer, texture ) {

		this.texture.type = texture.type;
		this.texture.format = RGBAFormat; // see #18859
		this.texture.encoding = texture.encoding;

		this.texture.generateMipmaps = texture.generateMipmaps;
		this.texture.minFilter = texture.minFilter;
		this.texture.magFilter = texture.magFilter;

		const shader = {

			uniforms: {
				tEquirect: { value: null },
			},

			vertexShader: /* glsl */`

			varying vec3 vWorldDirection;

			vec3 transformDirection( in vec3 dir, in mat4 matrix ) {

				return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );

			}

			void main() {

				vWorldDirection = transformDirection( position, modelMatrix );

				#include <begin_vertex>
				#include <project_vertex>

			}
		`,

			fragmentShader: /* glsl */`

			uniform sampler2D tEquirect;

			varying vec3 vWorldDirection;

			#include <common>

			void main() {

				vec3 direction = normalize( vWorldDirection );

				vec2 sampleUV = equirectUv( direction );

				gl_FragColor = texture2D( tEquirect, sampleUV );

			}
		`
		};

		const geometry = new BoxBufferGeometry( 5, 5, 5 );

		const material = new ShaderMaterial( {

			name: 'CubemapFromEquirect',

			uniforms: cloneUniforms( shader.uniforms ),
			vertexShader: shader.vertexShader,
			fragmentShader: shader.fragmentShader,
			side: BackSide,
			blending: NoBlending

		} );

		material.uniforms.tEquirect.value = texture;

		const mesh = new Mesh( geometry, material );

		const currentMinFilter = texture.minFilter;

		// Avoid blurred poles
		if ( texture.minFilter === LinearMipmapLinearFilter ) texture.minFilter = LinearFilter;

		const camera = new CubeCamera( 1, 10, this );
		camera.update( renderer, mesh );

		texture.minFilter = currentMinFilter;

		mesh.geometry.dispose();
		mesh.material.dispose();

		return this;

	};

	WebGLCubeRenderTarget.prototype.clear = function ( renderer, color, depth, stencil ) {

		const currentRenderTarget = renderer.getRenderTarget();

		for ( let i = 0; i < 6; i ++ ) {

			renderer.setRenderTarget( this, i );

			renderer.clear( color, depth, stencil );

		}

		renderer.setRenderTarget( currentRenderTarget );

	};

	function DataTexture( data, width, height, format, type, mapping, wrapS, wrapT, magFilter, minFilter, anisotropy, encoding ) {

		Texture.call( this, null, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, encoding );

		this.image = { data: data || null, width: width || 1, height: height || 1 };

		this.magFilter = magFilter !== undefined ? magFilter : NearestFilter;
		this.minFilter = minFilter !== undefined ? minFilter : NearestFilter;

		this.generateMipmaps = false;
		this.flipY = false;
		this.unpackAlignment = 1;

		this.needsUpdate = true;

	}

	DataTexture.prototype = Object.create( Texture.prototype );
	DataTexture.prototype.constructor = DataTexture;

	DataTexture.prototype.isDataTexture = true;

	const _sphere$1 = /*@__PURE__*/ new Sphere();
	const _vector$5 = /*@__PURE__*/ new Vector3();

	class Frustum {

		constructor( p0, p1, p2, p3, p4, p5 ) {

			this.planes = [

				( p0 !== undefined ) ? p0 : new Plane(),
				( p1 !== undefined ) ? p1 : new Plane(),
				( p2 !== undefined ) ? p2 : new Plane(),
				( p3 !== undefined ) ? p3 : new Plane(),
				( p4 !== undefined ) ? p4 : new Plane(),
				( p5 !== undefined ) ? p5 : new Plane()

			];

		}

		set( p0, p1, p2, p3, p4, p5 ) {

			const planes = this.planes;

			planes[ 0 ].copy( p0 );
			planes[ 1 ].copy( p1 );
			planes[ 2 ].copy( p2 );
			planes[ 3 ].copy( p3 );
			planes[ 4 ].copy( p4 );
			planes[ 5 ].copy( p5 );

			return this;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( frustum ) {

			const planes = this.planes;

			for ( let i = 0; i < 6; i ++ ) {

				planes[ i ].copy( frustum.planes[ i ] );

			}

			return this;

		}

		setFromProjectionMatrix( m ) {

			const planes = this.planes;
			const me = m.elements;
			const me0 = me[ 0 ], me1 = me[ 1 ], me2 = me[ 2 ], me3 = me[ 3 ];
			const me4 = me[ 4 ], me5 = me[ 5 ], me6 = me[ 6 ], me7 = me[ 7 ];
			const me8 = me[ 8 ], me9 = me[ 9 ], me10 = me[ 10 ], me11 = me[ 11 ];
			const me12 = me[ 12 ], me13 = me[ 13 ], me14 = me[ 14 ], me15 = me[ 15 ];

			planes[ 0 ].setComponents( me3 - me0, me7 - me4, me11 - me8, me15 - me12 ).normalize();
			planes[ 1 ].setComponents( me3 + me0, me7 + me4, me11 + me8, me15 + me12 ).normalize();
			planes[ 2 ].setComponents( me3 + me1, me7 + me5, me11 + me9, me15 + me13 ).normalize();
			planes[ 3 ].setComponents( me3 - me1, me7 - me5, me11 - me9, me15 - me13 ).normalize();
			planes[ 4 ].setComponents( me3 - me2, me7 - me6, me11 - me10, me15 - me14 ).normalize();
			planes[ 5 ].setComponents( me3 + me2, me7 + me6, me11 + me10, me15 + me14 ).normalize();

			return this;

		}

		intersectsObject( object ) {

			const geometry = object.geometry;

			if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();

			_sphere$1.copy( geometry.boundingSphere ).applyMatrix4( object.matrixWorld );

			return this.intersectsSphere( _sphere$1 );

		}

		intersectsSprite( sprite ) {

			_sphere$1.center.set( 0, 0, 0 );
			_sphere$1.radius = 0.7071067811865476;
			_sphere$1.applyMatrix4( sprite.matrixWorld );

			return this.intersectsSphere( _sphere$1 );

		}

		intersectsSphere( sphere ) {

			const planes = this.planes;
			const center = sphere.center;
			const negRadius = - sphere.radius;

			for ( let i = 0; i < 6; i ++ ) {

				const distance = planes[ i ].distanceToPoint( center );

				if ( distance < negRadius ) {

					return false;

				}

			}

			return true;

		}

		intersectsBox( box ) {

			const planes = this.planes;

			for ( let i = 0; i < 6; i ++ ) {

				const plane = planes[ i ];

				// corner at max distance

				_vector$5.x = plane.normal.x > 0 ? box.max.x : box.min.x;
				_vector$5.y = plane.normal.y > 0 ? box.max.y : box.min.y;
				_vector$5.z = plane.normal.z > 0 ? box.max.z : box.min.z;

				if ( plane.distanceToPoint( _vector$5 ) < 0 ) {

					return false;

				}

			}

			return true;

		}

		containsPoint( point ) {

			const planes = this.planes;

			for ( let i = 0; i < 6; i ++ ) {

				if ( planes[ i ].distanceToPoint( point ) < 0 ) {

					return false;

				}

			}

			return true;

		}

	}

	function WebGLAnimation() {

		let context = null;
		let isAnimating = false;
		let animationLoop = null;
		let requestId = null;

		function onAnimationFrame( time, frame ) {

			animationLoop( time, frame );

			requestId = context.requestAnimationFrame( onAnimationFrame );

		}

		return {

			start: function () {

				if ( isAnimating === true ) return;
				if ( animationLoop === null ) return;

				requestId = context.requestAnimationFrame( onAnimationFrame );

				isAnimating = true;

			},

			stop: function () {

				context.cancelAnimationFrame( requestId );

				isAnimating = false;

			},

			setAnimationLoop: function ( callback ) {

				animationLoop = callback;

			},

			setContext: function ( value ) {

				context = value;

			}

		};

	}

	function WebGLAttributes( gl, capabilities ) {

		const isWebGL2 = capabilities.isWebGL2;

		const buffers = new WeakMap();

		function createBuffer( attribute, bufferType ) {

			const array = attribute.array;
			const usage = attribute.usage;

			const buffer = gl.createBuffer();

			gl.bindBuffer( bufferType, buffer );
			gl.bufferData( bufferType, array, usage );

			attribute.onUploadCallback();

			let type = 5126;

			if ( array instanceof Float32Array ) {

				type = 5126;

			} else if ( array instanceof Float64Array ) {

				console.warn( 'THREE.WebGLAttributes: Unsupported data buffer format: Float64Array.' );

			} else if ( array instanceof Uint16Array ) {

				if ( attribute.isFloat16BufferAttribute ) {

					if ( isWebGL2 ) {

						type = 5131;

					} else {

						console.warn( 'THREE.WebGLAttributes: Usage of Float16BufferAttribute requires WebGL2.' );

					}

				} else {

					type = 5123;

				}

			} else if ( array instanceof Int16Array ) {

				type = 5122;

			} else if ( array instanceof Uint32Array ) {

				type = 5125;

			} else if ( array instanceof Int32Array ) {

				type = 5124;

			} else if ( array instanceof Int8Array ) {

				type = 5120;

			} else if ( array instanceof Uint8Array ) {

				type = 5121;

			}

			return {
				buffer: buffer,
				type: type,
				bytesPerElement: array.BYTES_PER_ELEMENT,
				version: attribute.version
			};

		}

		function updateBuffer( buffer, attribute, bufferType ) {

			const array = attribute.array;
			const updateRange = attribute.updateRange;

			gl.bindBuffer( bufferType, buffer );

			if ( updateRange.count === - 1 ) {

				// Not using update ranges

				gl.bufferSubData( bufferType, 0, array );

			} else {

				if ( isWebGL2 ) {

					gl.bufferSubData( bufferType, updateRange.offset * array.BYTES_PER_ELEMENT,
						array, updateRange.offset, updateRange.count );

				} else {

					gl.bufferSubData( bufferType, updateRange.offset * array.BYTES_PER_ELEMENT,
						array.subarray( updateRange.offset, updateRange.offset + updateRange.count ) );

				}

				updateRange.count = - 1; // reset range

			}

		}

		//

		function get( attribute ) {

			if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;

			return buffers.get( attribute );

		}

		function remove( attribute ) {

			if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;

			const data = buffers.get( attribute );

			if ( data ) {

				gl.deleteBuffer( data.buffer );

				buffers.delete( attribute );

			}

		}

		function update( attribute, bufferType ) {

			if ( attribute.isGLBufferAttribute ) {

				const cached = buffers.get( attribute );

				if ( ! cached || cached.version < attribute.version ) {

					buffers.set( attribute, {
						buffer: attribute.buffer,
						type: attribute.type,
						bytesPerElement: attribute.elementSize,
						version: attribute.version
					} );

				}

				return;

			}

			if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;

			const data = buffers.get( attribute );

			if ( data === undefined ) {

				buffers.set( attribute, createBuffer( attribute, bufferType ) );

			} else if ( data.version < attribute.version ) {

				updateBuffer( data.buffer, attribute, bufferType );

				data.version = attribute.version;

			}

		}

		return {

			get: get,
			remove: remove,
			update: update

		};

	}

	class PlaneBufferGeometry extends BufferGeometry {

		constructor( width = 1, height = 1, widthSegments = 1, heightSegments = 1 ) {

			super();
			this.type = 'PlaneBufferGeometry';

			this.parameters = {
				width: width,
				height: height,
				widthSegments: widthSegments,
				heightSegments: heightSegments
			};

			const width_half = width / 2;
			const height_half = height / 2;

			const gridX = Math.floor( widthSegments );
			const gridY = Math.floor( heightSegments );

			const gridX1 = gridX + 1;
			const gridY1 = gridY + 1;

			const segment_width = width / gridX;
			const segment_height = height / gridY;

			//

			const indices = [];
			const vertices = [];
			const normals = [];
			const uvs = [];

			for ( let iy = 0; iy < gridY1; iy ++ ) {

				const y = iy * segment_height - height_half;

				for ( let ix = 0; ix < gridX1; ix ++ ) {

					const x = ix * segment_width - width_half;

					vertices.push( x, - y, 0 );

					normals.push( 0, 0, 1 );

					uvs.push( ix / gridX );
					uvs.push( 1 - ( iy / gridY ) );

				}

			}

			for ( let iy = 0; iy < gridY; iy ++ ) {

				for ( let ix = 0; ix < gridX; ix ++ ) {

					const a = ix + gridX1 * iy;
					const b = ix + gridX1 * ( iy + 1 );
					const c = ( ix + 1 ) + gridX1 * ( iy + 1 );
					const d = ( ix + 1 ) + gridX1 * iy;

					indices.push( a, b, d );
					indices.push( b, c, d );

				}

			}

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

		}

	}

	var alphamap_fragment = "#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, vUv ).g;\n#endif";

	var alphamap_pars_fragment = "#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif";

	var alphatest_fragment = "#ifdef ALPHATEST\n\tif ( diffuseColor.a < ALPHATEST ) discard;\n#endif";

	var aomap_fragment = "#ifdef USE_AOMAP\n\tfloat ambientOcclusion = ( texture2D( aoMap, vUv2 ).r - 1.0 ) * aoMapIntensity + 1.0;\n\treflectedLight.indirectDiffuse *= ambientOcclusion;\n\t#if defined( USE_ENVMAP ) && defined( STANDARD )\n\t\tfloat dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );\n\t\treflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.specularRoughness );\n\t#endif\n#endif";

	var aomap_pars_fragment = "#ifdef USE_AOMAP\n\tuniform sampler2D aoMap;\n\tuniform float aoMapIntensity;\n#endif";

	var begin_vertex = "vec3 transformed = vec3( position );";

	var beginnormal_vertex = "vec3 objectNormal = vec3( normal );\n#ifdef USE_TANGENT\n\tvec3 objectTangent = vec3( tangent.xyz );\n#endif";

	var bsdfs = "vec2 integrateSpecularBRDF( const in float dotNV, const in float roughness ) {\n\tconst vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 );\n\tconst vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 );\n\tvec4 r = roughness * c0 + c1;\n\tfloat a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y;\n\treturn vec2( -1.04, 1.04 ) * a004 + r.zw;\n}\nfloat punctualLightIntensityToIrradianceFactor( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) {\n#if defined ( PHYSICALLY_CORRECT_LIGHTS )\n\tfloat distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 );\n\tif( cutoffDistance > 0.0 ) {\n\t\tdistanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\n\t}\n\treturn distanceFalloff;\n#else\n\tif( cutoffDistance > 0.0 && decayExponent > 0.0 ) {\n\t\treturn pow( saturate( -lightDistance / cutoffDistance + 1.0 ), decayExponent );\n\t}\n\treturn 1.0;\n#endif\n}\nvec3 BRDF_Diffuse_Lambert( const in vec3 diffuseColor ) {\n\treturn RECIPROCAL_PI * diffuseColor;\n}\nvec3 F_Schlick( const in vec3 specularColor, const in float dotLH ) {\n\tfloat fresnel = exp2( ( -5.55473 * dotLH - 6.98316 ) * dotLH );\n\treturn ( 1.0 - specularColor ) * fresnel + specularColor;\n}\nvec3 F_Schlick_RoughnessDependent( const in vec3 F0, const in float dotNV, const in float roughness ) {\n\tfloat fresnel = exp2( ( -5.55473 * dotNV - 6.98316 ) * dotNV );\n\tvec3 Fr = max( vec3( 1.0 - roughness ), F0 ) - F0;\n\treturn Fr * fresnel + F0;\n}\nfloat G_GGX_Smith( const in float alpha, const in float dotNL, const in float dotNV ) {\n\tfloat a2 = pow2( alpha );\n\tfloat gl = dotNL + sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\n\tfloat gv = dotNV + sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\n\treturn 1.0 / ( gl * gv );\n}\nfloat G_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) {\n\tfloat a2 = pow2( alpha );\n\tfloat gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\n\tfloat gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\n\treturn 0.5 / max( gv + gl, EPSILON );\n}\nfloat D_GGX( const in float alpha, const in float dotNH ) {\n\tfloat a2 = pow2( alpha );\n\tfloat denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0;\n\treturn RECIPROCAL_PI * a2 / pow2( denom );\n}\nvec3 BRDF_Specular_GGX( const in IncidentLight incidentLight, const in vec3 viewDir, const in vec3 normal, const in vec3 specularColor, const in float roughness ) {\n\tfloat alpha = pow2( roughness );\n\tvec3 halfDir = normalize( incidentLight.direction + viewDir );\n\tfloat dotNL = saturate( dot( normal, incidentLight.direction ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat dotLH = saturate( dot( incidentLight.direction, halfDir ) );\n\tvec3 F = F_Schlick( specularColor, dotLH );\n\tfloat G = G_GGX_SmithCorrelated( alpha, dotNL, dotNV );\n\tfloat D = D_GGX( alpha, dotNH );\n\treturn F * ( G * D );\n}\nvec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) {\n\tconst float LUT_SIZE = 64.0;\n\tconst float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;\n\tconst float LUT_BIAS = 0.5 / LUT_SIZE;\n\tfloat dotNV = saturate( dot( N, V ) );\n\tvec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) );\n\tuv = uv * LUT_SCALE + LUT_BIAS;\n\treturn uv;\n}\nfloat LTC_ClippedSphereFormFactor( const in vec3 f ) {\n\tfloat l = length( f );\n\treturn max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 );\n}\nvec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) {\n\tfloat x = dot( v1, v2 );\n\tfloat y = abs( x );\n\tfloat a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y;\n\tfloat b = 3.4175940 + ( 4.1616724 + y ) * y;\n\tfloat v = a / b;\n\tfloat theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v;\n\treturn cross( v1, v2 ) * theta_sintheta;\n}\nvec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) {\n\tvec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ];\n\tvec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ];\n\tvec3 lightNormal = cross( v1, v2 );\n\tif( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 );\n\tvec3 T1, T2;\n\tT1 = normalize( V - N * dot( V, N ) );\n\tT2 = - cross( N, T1 );\n\tmat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) );\n\tvec3 coords[ 4 ];\n\tcoords[ 0 ] = mat * ( rectCoords[ 0 ] - P );\n\tcoords[ 1 ] = mat * ( rectCoords[ 1 ] - P );\n\tcoords[ 2 ] = mat * ( rectCoords[ 2 ] - P );\n\tcoords[ 3 ] = mat * ( rectCoords[ 3 ] - P );\n\tcoords[ 0 ] = normalize( coords[ 0 ] );\n\tcoords[ 1 ] = normalize( coords[ 1 ] );\n\tcoords[ 2 ] = normalize( coords[ 2 ] );\n\tcoords[ 3 ] = normalize( coords[ 3 ] );\n\tvec3 vectorFormFactor = vec3( 0.0 );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] );\n\tfloat result = LTC_ClippedSphereFormFactor( vectorFormFactor );\n\treturn vec3( result );\n}\nvec3 BRDF_Specular_GGX_Environment( const in vec3 viewDir, const in vec3 normal, const in vec3 specularColor, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tvec2 brdf = integrateSpecularBRDF( dotNV, roughness );\n\treturn specularColor * brdf.x + brdf.y;\n}\nvoid BRDF_Specular_Multiscattering_Environment( const in GeometricContext geometry, const in vec3 specularColor, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n\tfloat dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );\n\tvec3 F = F_Schlick_RoughnessDependent( specularColor, dotNV, roughness );\n\tvec2 brdf = integrateSpecularBRDF( dotNV, roughness );\n\tvec3 FssEss = F * brdf.x + brdf.y;\n\tfloat Ess = brdf.x + brdf.y;\n\tfloat Ems = 1.0 - Ess;\n\tvec3 Favg = specularColor + ( 1.0 - specularColor ) * 0.047619;\tvec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );\n\tsingleScatter += FssEss;\n\tmultiScatter += Fms * Ems;\n}\nfloat G_BlinnPhong_Implicit( ) {\n\treturn 0.25;\n}\nfloat D_BlinnPhong( const in float shininess, const in float dotNH ) {\n\treturn RECIPROCAL_PI * ( shininess * 0.5 + 1.0 ) * pow( dotNH, shininess );\n}\nvec3 BRDF_Specular_BlinnPhong( const in IncidentLight incidentLight, const in GeometricContext geometry, const in vec3 specularColor, const in float shininess ) {\n\tvec3 halfDir = normalize( incidentLight.direction + geometry.viewDir );\n\tfloat dotNH = saturate( dot( geometry.normal, halfDir ) );\n\tfloat dotLH = saturate( dot( incidentLight.direction, halfDir ) );\n\tvec3 F = F_Schlick( specularColor, dotLH );\n\tfloat G = G_BlinnPhong_Implicit( );\n\tfloat D = D_BlinnPhong( shininess, dotNH );\n\treturn F * ( G * D );\n}\nfloat GGXRoughnessToBlinnExponent( const in float ggxRoughness ) {\n\treturn ( 2.0 / pow2( ggxRoughness + 0.0001 ) - 2.0 );\n}\nfloat BlinnExponentToGGXRoughness( const in float blinnExponent ) {\n\treturn sqrt( 2.0 / ( blinnExponent + 2.0 ) );\n}\n#if defined( USE_SHEEN )\nfloat D_Charlie(float roughness, float NoH) {\n\tfloat invAlpha = 1.0 / roughness;\n\tfloat cos2h = NoH * NoH;\n\tfloat sin2h = max(1.0 - cos2h, 0.0078125);\treturn (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / (2.0 * PI);\n}\nfloat V_Neubelt(float NoV, float NoL) {\n\treturn saturate(1.0 / (4.0 * (NoL + NoV - NoL * NoV)));\n}\nvec3 BRDF_Specular_Sheen( const in float roughness, const in vec3 L, const in GeometricContext geometry, vec3 specularColor ) {\n\tvec3 N = geometry.normal;\n\tvec3 V = geometry.viewDir;\n\tvec3 H = normalize( V + L );\n\tfloat dotNH = saturate( dot( N, H ) );\n\treturn specularColor * D_Charlie( roughness, dotNH ) * V_Neubelt( dot(N, V), dot(N, L) );\n}\n#endif";

	var bumpmap_pars_fragment = "#ifdef USE_BUMPMAP\n\tuniform sampler2D bumpMap;\n\tuniform float bumpScale;\n\tvec2 dHdxy_fwd() {\n\t\tvec2 dSTdx = dFdx( vUv );\n\t\tvec2 dSTdy = dFdy( vUv );\n\t\tfloat Hll = bumpScale * texture2D( bumpMap, vUv ).x;\n\t\tfloat dBx = bumpScale * texture2D( bumpMap, vUv + dSTdx ).x - Hll;\n\t\tfloat dBy = bumpScale * texture2D( bumpMap, vUv + dSTdy ).x - Hll;\n\t\treturn vec2( dBx, dBy );\n\t}\n\tvec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy ) {\n\t\tvec3 vSigmaX = vec3( dFdx( surf_pos.x ), dFdx( surf_pos.y ), dFdx( surf_pos.z ) );\n\t\tvec3 vSigmaY = vec3( dFdy( surf_pos.x ), dFdy( surf_pos.y ), dFdy( surf_pos.z ) );\n\t\tvec3 vN = surf_norm;\n\t\tvec3 R1 = cross( vSigmaY, vN );\n\t\tvec3 R2 = cross( vN, vSigmaX );\n\t\tfloat fDet = dot( vSigmaX, R1 );\n\t\tfDet *= ( float( gl_FrontFacing ) * 2.0 - 1.0 );\n\t\tvec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );\n\t\treturn normalize( abs( fDet ) * surf_norm - vGrad );\n\t}\n#endif";

	var clipping_planes_fragment = "#if NUM_CLIPPING_PLANES > 0\n\tvec4 plane;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\tplane = clippingPlanes[ i ];\n\t\tif ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;\n\t}\n\t#pragma unroll_loop_end\n\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\tbool clipped = true;\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tclipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\tif ( clipped ) discard;\n\t#endif\n#endif";

	var clipping_planes_pars_fragment = "#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n\tuniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];\n#endif";

	var clipping_planes_pars_vertex = "#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n#endif";

	var clipping_planes_vertex = "#if NUM_CLIPPING_PLANES > 0\n\tvClipPosition = - mvPosition.xyz;\n#endif";

	var color_fragment = "#ifdef USE_COLOR\n\tdiffuseColor.rgb *= vColor;\n#endif";

	var color_pars_fragment = "#ifdef USE_COLOR\n\tvarying vec3 vColor;\n#endif";

	var color_pars_vertex = "#if defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR )\n\tvarying vec3 vColor;\n#endif";

	var color_vertex = "#if defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR )\n\tvColor = vec3( 1.0 );\n#endif\n#ifdef USE_COLOR\n\tvColor.xyz *= color.xyz;\n#endif\n#ifdef USE_INSTANCING_COLOR\n\tvColor.xyz *= instanceColor.xyz;\n#endif";

	var common = "#define PI 3.141592653589793\n#define PI2 6.283185307179586\n#define PI_HALF 1.5707963267948966\n#define RECIPROCAL_PI 0.3183098861837907\n#define RECIPROCAL_PI2 0.15915494309189535\n#define EPSILON 1e-6\n#ifndef saturate\n#define saturate(a) clamp( a, 0.0, 1.0 )\n#endif\n#define whiteComplement(a) ( 1.0 - saturate( a ) )\nfloat pow2( const in float x ) { return x*x; }\nfloat pow3( const in float x ) { return x*x*x; }\nfloat pow4( const in float x ) { float x2 = x*x; return x2*x2; }\nfloat average( const in vec3 color ) { return dot( color, vec3( 0.3333 ) ); }\nhighp float rand( const in vec2 uv ) {\n\tconst highp float a = 12.9898, b = 78.233, c = 43758.5453;\n\thighp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );\n\treturn fract(sin(sn) * c);\n}\n#ifdef HIGH_PRECISION\n\tfloat precisionSafeLength( vec3 v ) { return length( v ); }\n#else\n\tfloat max3( vec3 v ) { return max( max( v.x, v.y ), v.z ); }\n\tfloat precisionSafeLength( vec3 v ) {\n\t\tfloat maxComponent = max3( abs( v ) );\n\t\treturn length( v / maxComponent ) * maxComponent;\n\t}\n#endif\nstruct IncidentLight {\n\tvec3 color;\n\tvec3 direction;\n\tbool visible;\n};\nstruct ReflectedLight {\n\tvec3 directDiffuse;\n\tvec3 directSpecular;\n\tvec3 indirectDiffuse;\n\tvec3 indirectSpecular;\n};\nstruct GeometricContext {\n\tvec3 position;\n\tvec3 normal;\n\tvec3 viewDir;\n#ifdef CLEARCOAT\n\tvec3 clearcoatNormal;\n#endif\n};\nvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n}\nvec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );\n}\nvec3 projectOnPlane(in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal ) {\n\tfloat distance = dot( planeNormal, point - pointOnPlane );\n\treturn - distance * planeNormal + point;\n}\nfloat sideOfPlane( in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal ) {\n\treturn sign( dot( point - pointOnPlane, planeNormal ) );\n}\nvec3 linePlaneIntersect( in vec3 pointOnLine, in vec3 lineDirection, in vec3 pointOnPlane, in vec3 planeNormal ) {\n\treturn lineDirection * ( dot( planeNormal, pointOnPlane - pointOnLine ) / dot( planeNormal, lineDirection ) ) + pointOnLine;\n}\nmat3 transposeMat3( const in mat3 m ) {\n\tmat3 tmp;\n\ttmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x );\n\ttmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y );\n\ttmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z );\n\treturn tmp;\n}\nfloat linearToRelativeLuminance( const in vec3 color ) {\n\tvec3 weights = vec3( 0.2126, 0.7152, 0.0722 );\n\treturn dot( weights, color.rgb );\n}\nbool isPerspectiveMatrix( mat4 m ) {\n\treturn m[ 2 ][ 3 ] == - 1.0;\n}\nvec2 equirectUv( in vec3 dir ) {\n\tfloat u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5;\n\tfloat v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\treturn vec2( u, v );\n}";

	var cube_uv_reflection_fragment = "#ifdef ENVMAP_TYPE_CUBE_UV\n\t#define cubeUV_maxMipLevel 8.0\n\t#define cubeUV_minMipLevel 4.0\n\t#define cubeUV_maxTileSize 256.0\n\t#define cubeUV_minTileSize 16.0\n\tfloat getFace( vec3 direction ) {\n\t\tvec3 absDirection = abs( direction );\n\t\tfloat face = - 1.0;\n\t\tif ( absDirection.x > absDirection.z ) {\n\t\t\tif ( absDirection.x > absDirection.y )\n\t\t\t\tface = direction.x > 0.0 ? 0.0 : 3.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t} else {\n\t\t\tif ( absDirection.z > absDirection.y )\n\t\t\t\tface = direction.z > 0.0 ? 2.0 : 5.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t}\n\t\treturn face;\n\t}\n\tvec2 getUV( vec3 direction, float face ) {\n\t\tvec2 uv;\n\t\tif ( face == 0.0 ) {\n\t\t\tuv = vec2( direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 1.0 ) {\n\t\t\tuv = vec2( - direction.x, - direction.z ) / abs( direction.y );\n\t\t} else if ( face == 2.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.y ) / abs( direction.z );\n\t\t} else if ( face == 3.0 ) {\n\t\t\tuv = vec2( - direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 4.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.z ) / abs( direction.y );\n\t\t} else {\n\t\t\tuv = vec2( direction.x, direction.y ) / abs( direction.z );\n\t\t}\n\t\treturn 0.5 * ( uv + 1.0 );\n\t}\n\tvec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) {\n\t\tfloat face = getFace( direction );\n\t\tfloat filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 );\n\t\tmipInt = max( mipInt, cubeUV_minMipLevel );\n\t\tfloat faceSize = exp2( mipInt );\n\t\tfloat texelSize = 1.0 / ( 3.0 * cubeUV_maxTileSize );\n\t\tvec2 uv = getUV( direction, face ) * ( faceSize - 1.0 );\n\t\tvec2 f = fract( uv );\n\t\tuv += 0.5 - f;\n\t\tif ( face > 2.0 ) {\n\t\t\tuv.y += faceSize;\n\t\t\tface -= 3.0;\n\t\t}\n\t\tuv.x += face * faceSize;\n\t\tif ( mipInt < cubeUV_maxMipLevel ) {\n\t\t\tuv.y += 2.0 * cubeUV_maxTileSize;\n\t\t}\n\t\tuv.y += filterInt * 2.0 * cubeUV_minTileSize;\n\t\tuv.x += 3.0 * max( 0.0, cubeUV_maxTileSize - 2.0 * faceSize );\n\t\tuv *= texelSize;\n\t\tvec3 tl = envMapTexelToLinear( texture2D( envMap, uv ) ).rgb;\n\t\tuv.x += texelSize;\n\t\tvec3 tr = envMapTexelToLinear( texture2D( envMap, uv ) ).rgb;\n\t\tuv.y += texelSize;\n\t\tvec3 br = envMapTexelToLinear( texture2D( envMap, uv ) ).rgb;\n\t\tuv.x -= texelSize;\n\t\tvec3 bl = envMapTexelToLinear( texture2D( envMap, uv ) ).rgb;\n\t\tvec3 tm = mix( tl, tr, f.x );\n\t\tvec3 bm = mix( bl, br, f.x );\n\t\treturn mix( tm, bm, f.y );\n\t}\n\t#define r0 1.0\n\t#define v0 0.339\n\t#define m0 - 2.0\n\t#define r1 0.8\n\t#define v1 0.276\n\t#define m1 - 1.0\n\t#define r4 0.4\n\t#define v4 0.046\n\t#define m4 2.0\n\t#define r5 0.305\n\t#define v5 0.016\n\t#define m5 3.0\n\t#define r6 0.21\n\t#define v6 0.0038\n\t#define m6 4.0\n\tfloat roughnessToMip( float roughness ) {\n\t\tfloat mip = 0.0;\n\t\tif ( roughness >= r1 ) {\n\t\t\tmip = ( r0 - roughness ) * ( m1 - m0 ) / ( r0 - r1 ) + m0;\n\t\t} else if ( roughness >= r4 ) {\n\t\t\tmip = ( r1 - roughness ) * ( m4 - m1 ) / ( r1 - r4 ) + m1;\n\t\t} else if ( roughness >= r5 ) {\n\t\t\tmip = ( r4 - roughness ) * ( m5 - m4 ) / ( r4 - r5 ) + m4;\n\t\t} else if ( roughness >= r6 ) {\n\t\t\tmip = ( r5 - roughness ) * ( m6 - m5 ) / ( r5 - r6 ) + m5;\n\t\t} else {\n\t\t\tmip = - 2.0 * log2( 1.16 * roughness );\t\t}\n\t\treturn mip;\n\t}\n\tvec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) {\n\t\tfloat mip = clamp( roughnessToMip( roughness ), m0, cubeUV_maxMipLevel );\n\t\tfloat mipF = fract( mip );\n\t\tfloat mipInt = floor( mip );\n\t\tvec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt );\n\t\tif ( mipF == 0.0 ) {\n\t\t\treturn vec4( color0, 1.0 );\n\t\t} else {\n\t\t\tvec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 );\n\t\t\treturn vec4( mix( color0, color1, mipF ), 1.0 );\n\t\t}\n\t}\n#endif";

	var defaultnormal_vertex = "vec3 transformedNormal = objectNormal;\n#ifdef USE_INSTANCING\n\tmat3 m = mat3( instanceMatrix );\n\ttransformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );\n\ttransformedNormal = m * transformedNormal;\n#endif\ntransformedNormal = normalMatrix * transformedNormal;\n#ifdef FLIP_SIDED\n\ttransformedNormal = - transformedNormal;\n#endif\n#ifdef USE_TANGENT\n\tvec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;\n\t#ifdef FLIP_SIDED\n\t\ttransformedTangent = - transformedTangent;\n\t#endif\n#endif";

	var displacementmap_pars_vertex = "#ifdef USE_DISPLACEMENTMAP\n\tuniform sampler2D displacementMap;\n\tuniform float displacementScale;\n\tuniform float displacementBias;\n#endif";

	var displacementmap_vertex = "#ifdef USE_DISPLACEMENTMAP\n\ttransformed += normalize( objectNormal ) * ( texture2D( displacementMap, vUv ).x * displacementScale + displacementBias );\n#endif";

	var emissivemap_fragment = "#ifdef USE_EMISSIVEMAP\n\tvec4 emissiveColor = texture2D( emissiveMap, vUv );\n\temissiveColor.rgb = emissiveMapTexelToLinear( emissiveColor ).rgb;\n\ttotalEmissiveRadiance *= emissiveColor.rgb;\n#endif";

	var emissivemap_pars_fragment = "#ifdef USE_EMISSIVEMAP\n\tuniform sampler2D emissiveMap;\n#endif";

	var encodings_fragment = "gl_FragColor = linearToOutputTexel( gl_FragColor );";

	var encodings_pars_fragment = "\nvec4 LinearToLinear( in vec4 value ) {\n\treturn value;\n}\nvec4 GammaToLinear( in vec4 value, in float gammaFactor ) {\n\treturn vec4( pow( value.rgb, vec3( gammaFactor ) ), value.a );\n}\nvec4 LinearToGamma( in vec4 value, in float gammaFactor ) {\n\treturn vec4( pow( value.rgb, vec3( 1.0 / gammaFactor ) ), value.a );\n}\nvec4 sRGBToLinear( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );\n}\nvec4 LinearTosRGB( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );\n}\nvec4 RGBEToLinear( in vec4 value ) {\n\treturn vec4( value.rgb * exp2( value.a * 255.0 - 128.0 ), 1.0 );\n}\nvec4 LinearToRGBE( in vec4 value ) {\n\tfloat maxComponent = max( max( value.r, value.g ), value.b );\n\tfloat fExp = clamp( ceil( log2( maxComponent ) ), -128.0, 127.0 );\n\treturn vec4( value.rgb / exp2( fExp ), ( fExp + 128.0 ) / 255.0 );\n}\nvec4 RGBMToLinear( in vec4 value, in float maxRange ) {\n\treturn vec4( value.rgb * value.a * maxRange, 1.0 );\n}\nvec4 LinearToRGBM( in vec4 value, in float maxRange ) {\n\tfloat maxRGB = max( value.r, max( value.g, value.b ) );\n\tfloat M = clamp( maxRGB / maxRange, 0.0, 1.0 );\n\tM = ceil( M * 255.0 ) / 255.0;\n\treturn vec4( value.rgb / ( M * maxRange ), M );\n}\nvec4 RGBDToLinear( in vec4 value, in float maxRange ) {\n\treturn vec4( value.rgb * ( ( maxRange / 255.0 ) / value.a ), 1.0 );\n}\nvec4 LinearToRGBD( in vec4 value, in float maxRange ) {\n\tfloat maxRGB = max( value.r, max( value.g, value.b ) );\n\tfloat D = max( maxRange / maxRGB, 1.0 );\n\tD = clamp( floor( D ) / 255.0, 0.0, 1.0 );\n\treturn vec4( value.rgb * ( D * ( 255.0 / maxRange ) ), D );\n}\nconst mat3 cLogLuvM = mat3( 0.2209, 0.3390, 0.4184, 0.1138, 0.6780, 0.7319, 0.0102, 0.1130, 0.2969 );\nvec4 LinearToLogLuv( in vec4 value ) {\n\tvec3 Xp_Y_XYZp = cLogLuvM * value.rgb;\n\tXp_Y_XYZp = max( Xp_Y_XYZp, vec3( 1e-6, 1e-6, 1e-6 ) );\n\tvec4 vResult;\n\tvResult.xy = Xp_Y_XYZp.xy / Xp_Y_XYZp.z;\n\tfloat Le = 2.0 * log2(Xp_Y_XYZp.y) + 127.0;\n\tvResult.w = fract( Le );\n\tvResult.z = ( Le - ( floor( vResult.w * 255.0 ) ) / 255.0 ) / 255.0;\n\treturn vResult;\n}\nconst mat3 cLogLuvInverseM = mat3( 6.0014, -2.7008, -1.7996, -1.3320, 3.1029, -5.7721, 0.3008, -1.0882, 5.6268 );\nvec4 LogLuvToLinear( in vec4 value ) {\n\tfloat Le = value.z * 255.0 + value.w;\n\tvec3 Xp_Y_XYZp;\n\tXp_Y_XYZp.y = exp2( ( Le - 127.0 ) / 2.0 );\n\tXp_Y_XYZp.z = Xp_Y_XYZp.y / value.y;\n\tXp_Y_XYZp.x = value.x * Xp_Y_XYZp.z;\n\tvec3 vRGB = cLogLuvInverseM * Xp_Y_XYZp.rgb;\n\treturn vec4( max( vRGB, 0.0 ), 1.0 );\n}";

	var envmap_fragment = "#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvec3 cameraToFrag;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToFrag = normalize( vWorldPosition - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( cameraToFrag, worldNormal );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio );\n\t\t#endif\n\t#else\n\t\tvec3 reflectVec = vReflect;\n\t#endif\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 envColor = textureCube( envMap, vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) );\n\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\tvec4 envColor = textureCubeUV( envMap, reflectVec, 0.0 );\n\t#else\n\t\tvec4 envColor = vec4( 0.0 );\n\t#endif\n\t#ifndef ENVMAP_TYPE_CUBE_UV\n\t\tenvColor = envMapTexelToLinear( envColor );\n\t#endif\n\t#ifdef ENVMAP_BLENDING_MULTIPLY\n\t\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_MIX )\n\t\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_ADD )\n\t\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\n\t#endif\n#endif";

	var envmap_common_pars_fragment = "#ifdef USE_ENVMAP\n\tuniform float envMapIntensity;\n\tuniform float flipEnvMap;\n\tuniform int maxMipLevel;\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tuniform samplerCube envMap;\n\t#else\n\t\tuniform sampler2D envMap;\n\t#endif\n\t\n#endif";

	var envmap_pars_fragment = "#ifdef USE_ENVMAP\n\tuniform float reflectivity;\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\tvarying vec3 vWorldPosition;\n\t\tuniform float refractionRatio;\n\t#else\n\t\tvarying vec3 vReflect;\n\t#endif\n#endif";

	var envmap_pars_vertex = "#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) ||defined( PHONG )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\t\n\t\tvarying vec3 vWorldPosition;\n\t#else\n\t\tvarying vec3 vReflect;\n\t\tuniform float refractionRatio;\n\t#endif\n#endif";

	var envmap_vertex = "#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvWorldPosition = worldPosition.xyz;\n\t#else\n\t\tvec3 cameraToVertex;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToVertex = normalize( worldPosition.xyz - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvReflect = reflect( cameraToVertex, worldNormal );\n\t\t#else\n\t\t\tvReflect = refract( cameraToVertex, worldNormal, refractionRatio );\n\t\t#endif\n\t#endif\n#endif";

	var fog_vertex = "#ifdef USE_FOG\n\tfogDepth = - mvPosition.z;\n#endif";

	var fog_pars_vertex = "#ifdef USE_FOG\n\tvarying float fogDepth;\n#endif";

	var fog_fragment = "#ifdef USE_FOG\n\t#ifdef FOG_EXP2\n\t\tfloat fogFactor = 1.0 - exp( - fogDensity * fogDensity * fogDepth * fogDepth );\n\t#else\n\t\tfloat fogFactor = smoothstep( fogNear, fogFar, fogDepth );\n\t#endif\n\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n#endif";

	var fog_pars_fragment = "#ifdef USE_FOG\n\tuniform vec3 fogColor;\n\tvarying float fogDepth;\n\t#ifdef FOG_EXP2\n\t\tuniform float fogDensity;\n\t#else\n\t\tuniform float fogNear;\n\t\tuniform float fogFar;\n\t#endif\n#endif";

	var gradientmap_pars_fragment = "#ifdef USE_GRADIENTMAP\n\tuniform sampler2D gradientMap;\n#endif\nvec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) {\n\tfloat dotNL = dot( normal, lightDirection );\n\tvec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 );\n\t#ifdef USE_GRADIENTMAP\n\t\treturn texture2D( gradientMap, coord ).rgb;\n\t#else\n\t\treturn ( coord.x < 0.7 ) ? vec3( 0.7 ) : vec3( 1.0 );\n\t#endif\n}";

	var lightmap_fragment = "#ifdef USE_LIGHTMAP\n\tvec4 lightMapTexel= texture2D( lightMap, vUv2 );\n\treflectedLight.indirectDiffuse += PI * lightMapTexelToLinear( lightMapTexel ).rgb * lightMapIntensity;\n#endif";

	var lightmap_pars_fragment = "#ifdef USE_LIGHTMAP\n\tuniform sampler2D lightMap;\n\tuniform float lightMapIntensity;\n#endif";

	var lights_lambert_vertex = "vec3 diffuse = vec3( 1.0 );\nGeometricContext geometry;\ngeometry.position = mvPosition.xyz;\ngeometry.normal = normalize( transformedNormal );\ngeometry.viewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( -mvPosition.xyz );\nGeometricContext backGeometry;\nbackGeometry.position = geometry.position;\nbackGeometry.normal = -geometry.normal;\nbackGeometry.viewDir = geometry.viewDir;\nvLightFront = vec3( 0.0 );\nvIndirectFront = vec3( 0.0 );\n#ifdef DOUBLE_SIDED\n\tvLightBack = vec3( 0.0 );\n\tvIndirectBack = vec3( 0.0 );\n#endif\nIncidentLight directLight;\nfloat dotNL;\nvec3 directLightColor_Diffuse;\nvIndirectFront += getAmbientLightIrradiance( ambientLightColor );\nvIndirectFront += getLightProbeIrradiance( lightProbe, geometry );\n#ifdef DOUBLE_SIDED\n\tvIndirectBack += getAmbientLightIrradiance( ambientLightColor );\n\tvIndirectBack += getLightProbeIrradiance( lightProbe, backGeometry );\n#endif\n#if NUM_POINT_LIGHTS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tgetPointDirectLightIrradiance( pointLights[ i ], geometry, directLight );\n\t\tdotNL = dot( geometry.normal, directLight.direction );\n\t\tdirectLightColor_Diffuse = PI * directLight.color;\n\t\tvLightFront += saturate( dotNL ) * directLightColor_Diffuse;\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += saturate( -dotNL ) * directLightColor_Diffuse;\n\t\t#endif\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tgetSpotDirectLightIrradiance( spotLights[ i ], geometry, directLight );\n\t\tdotNL = dot( geometry.normal, directLight.direction );\n\t\tdirectLightColor_Diffuse = PI * directLight.color;\n\t\tvLightFront += saturate( dotNL ) * directLightColor_Diffuse;\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += saturate( -dotNL ) * directLightColor_Diffuse;\n\t\t#endif\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if NUM_DIR_LIGHTS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tgetDirectionalDirectLightIrradiance( directionalLights[ i ], geometry, directLight );\n\t\tdotNL = dot( geometry.normal, directLight.direction );\n\t\tdirectLightColor_Diffuse = PI * directLight.color;\n\t\tvLightFront += saturate( dotNL ) * directLightColor_Diffuse;\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += saturate( -dotNL ) * directLightColor_Diffuse;\n\t\t#endif\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\tvIndirectFront += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry );\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvIndirectBack += getHemisphereLightIrradiance( hemisphereLights[ i ], backGeometry );\n\t\t#endif\n\t}\n\t#pragma unroll_loop_end\n#endif";

	var lights_pars_begin = "uniform bool receiveShadow;\nuniform vec3 ambientLightColor;\nuniform vec3 lightProbe[ 9 ];\nvec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {\n\tfloat x = normal.x, y = normal.y, z = normal.z;\n\tvec3 result = shCoefficients[ 0 ] * 0.886227;\n\tresult += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;\n\tresult += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;\n\tresult += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;\n\tresult += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;\n\tresult += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;\n\tresult += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );\n\tresult += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;\n\tresult += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );\n\treturn result;\n}\nvec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in GeometricContext geometry ) {\n\tvec3 worldNormal = inverseTransformDirection( geometry.normal, viewMatrix );\n\tvec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe );\n\treturn irradiance;\n}\nvec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {\n\tvec3 irradiance = ambientLightColor;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\treturn irradiance;\n}\n#if NUM_DIR_LIGHTS > 0\n\tstruct DirectionalLight {\n\t\tvec3 direction;\n\t\tvec3 color;\n\t};\n\tuniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];\n\tvoid getDirectionalDirectLightIrradiance( const in DirectionalLight directionalLight, const in GeometricContext geometry, out IncidentLight directLight ) {\n\t\tdirectLight.color = directionalLight.color;\n\t\tdirectLight.direction = directionalLight.direction;\n\t\tdirectLight.visible = true;\n\t}\n#endif\n#if NUM_POINT_LIGHTS > 0\n\tstruct PointLight {\n\t\tvec3 position;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t};\n\tuniform PointLight pointLights[ NUM_POINT_LIGHTS ];\n\tvoid getPointDirectLightIrradiance( const in PointLight pointLight, const in GeometricContext geometry, out IncidentLight directLight ) {\n\t\tvec3 lVector = pointLight.position - geometry.position;\n\t\tdirectLight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tdirectLight.color = pointLight.color;\n\t\tdirectLight.color *= punctualLightIntensityToIrradianceFactor( lightDistance, pointLight.distance, pointLight.decay );\n\t\tdirectLight.visible = ( directLight.color != vec3( 0.0 ) );\n\t}\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\tstruct SpotLight {\n\t\tvec3 position;\n\t\tvec3 direction;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t\tfloat coneCos;\n\t\tfloat penumbraCos;\n\t};\n\tuniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];\n\tvoid getSpotDirectLightIrradiance( const in SpotLight spotLight, const in GeometricContext geometry, out IncidentLight directLight ) {\n\t\tvec3 lVector = spotLight.position - geometry.position;\n\t\tdirectLight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tfloat angleCos = dot( directLight.direction, spotLight.direction );\n\t\tif ( angleCos > spotLight.coneCos ) {\n\t\t\tfloat spotEffect = smoothstep( spotLight.coneCos, spotLight.penumbraCos, angleCos );\n\t\t\tdirectLight.color = spotLight.color;\n\t\t\tdirectLight.color *= spotEffect * punctualLightIntensityToIrradianceFactor( lightDistance, spotLight.distance, spotLight.decay );\n\t\t\tdirectLight.visible = true;\n\t\t} else {\n\t\t\tdirectLight.color = vec3( 0.0 );\n\t\t\tdirectLight.visible = false;\n\t\t}\n\t}\n#endif\n#if NUM_RECT_AREA_LIGHTS > 0\n\tstruct RectAreaLight {\n\t\tvec3 color;\n\t\tvec3 position;\n\t\tvec3 halfWidth;\n\t\tvec3 halfHeight;\n\t};\n\tuniform sampler2D ltc_1;\tuniform sampler2D ltc_2;\n\tuniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\tstruct HemisphereLight {\n\t\tvec3 direction;\n\t\tvec3 skyColor;\n\t\tvec3 groundColor;\n\t};\n\tuniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ];\n\tvec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in GeometricContext geometry ) {\n\t\tfloat dotNL = dot( geometry.normal, hemiLight.direction );\n\t\tfloat hemiDiffuseWeight = 0.5 * dotNL + 0.5;\n\t\tvec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );\n\t\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\t\tirradiance *= PI;\n\t\t#endif\n\t\treturn irradiance;\n\t}\n#endif";

	var envmap_physical_pars_fragment = "#if defined( USE_ENVMAP )\n\t#ifdef ENVMAP_MODE_REFRACTION\n\t\tuniform float refractionRatio;\n\t#endif\n\tvec3 getLightProbeIndirectIrradiance( const in GeometricContext geometry, const in int maxMIPLevel ) {\n\t\tvec3 worldNormal = inverseTransformDirection( geometry.normal, viewMatrix );\n\t\t#ifdef ENVMAP_TYPE_CUBE\n\t\t\tvec3 queryVec = vec3( flipEnvMap * worldNormal.x, worldNormal.yz );\n\t\t\t#ifdef TEXTURE_LOD_EXT\n\t\t\t\tvec4 envMapColor = textureCubeLodEXT( envMap, queryVec, float( maxMIPLevel ) );\n\t\t\t#else\n\t\t\t\tvec4 envMapColor = textureCube( envMap, queryVec, float( maxMIPLevel ) );\n\t\t\t#endif\n\t\t\tenvMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;\n\t\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, worldNormal, 1.0 );\n\t\t#else\n\t\t\tvec4 envMapColor = vec4( 0.0 );\n\t\t#endif\n\t\treturn PI * envMapColor.rgb * envMapIntensity;\n\t}\n\tfloat getSpecularMIPLevel( const in float roughness, const in int maxMIPLevel ) {\n\t\tfloat maxMIPLevelScalar = float( maxMIPLevel );\n\t\tfloat sigma = PI * roughness * roughness / ( 1.0 + roughness );\n\t\tfloat desiredMIPLevel = maxMIPLevelScalar + log2( sigma );\n\t\treturn clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );\n\t}\n\tvec3 getLightProbeIndirectRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in int maxMIPLevel ) {\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( -viewDir, normal );\n\t\t\treflectVec = normalize( mix( reflectVec, normal, roughness * roughness) );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( -viewDir, normal, refractionRatio );\n\t\t#endif\n\t\treflectVec = inverseTransformDirection( reflectVec, viewMatrix );\n\t\tfloat specularMIPLevel = getSpecularMIPLevel( roughness, maxMIPLevel );\n\t\t#ifdef ENVMAP_TYPE_CUBE\n\t\t\tvec3 queryReflectVec = vec3( flipEnvMap * reflectVec.x, reflectVec.yz );\n\t\t\t#ifdef TEXTURE_LOD_EXT\n\t\t\t\tvec4 envMapColor = textureCubeLodEXT( envMap, queryReflectVec, specularMIPLevel );\n\t\t\t#else\n\t\t\t\tvec4 envMapColor = textureCube( envMap, queryReflectVec, specularMIPLevel );\n\t\t\t#endif\n\t\t\tenvMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;\n\t\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, reflectVec, roughness );\n\t\t#endif\n\t\treturn envMapColor.rgb * envMapIntensity;\n\t}\n#endif";

	var lights_toon_fragment = "ToonMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;";

	var lights_toon_pars_fragment = "varying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\nstruct ToonMaterial {\n\tvec3 diffuseColor;\n};\nvoid RE_Direct_Toon( const in IncidentLight directLight, const in GeometricContext geometry, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\tvec3 irradiance = getGradientIrradiance( geometry.normal, directLight.direction ) * directLight.color;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\treflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in GeometricContext geometry, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Toon\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Toon\n#define Material_LightProbeLOD( material )\t(0)";

	var lights_phong_fragment = "BlinnPhongMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularColor = specular;\nmaterial.specularShininess = shininess;\nmaterial.specularStrength = specularStrength;";

	var lights_phong_pars_fragment = "varying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\nstruct BlinnPhongMaterial {\n\tvec3 diffuseColor;\n\tvec3 specularColor;\n\tfloat specularShininess;\n\tfloat specularStrength;\n};\nvoid RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometry.normal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\treflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n\treflectedLight.directSpecular += irradiance * BRDF_Specular_BlinnPhong( directLight, geometry, material.specularColor, material.specularShininess ) * material.specularStrength;\n}\nvoid RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_BlinnPhong\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_BlinnPhong\n#define Material_LightProbeLOD( material )\t(0)";

	var lights_physical_fragment = "PhysicalMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor );\nvec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );\nfloat geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );\nmaterial.specularRoughness = max( roughnessFactor, 0.0525 );material.specularRoughness += geometryRoughness;\nmaterial.specularRoughness = min( material.specularRoughness, 1.0 );\n#ifdef REFLECTIVITY\n\tmaterial.specularColor = mix( vec3( MAXIMUM_SPECULAR_COEFFICIENT * pow2( reflectivity ) ), diffuseColor.rgb, metalnessFactor );\n#else\n\tmaterial.specularColor = mix( vec3( DEFAULT_SPECULAR_COEFFICIENT ), diffuseColor.rgb, metalnessFactor );\n#endif\n#ifdef CLEARCOAT\n\tmaterial.clearcoat = clearcoat;\n\tmaterial.clearcoatRoughness = clearcoatRoughness;\n\t#ifdef USE_CLEARCOATMAP\n\t\tmaterial.clearcoat *= texture2D( clearcoatMap, vUv ).x;\n\t#endif\n\t#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\t\tmaterial.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vUv ).y;\n\t#endif\n\tmaterial.clearcoat = saturate( material.clearcoat );\tmaterial.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 );\n\tmaterial.clearcoatRoughness += geometryRoughness;\n\tmaterial.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 );\n#endif\n#ifdef USE_SHEEN\n\tmaterial.sheenColor = sheen;\n#endif";

	var lights_physical_pars_fragment = "struct PhysicalMaterial {\n\tvec3 diffuseColor;\n\tfloat specularRoughness;\n\tvec3 specularColor;\n#ifdef CLEARCOAT\n\tfloat clearcoat;\n\tfloat clearcoatRoughness;\n#endif\n#ifdef USE_SHEEN\n\tvec3 sheenColor;\n#endif\n};\n#define MAXIMUM_SPECULAR_COEFFICIENT 0.16\n#define DEFAULT_SPECULAR_COEFFICIENT 0.04\nfloat clearcoatDHRApprox( const in float roughness, const in float dotNL ) {\n\treturn DEFAULT_SPECULAR_COEFFICIENT + ( 1.0 - DEFAULT_SPECULAR_COEFFICIENT ) * ( pow( 1.0 - dotNL, 5.0 ) * pow( 1.0 - roughness, 2.0 ) );\n}\n#if NUM_RECT_AREA_LIGHTS > 0\n\tvoid RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\t\tvec3 normal = geometry.normal;\n\t\tvec3 viewDir = geometry.viewDir;\n\t\tvec3 position = geometry.position;\n\t\tvec3 lightPos = rectAreaLight.position;\n\t\tvec3 halfWidth = rectAreaLight.halfWidth;\n\t\tvec3 halfHeight = rectAreaLight.halfHeight;\n\t\tvec3 lightColor = rectAreaLight.color;\n\t\tfloat roughness = material.specularRoughness;\n\t\tvec3 rectCoords[ 4 ];\n\t\trectCoords[ 0 ] = lightPos + halfWidth - halfHeight;\t\trectCoords[ 1 ] = lightPos - halfWidth - halfHeight;\n\t\trectCoords[ 2 ] = lightPos - halfWidth + halfHeight;\n\t\trectCoords[ 3 ] = lightPos + halfWidth + halfHeight;\n\t\tvec2 uv = LTC_Uv( normal, viewDir, roughness );\n\t\tvec4 t1 = texture2D( ltc_1, uv );\n\t\tvec4 t2 = texture2D( ltc_2, uv );\n\t\tmat3 mInv = mat3(\n\t\t\tvec3( t1.x, 0, t1.y ),\n\t\t\tvec3(    0, 1,    0 ),\n\t\t\tvec3( t1.z, 0, t1.w )\n\t\t);\n\t\tvec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y );\n\t\treflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords );\n\t\treflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords );\n\t}\n#endif\nvoid RE_Direct_Physical( const in IncidentLight directLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometry.normal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\t#ifdef CLEARCOAT\n\t\tfloat ccDotNL = saturate( dot( geometry.clearcoatNormal, directLight.direction ) );\n\t\tvec3 ccIrradiance = ccDotNL * directLight.color;\n\t\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\t\tccIrradiance *= PI;\n\t\t#endif\n\t\tfloat clearcoatDHR = material.clearcoat * clearcoatDHRApprox( material.clearcoatRoughness, ccDotNL );\n\t\treflectedLight.directSpecular += ccIrradiance * material.clearcoat * BRDF_Specular_GGX( directLight, geometry.viewDir, geometry.clearcoatNormal, vec3( DEFAULT_SPECULAR_COEFFICIENT ), material.clearcoatRoughness );\n\t#else\n\t\tfloat clearcoatDHR = 0.0;\n\t#endif\n\t#ifdef USE_SHEEN\n\t\treflectedLight.directSpecular += ( 1.0 - clearcoatDHR ) * irradiance * BRDF_Specular_Sheen(\n\t\t\tmaterial.specularRoughness,\n\t\t\tdirectLight.direction,\n\t\t\tgeometry,\n\t\t\tmaterial.sheenColor\n\t\t);\n\t#else\n\t\treflectedLight.directSpecular += ( 1.0 - clearcoatDHR ) * irradiance * BRDF_Specular_GGX( directLight, geometry.viewDir, geometry.normal, material.specularColor, material.specularRoughness);\n\t#endif\n\treflectedLight.directDiffuse += ( 1.0 - clearcoatDHR ) * irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {\n\t#ifdef CLEARCOAT\n\t\tfloat ccDotNV = saturate( dot( geometry.clearcoatNormal, geometry.viewDir ) );\n\t\treflectedLight.indirectSpecular += clearcoatRadiance * material.clearcoat * BRDF_Specular_GGX_Environment( geometry.viewDir, geometry.clearcoatNormal, vec3( DEFAULT_SPECULAR_COEFFICIENT ), material.clearcoatRoughness );\n\t\tfloat ccDotNL = ccDotNV;\n\t\tfloat clearcoatDHR = material.clearcoat * clearcoatDHRApprox( material.clearcoatRoughness, ccDotNL );\n\t#else\n\t\tfloat clearcoatDHR = 0.0;\n\t#endif\n\tfloat clearcoatInv = 1.0 - clearcoatDHR;\n\tvec3 singleScattering = vec3( 0.0 );\n\tvec3 multiScattering = vec3( 0.0 );\n\tvec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;\n\tBRDF_Specular_Multiscattering_Environment( geometry, material.specularColor, material.specularRoughness, singleScattering, multiScattering );\n\tvec3 diffuse = material.diffuseColor * ( 1.0 - ( singleScattering + multiScattering ) );\n\treflectedLight.indirectSpecular += clearcoatInv * radiance * singleScattering;\n\treflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance;\n\treflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance;\n}\n#define RE_Direct\t\t\t\tRE_Direct_Physical\n#define RE_Direct_RectArea\t\tRE_Direct_RectArea_Physical\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Physical\n#define RE_IndirectSpecular\t\tRE_IndirectSpecular_Physical\nfloat computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) {\n\treturn saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion );\n}";

	var lights_fragment_begin = "\nGeometricContext geometry;\ngeometry.position = - vViewPosition;\ngeometry.normal = normal;\ngeometry.viewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );\n#ifdef CLEARCOAT\n\tgeometry.clearcoatNormal = clearcoatNormal;\n#endif\nIncidentLight directLight;\n#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )\n\tPointLight pointLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tpointLight = pointLights[ i ];\n\t\tgetPointDirectLightIrradiance( pointLight, geometry, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS )\n\t\tpointLightShadow = pointLightShadows[ i ];\n\t\tdirectLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )\n\tSpotLight spotLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tspotLight = spotLights[ i ];\n\t\tgetSpotDirectLightIrradiance( spotLight, geometry, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\tspotLightShadow = spotLightShadows[ i ];\n\t\tdirectLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )\n\tDirectionalLight directionalLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tdirectionalLight = directionalLights[ i ];\n\t\tgetDirectionalDirectLightIrradiance( directionalLight, geometry, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )\n\t\tdirectionalLightShadow = directionalLightShadows[ i ];\n\t\tdirectLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )\n\tRectAreaLight rectAreaLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {\n\t\trectAreaLight = rectAreaLights[ i ];\n\t\tRE_Direct_RectArea( rectAreaLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if defined( RE_IndirectDiffuse )\n\tvec3 iblIrradiance = vec3( 0.0 );\n\tvec3 irradiance = getAmbientLightIrradiance( ambientLightColor );\n\tirradiance += getLightProbeIrradiance( lightProbe, geometry );\n\t#if ( NUM_HEMI_LIGHTS > 0 )\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\t\tirradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry );\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n#endif\n#if defined( RE_IndirectSpecular )\n\tvec3 radiance = vec3( 0.0 );\n\tvec3 clearcoatRadiance = vec3( 0.0 );\n#endif";

	var lights_fragment_maps = "#if defined( RE_IndirectDiffuse )\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel= texture2D( lightMap, vUv2 );\n\t\tvec3 lightMapIrradiance = lightMapTexelToLinear( lightMapTexel ).rgb * lightMapIntensity;\n\t\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\t\tlightMapIrradiance *= PI;\n\t\t#endif\n\t\tirradiance += lightMapIrradiance;\n\t#endif\n\t#if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV )\n\t\tiblIrradiance += getLightProbeIndirectIrradiance( geometry, maxMipLevel );\n\t#endif\n#endif\n#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular )\n\tradiance += getLightProbeIndirectRadiance( geometry.viewDir, geometry.normal, material.specularRoughness, maxMipLevel );\n\t#ifdef CLEARCOAT\n\t\tclearcoatRadiance += getLightProbeIndirectRadiance( geometry.viewDir, geometry.clearcoatNormal, material.clearcoatRoughness, maxMipLevel );\n\t#endif\n#endif";

	var lights_fragment_end = "#if defined( RE_IndirectDiffuse )\n\tRE_IndirectDiffuse( irradiance, geometry, material, reflectedLight );\n#endif\n#if defined( RE_IndirectSpecular )\n\tRE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometry, material, reflectedLight );\n#endif";

	var logdepthbuf_fragment = "#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT )\n\tgl_FragDepthEXT = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;\n#endif";

	var logdepthbuf_pars_fragment = "#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT )\n\tuniform float logDepthBufFC;\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif";

	var logdepthbuf_pars_vertex = "#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvarying float vFragDepth;\n\t\tvarying float vIsPerspective;\n\t#else\n\t\tuniform float logDepthBufFC;\n\t#endif\n#endif";

	var logdepthbuf_vertex = "#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvFragDepth = 1.0 + gl_Position.w;\n\t\tvIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) );\n\t#else\n\t\tif ( isPerspectiveMatrix( projectionMatrix ) ) {\n\t\t\tgl_Position.z = log2( max( EPSILON, gl_Position.w + 1.0 ) ) * logDepthBufFC - 1.0;\n\t\t\tgl_Position.z *= gl_Position.w;\n\t\t}\n\t#endif\n#endif";

	var map_fragment = "#ifdef USE_MAP\n\tvec4 texelColor = texture2D( map, vUv );\n\ttexelColor = mapTexelToLinear( texelColor );\n\tdiffuseColor *= texelColor;\n#endif";

	var map_pars_fragment = "#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif";

	var map_particle_fragment = "#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\tvec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy;\n#endif\n#ifdef USE_MAP\n\tvec4 mapTexel = texture2D( map, uv );\n\tdiffuseColor *= mapTexelToLinear( mapTexel );\n#endif\n#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, uv ).g;\n#endif";

	var map_particle_pars_fragment = "#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\tuniform mat3 uvTransform;\n#endif\n#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif\n#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif";

	var metalnessmap_fragment = "float metalnessFactor = metalness;\n#ifdef USE_METALNESSMAP\n\tvec4 texelMetalness = texture2D( metalnessMap, vUv );\n\tmetalnessFactor *= texelMetalness.b;\n#endif";

	var metalnessmap_pars_fragment = "#ifdef USE_METALNESSMAP\n\tuniform sampler2D metalnessMap;\n#endif";

	var morphnormal_vertex = "#ifdef USE_MORPHNORMALS\n\tobjectNormal *= morphTargetBaseInfluence;\n\tobjectNormal += morphNormal0 * morphTargetInfluences[ 0 ];\n\tobjectNormal += morphNormal1 * morphTargetInfluences[ 1 ];\n\tobjectNormal += morphNormal2 * morphTargetInfluences[ 2 ];\n\tobjectNormal += morphNormal3 * morphTargetInfluences[ 3 ];\n#endif";

	var morphtarget_pars_vertex = "#ifdef USE_MORPHTARGETS\n\tuniform float morphTargetBaseInfluence;\n\t#ifndef USE_MORPHNORMALS\n\t\tuniform float morphTargetInfluences[ 8 ];\n\t#else\n\t\tuniform float morphTargetInfluences[ 4 ];\n\t#endif\n#endif";

	var morphtarget_vertex = "#ifdef USE_MORPHTARGETS\n\ttransformed *= morphTargetBaseInfluence;\n\ttransformed += morphTarget0 * morphTargetInfluences[ 0 ];\n\ttransformed += morphTarget1 * morphTargetInfluences[ 1 ];\n\ttransformed += morphTarget2 * morphTargetInfluences[ 2 ];\n\ttransformed += morphTarget3 * morphTargetInfluences[ 3 ];\n\t#ifndef USE_MORPHNORMALS\n\t\ttransformed += morphTarget4 * morphTargetInfluences[ 4 ];\n\t\ttransformed += morphTarget5 * morphTargetInfluences[ 5 ];\n\t\ttransformed += morphTarget6 * morphTargetInfluences[ 6 ];\n\t\ttransformed += morphTarget7 * morphTargetInfluences[ 7 ];\n\t#endif\n#endif";

	var normal_fragment_begin = "#ifdef FLAT_SHADED\n\tvec3 fdx = vec3( dFdx( vViewPosition.x ), dFdx( vViewPosition.y ), dFdx( vViewPosition.z ) );\n\tvec3 fdy = vec3( dFdy( vViewPosition.x ), dFdy( vViewPosition.y ), dFdy( vViewPosition.z ) );\n\tvec3 normal = normalize( cross( fdx, fdy ) );\n#else\n\tvec3 normal = normalize( vNormal );\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * ( float( gl_FrontFacing ) * 2.0 - 1.0 );\n\t#endif\n\t#ifdef USE_TANGENT\n\t\tvec3 tangent = normalize( vTangent );\n\t\tvec3 bitangent = normalize( vBitangent );\n\t\t#ifdef DOUBLE_SIDED\n\t\t\ttangent = tangent * ( float( gl_FrontFacing ) * 2.0 - 1.0 );\n\t\t\tbitangent = bitangent * ( float( gl_FrontFacing ) * 2.0 - 1.0 );\n\t\t#endif\n\t\t#if defined( TANGENTSPACE_NORMALMAP ) || defined( USE_CLEARCOAT_NORMALMAP )\n\t\t\tmat3 vTBN = mat3( tangent, bitangent, normal );\n\t\t#endif\n\t#endif\n#endif\nvec3 geometryNormal = normal;";

	var normal_fragment_maps = "#ifdef OBJECTSPACE_NORMALMAP\n\tnormal = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;\n\t#ifdef FLIP_SIDED\n\t\tnormal = - normal;\n\t#endif\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * ( float( gl_FrontFacing ) * 2.0 - 1.0 );\n\t#endif\n\tnormal = normalize( normalMatrix * normal );\n#elif defined( TANGENTSPACE_NORMALMAP )\n\tvec3 mapN = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;\n\tmapN.xy *= normalScale;\n\t#ifdef USE_TANGENT\n\t\tnormal = normalize( vTBN * mapN );\n\t#else\n\t\tnormal = perturbNormal2Arb( -vViewPosition, normal, mapN );\n\t#endif\n#elif defined( USE_BUMPMAP )\n\tnormal = perturbNormalArb( -vViewPosition, normal, dHdxy_fwd() );\n#endif";

	var normalmap_pars_fragment = "#ifdef USE_NORMALMAP\n\tuniform sampler2D normalMap;\n\tuniform vec2 normalScale;\n#endif\n#ifdef OBJECTSPACE_NORMALMAP\n\tuniform mat3 normalMatrix;\n#endif\n#if ! defined ( USE_TANGENT ) && ( defined ( TANGENTSPACE_NORMALMAP ) || defined ( USE_CLEARCOAT_NORMALMAP ) )\n\tvec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec3 mapN ) {\n\t\tvec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );\n\t\tvec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );\n\t\tvec2 st0 = dFdx( vUv.st );\n\t\tvec2 st1 = dFdy( vUv.st );\n\t\tfloat scale = sign( st1.t * st0.s - st0.t * st1.s );\n\t\tvec3 S = normalize( ( q0 * st1.t - q1 * st0.t ) * scale );\n\t\tvec3 T = normalize( ( - q0 * st1.s + q1 * st0.s ) * scale );\n\t\tvec3 N = normalize( surf_norm );\n\t\tmat3 tsn = mat3( S, T, N );\n\t\tmapN.xy *= ( float( gl_FrontFacing ) * 2.0 - 1.0 );\n\t\treturn normalize( tsn * mapN );\n\t}\n#endif";

	var clearcoat_normal_fragment_begin = "#ifdef CLEARCOAT\n\tvec3 clearcoatNormal = geometryNormal;\n#endif";

	var clearcoat_normal_fragment_maps = "#ifdef USE_CLEARCOAT_NORMALMAP\n\tvec3 clearcoatMapN = texture2D( clearcoatNormalMap, vUv ).xyz * 2.0 - 1.0;\n\tclearcoatMapN.xy *= clearcoatNormalScale;\n\t#ifdef USE_TANGENT\n\t\tclearcoatNormal = normalize( vTBN * clearcoatMapN );\n\t#else\n\t\tclearcoatNormal = perturbNormal2Arb( - vViewPosition, clearcoatNormal, clearcoatMapN );\n\t#endif\n#endif";

	var clearcoat_pars_fragment = "#ifdef USE_CLEARCOATMAP\n\tuniform sampler2D clearcoatMap;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tuniform sampler2D clearcoatRoughnessMap;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tuniform sampler2D clearcoatNormalMap;\n\tuniform vec2 clearcoatNormalScale;\n#endif";

	var packing = "vec3 packNormalToRGB( const in vec3 normal ) {\n\treturn normalize( normal ) * 0.5 + 0.5;\n}\nvec3 unpackRGBToNormal( const in vec3 rgb ) {\n\treturn 2.0 * rgb.xyz - 1.0;\n}\nconst float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;\nconst vec3 PackFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );\nconst vec4 UnpackFactors = UnpackDownscale / vec4( PackFactors, 1. );\nconst float ShiftRight8 = 1. / 256.;\nvec4 packDepthToRGBA( const in float v ) {\n\tvec4 r = vec4( fract( v * PackFactors ), v );\n\tr.yzw -= r.xyz * ShiftRight8;\treturn r * PackUpscale;\n}\nfloat unpackRGBAToDepth( const in vec4 v ) {\n\treturn dot( v, UnpackFactors );\n}\nvec4 pack2HalfToRGBA( vec2 v ) {\n\tvec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ));\n\treturn vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w);\n}\nvec2 unpackRGBATo2Half( vec4 v ) {\n\treturn vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) );\n}\nfloat viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( viewZ + near ) / ( near - far );\n}\nfloat orthographicDepthToViewZ( const in float linearClipZ, const in float near, const in float far ) {\n\treturn linearClipZ * ( near - far ) - near;\n}\nfloat viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn (( near + viewZ ) * far ) / (( far - near ) * viewZ );\n}\nfloat perspectiveDepthToViewZ( const in float invClipZ, const in float near, const in float far ) {\n\treturn ( near * far ) / ( ( far - near ) * invClipZ - far );\n}";

	var premultiplied_alpha_fragment = "#ifdef PREMULTIPLIED_ALPHA\n\tgl_FragColor.rgb *= gl_FragColor.a;\n#endif";

	var project_vertex = "vec4 mvPosition = vec4( transformed, 1.0 );\n#ifdef USE_INSTANCING\n\tmvPosition = instanceMatrix * mvPosition;\n#endif\nmvPosition = modelViewMatrix * mvPosition;\ngl_Position = projectionMatrix * mvPosition;";

	var dithering_fragment = "#ifdef DITHERING\n\tgl_FragColor.rgb = dithering( gl_FragColor.rgb );\n#endif";

	var dithering_pars_fragment = "#ifdef DITHERING\n\tvec3 dithering( vec3 color ) {\n\t\tfloat grid_position = rand( gl_FragCoord.xy );\n\t\tvec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 );\n\t\tdither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position );\n\t\treturn color + dither_shift_RGB;\n\t}\n#endif";

	var roughnessmap_fragment = "float roughnessFactor = roughness;\n#ifdef USE_ROUGHNESSMAP\n\tvec4 texelRoughness = texture2D( roughnessMap, vUv );\n\troughnessFactor *= texelRoughness.g;\n#endif";

	var roughnessmap_pars_fragment = "#ifdef USE_ROUGHNESSMAP\n\tuniform sampler2D roughnessMap;\n#endif";

	var shadowmap_pars_fragment = "#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n\tfloat texture2DCompare( sampler2D depths, vec2 uv, float compare ) {\n\t\treturn step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) );\n\t}\n\tvec2 texture2DDistribution( sampler2D shadow, vec2 uv ) {\n\t\treturn unpackRGBATo2Half( texture2D( shadow, uv ) );\n\t}\n\tfloat VSMShadow (sampler2D shadow, vec2 uv, float compare ){\n\t\tfloat occlusion = 1.0;\n\t\tvec2 distribution = texture2DDistribution( shadow, uv );\n\t\tfloat hard_shadow = step( compare , distribution.x );\n\t\tif (hard_shadow != 1.0 ) {\n\t\t\tfloat distance = compare - distribution.x ;\n\t\t\tfloat variance = max( 0.00000, distribution.y * distribution.y );\n\t\t\tfloat softness_probability = variance / (variance + distance * distance );\t\t\tsoftness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 );\t\t\tocclusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 );\n\t\t}\n\t\treturn occlusion;\n\t}\n\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\tfloat shadow = 1.0;\n\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\tshadowCoord.z += shadowBias;\n\t\tbvec4 inFrustumVec = bvec4 ( shadowCoord.x >= 0.0, shadowCoord.x <= 1.0, shadowCoord.y >= 0.0, shadowCoord.y <= 1.0 );\n\t\tbool inFrustum = all( inFrustumVec );\n\t\tbvec2 frustumTestVec = bvec2( inFrustum, shadowCoord.z <= 1.0 );\n\t\tbool frustumTest = all( frustumTestVec );\n\t\tif ( frustumTest ) {\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx0 = - texelSize.x * shadowRadius;\n\t\t\tfloat dy0 = - texelSize.y * shadowRadius;\n\t\t\tfloat dx1 = + texelSize.x * shadowRadius;\n\t\t\tfloat dy1 = + texelSize.y * shadowRadius;\n\t\t\tfloat dx2 = dx0 / 2.0;\n\t\t\tfloat dy2 = dy0 / 2.0;\n\t\t\tfloat dx3 = dx1 / 2.0;\n\t\t\tfloat dy3 = dy1 / 2.0;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z )\n\t\t\t) * ( 1.0 / 17.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_PCF_SOFT )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx = texelSize.x;\n\t\t\tfloat dy = texelSize.y;\n\t\t\tvec2 uv = shadowCoord.xy;\n\t\t\tvec2 f = fract( uv * shadowMapSize + 0.5 );\n\t\t\tuv -= f * texelSize;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, uv, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ), \n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ), \n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ), \n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ), \n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ), \n\t\t\t\t\t\t  texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t  f.x ),\n\t\t\t\t\t mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ), \n\t\t\t\t\t\t  texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t  f.x ),\n\t\t\t\t\t f.y )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_VSM )\n\t\t\tshadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#else\n\t\t\tshadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#endif\n\t\t}\n\t\treturn shadow;\n\t}\n\tvec2 cubeToUV( vec3 v, float texelSizeY ) {\n\t\tvec3 absV = abs( v );\n\t\tfloat scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) );\n\t\tabsV *= scaleToCube;\n\t\tv *= scaleToCube * ( 1.0 - 2.0 * texelSizeY );\n\t\tvec2 planar = v.xy;\n\t\tfloat almostATexel = 1.5 * texelSizeY;\n\t\tfloat almostOne = 1.0 - almostATexel;\n\t\tif ( absV.z >= almostOne ) {\n\t\t\tif ( v.z > 0.0 )\n\t\t\t\tplanar.x = 4.0 - v.x;\n\t\t} else if ( absV.x >= almostOne ) {\n\t\t\tfloat signX = sign( v.x );\n\t\t\tplanar.x = v.z * signX + 2.0 * signX;\n\t\t} else if ( absV.y >= almostOne ) {\n\t\t\tfloat signY = sign( v.y );\n\t\t\tplanar.x = v.x + 2.0 * signY + 2.0;\n\t\t\tplanar.y = v.z * signY - 2.0;\n\t\t}\n\t\treturn vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 );\n\t}\n\tfloat getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\n\t\tvec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) );\n\t\tvec3 lightToPosition = shadowCoord.xyz;\n\t\tfloat dp = ( length( lightToPosition ) - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear );\t\tdp += shadowBias;\n\t\tvec3 bd3D = normalize( lightToPosition );\n\t\t#if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM )\n\t\t\tvec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y;\n\t\t\treturn (\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#else\n\t\t\treturn texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp );\n\t\t#endif\n\t}\n#endif";

	var shadowmap_pars_vertex = "#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tuniform mat4 spotShadowMatrix[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n#endif";

	var shadowmap_vertex = "#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0 || NUM_SPOT_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0\n\t\tvec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\t\tvec4 shadowWorldPosition;\n\t#endif\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );\n\t\tvDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias, 0 );\n\t\tvSpotShadowCoord[ i ] = spotShadowMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 );\n\t\tvPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n#endif";

	var shadowmask_pars_fragment = "float getShadowMask() {\n\tfloat shadow = 1.0;\n\t#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\tdirectionalLight = directionalLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) {\n\t\tspotLight = spotLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowBias, spotLight.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\tpointLight = pointLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#endif\n\treturn shadow;\n}";

	var skinbase_vertex = "#ifdef USE_SKINNING\n\tmat4 boneMatX = getBoneMatrix( skinIndex.x );\n\tmat4 boneMatY = getBoneMatrix( skinIndex.y );\n\tmat4 boneMatZ = getBoneMatrix( skinIndex.z );\n\tmat4 boneMatW = getBoneMatrix( skinIndex.w );\n#endif";

	var skinning_pars_vertex = "#ifdef USE_SKINNING\n\tuniform mat4 bindMatrix;\n\tuniform mat4 bindMatrixInverse;\n\t#ifdef BONE_TEXTURE\n\t\tuniform highp sampler2D boneTexture;\n\t\tuniform int boneTextureSize;\n\t\tmat4 getBoneMatrix( const in float i ) {\n\t\t\tfloat j = i * 4.0;\n\t\t\tfloat x = mod( j, float( boneTextureSize ) );\n\t\t\tfloat y = floor( j / float( boneTextureSize ) );\n\t\t\tfloat dx = 1.0 / float( boneTextureSize );\n\t\t\tfloat dy = 1.0 / float( boneTextureSize );\n\t\t\ty = dy * ( y + 0.5 );\n\t\t\tvec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) );\n\t\t\tvec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) );\n\t\t\tvec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) );\n\t\t\tvec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) );\n\t\t\tmat4 bone = mat4( v1, v2, v3, v4 );\n\t\t\treturn bone;\n\t\t}\n\t#else\n\t\tuniform mat4 boneMatrices[ MAX_BONES ];\n\t\tmat4 getBoneMatrix( const in float i ) {\n\t\t\tmat4 bone = boneMatrices[ int(i) ];\n\t\t\treturn bone;\n\t\t}\n\t#endif\n#endif";

	var skinning_vertex = "#ifdef USE_SKINNING\n\tvec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );\n\tvec4 skinned = vec4( 0.0 );\n\tskinned += boneMatX * skinVertex * skinWeight.x;\n\tskinned += boneMatY * skinVertex * skinWeight.y;\n\tskinned += boneMatZ * skinVertex * skinWeight.z;\n\tskinned += boneMatW * skinVertex * skinWeight.w;\n\ttransformed = ( bindMatrixInverse * skinned ).xyz;\n#endif";

	var skinnormal_vertex = "#ifdef USE_SKINNING\n\tmat4 skinMatrix = mat4( 0.0 );\n\tskinMatrix += skinWeight.x * boneMatX;\n\tskinMatrix += skinWeight.y * boneMatY;\n\tskinMatrix += skinWeight.z * boneMatZ;\n\tskinMatrix += skinWeight.w * boneMatW;\n\tskinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;\n\tobjectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz;\n\t#ifdef USE_TANGENT\n\t\tobjectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz;\n\t#endif\n#endif";

	var specularmap_fragment = "float specularStrength;\n#ifdef USE_SPECULARMAP\n\tvec4 texelSpecular = texture2D( specularMap, vUv );\n\tspecularStrength = texelSpecular.r;\n#else\n\tspecularStrength = 1.0;\n#endif";

	var specularmap_pars_fragment = "#ifdef USE_SPECULARMAP\n\tuniform sampler2D specularMap;\n#endif";

	var tonemapping_fragment = "#if defined( TONE_MAPPING )\n\tgl_FragColor.rgb = toneMapping( gl_FragColor.rgb );\n#endif";

	var tonemapping_pars_fragment = "#ifndef saturate\n#define saturate(a) clamp( a, 0.0, 1.0 )\n#endif\nuniform float toneMappingExposure;\nvec3 LinearToneMapping( vec3 color ) {\n\treturn toneMappingExposure * color;\n}\nvec3 ReinhardToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\treturn saturate( color / ( vec3( 1.0 ) + color ) );\n}\nvec3 OptimizedCineonToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\tcolor = max( vec3( 0.0 ), color - 0.004 );\n\treturn pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );\n}\nvec3 RRTAndODTFit( vec3 v ) {\n\tvec3 a = v * ( v + 0.0245786 ) - 0.000090537;\n\tvec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;\n\treturn a / b;\n}\nvec3 ACESFilmicToneMapping( vec3 color ) {\n\tconst mat3 ACESInputMat = mat3(\n\t\tvec3( 0.59719, 0.07600, 0.02840 ),\t\tvec3( 0.35458, 0.90834, 0.13383 ),\n\t\tvec3( 0.04823, 0.01566, 0.83777 )\n\t);\n\tconst mat3 ACESOutputMat = mat3(\n\t\tvec3(  1.60475, -0.10208, -0.00327 ),\t\tvec3( -0.53108,  1.10813, -0.07276 ),\n\t\tvec3( -0.07367, -0.00605,  1.07602 )\n\t);\n\tcolor *= toneMappingExposure / 0.6;\n\tcolor = ACESInputMat * color;\n\tcolor = RRTAndODTFit( color );\n\tcolor = ACESOutputMat * color;\n\treturn saturate( color );\n}\nvec3 CustomToneMapping( vec3 color ) { return color; }";

	var transmissionmap_fragment = "#ifdef USE_TRANSMISSIONMAP\n\ttotalTransmission *= texture2D( transmissionMap, vUv ).r;\n#endif";

	var transmissionmap_pars_fragment = "#ifdef USE_TRANSMISSIONMAP\n\tuniform sampler2D transmissionMap;\n#endif";

	var uv_pars_fragment = "#if ( defined( USE_UV ) && ! defined( UVS_VERTEX_ONLY ) )\n\tvarying vec2 vUv;\n#endif";

	var uv_pars_vertex = "#ifdef USE_UV\n\t#ifdef UVS_VERTEX_ONLY\n\t\tvec2 vUv;\n\t#else\n\t\tvarying vec2 vUv;\n\t#endif\n\tuniform mat3 uvTransform;\n#endif";

	var uv_vertex = "#ifdef USE_UV\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n#endif";

	var uv2_pars_fragment = "#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tvarying vec2 vUv2;\n#endif";

	var uv2_pars_vertex = "#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tattribute vec2 uv2;\n\tvarying vec2 vUv2;\n\tuniform mat3 uv2Transform;\n#endif";

	var uv2_vertex = "#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tvUv2 = ( uv2Transform * vec3( uv2, 1 ) ).xy;\n#endif";

	var worldpos_vertex = "#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP )\n\tvec4 worldPosition = vec4( transformed, 1.0 );\n\t#ifdef USE_INSTANCING\n\t\tworldPosition = instanceMatrix * worldPosition;\n\t#endif\n\tworldPosition = modelMatrix * worldPosition;\n#endif";

	var background_frag = "uniform sampler2D t2D;\nvarying vec2 vUv;\nvoid main() {\n\tvec4 texColor = texture2D( t2D, vUv );\n\tgl_FragColor = mapTexelToLinear( texColor );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n}";

	var background_vert = "varying vec2 vUv;\nuniform mat3 uvTransform;\nvoid main() {\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n\tgl_Position = vec4( position.xy, 1.0, 1.0 );\n}";

	var cube_frag = "#include <envmap_common_pars_fragment>\nuniform float opacity;\nvarying vec3 vWorldDirection;\n#include <cube_uv_reflection_fragment>\nvoid main() {\n\tvec3 vReflect = vWorldDirection;\n\t#include <envmap_fragment>\n\tgl_FragColor = envColor;\n\tgl_FragColor.a *= opacity;\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n}";

	var cube_vert = "varying vec3 vWorldDirection;\n#include <common>\nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include <begin_vertex>\n\t#include <project_vertex>\n\tgl_Position.z = gl_Position.w;\n}";

	var depth_frag = "#if DEPTH_PACKING == 3200\n\tuniform float opacity;\n#endif\n#include <common>\n#include <packing>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#if DEPTH_PACKING == 3200\n\t\tdiffuseColor.a = opacity;\n\t#endif\n\t#include <map_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <logdepthbuf_fragment>\n\tfloat fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;\n\t#if DEPTH_PACKING == 3200\n\t\tgl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );\n\t#elif DEPTH_PACKING == 3201\n\t\tgl_FragColor = packDepthToRGBA( fragCoordZ );\n\t#endif\n}";

	var depth_vert = "#include <common>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include <uv_vertex>\n\t#include <skinbase_vertex>\n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include <beginnormal_vertex>\n\t\t#include <morphnormal_vertex>\n\t\t#include <skinnormal_vertex>\n\t#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\tvHighPrecisionZW = gl_Position.zw;\n}";

	var distanceRGBA_frag = "#define DISTANCE\nuniform vec3 referencePosition;\nuniform float nearDistance;\nuniform float farDistance;\nvarying vec3 vWorldPosition;\n#include <common>\n#include <packing>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main () {\n\t#include <clipping_planes_fragment>\n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include <map_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\tfloat dist = length( vWorldPosition - referencePosition );\n\tdist = ( dist - nearDistance ) / ( farDistance - nearDistance );\n\tdist = saturate( dist );\n\tgl_FragColor = packDepthToRGBA( dist );\n}";

	var distanceRGBA_vert = "#define DISTANCE\nvarying vec3 vWorldPosition;\n#include <common>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <skinbase_vertex>\n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include <beginnormal_vertex>\n\t\t#include <morphnormal_vertex>\n\t\t#include <skinnormal_vertex>\n\t#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <worldpos_vertex>\n\t#include <clipping_planes_vertex>\n\tvWorldPosition = worldPosition.xyz;\n}";

	var equirect_frag = "uniform sampler2D tEquirect;\nvarying vec3 vWorldDirection;\n#include <common>\nvoid main() {\n\tvec3 direction = normalize( vWorldDirection );\n\tvec2 sampleUV = equirectUv( direction );\n\tvec4 texColor = texture2D( tEquirect, sampleUV );\n\tgl_FragColor = mapTexelToLinear( texColor );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n}";

	var equirect_vert = "varying vec3 vWorldDirection;\n#include <common>\nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include <begin_vertex>\n\t#include <project_vertex>\n}";

	var linedashed_frag = "uniform vec3 diffuse;\nuniform float opacity;\nuniform float dashSize;\nuniform float totalSize;\nvarying float vLineDistance;\n#include <common>\n#include <color_pars_fragment>\n#include <fog_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tif ( mod( vLineDistance, totalSize ) > dashSize ) {\n\t\tdiscard;\n\t}\n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <logdepthbuf_fragment>\n\t#include <color_fragment>\n\toutgoingLight = diffuseColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n}";

	var linedashed_vert = "uniform float scale;\nattribute float lineDistance;\nvarying float vLineDistance;\n#include <common>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\tvLineDistance = scale * lineDistance;\n\t#include <color_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <fog_vertex>\n}";

	var meshbasic_frag = "uniform vec3 diffuse;\nuniform float opacity;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include <common>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <uv2_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <envmap_common_pars_fragment>\n#include <envmap_pars_fragment>\n#include <cube_uv_reflection_fragment>\n#include <fog_pars_fragment>\n#include <specularmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <specularmap_fragment>\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\t#ifdef USE_LIGHTMAP\n\t\n\t\tvec4 lightMapTexel= texture2D( lightMap, vUv2 );\n\t\treflectedLight.indirectDiffuse += lightMapTexelToLinear( lightMapTexel ).rgb * lightMapIntensity;\n\t#else\n\t\treflectedLight.indirectDiffuse += vec3( 1.0 );\n\t#endif\n\t#include <aomap_fragment>\n\treflectedLight.indirectDiffuse *= diffuseColor.rgb;\n\tvec3 outgoingLight = reflectedLight.indirectDiffuse;\n\t#include <envmap_fragment>\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

	var meshbasic_vert = "#include <common>\n#include <uv_pars_vertex>\n#include <uv2_pars_vertex>\n#include <envmap_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <uv2_vertex>\n\t#include <color_vertex>\n\t#include <skinbase_vertex>\n\t#ifdef USE_ENVMAP\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <worldpos_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <envmap_vertex>\n\t#include <fog_vertex>\n}";

	var meshlambert_frag = "uniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\nvarying vec3 vLightFront;\nvarying vec3 vIndirectFront;\n#ifdef DOUBLE_SIDED\n\tvarying vec3 vLightBack;\n\tvarying vec3 vIndirectBack;\n#endif\n#include <common>\n#include <packing>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <uv2_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <emissivemap_pars_fragment>\n#include <envmap_common_pars_fragment>\n#include <envmap_pars_fragment>\n#include <cube_uv_reflection_fragment>\n#include <bsdfs>\n#include <lights_pars_begin>\n#include <fog_pars_fragment>\n#include <shadowmap_pars_fragment>\n#include <shadowmask_pars_fragment>\n#include <specularmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <specularmap_fragment>\n\t#include <emissivemap_fragment>\n\t#ifdef DOUBLE_SIDED\n\t\treflectedLight.indirectDiffuse += ( gl_FrontFacing ) ? vIndirectFront : vIndirectBack;\n\t#else\n\t\treflectedLight.indirectDiffuse += vIndirectFront;\n\t#endif\n\t#include <lightmap_fragment>\n\treflectedLight.indirectDiffuse *= BRDF_Diffuse_Lambert( diffuseColor.rgb );\n\t#ifdef DOUBLE_SIDED\n\t\treflectedLight.directDiffuse = ( gl_FrontFacing ) ? vLightFront : vLightBack;\n\t#else\n\t\treflectedLight.directDiffuse = vLightFront;\n\t#endif\n\treflectedLight.directDiffuse *= BRDF_Diffuse_Lambert( diffuseColor.rgb ) * getShadowMask();\n\t#include <aomap_fragment>\n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include <envmap_fragment>\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

	var meshlambert_vert = "#define LAMBERT\nvarying vec3 vLightFront;\nvarying vec3 vIndirectFront;\n#ifdef DOUBLE_SIDED\n\tvarying vec3 vLightBack;\n\tvarying vec3 vIndirectBack;\n#endif\n#include <common>\n#include <uv_pars_vertex>\n#include <uv2_pars_vertex>\n#include <envmap_pars_vertex>\n#include <bsdfs>\n#include <lights_pars_begin>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <shadowmap_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <uv2_vertex>\n\t#include <color_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <worldpos_vertex>\n\t#include <envmap_vertex>\n\t#include <lights_lambert_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n}";

	var meshmatcap_frag = "#define MATCAP\nuniform vec3 diffuse;\nuniform float opacity;\nuniform sampler2D matcap;\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include <common>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <fog_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\tvec3 viewDir = normalize( vViewPosition );\n\tvec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) );\n\tvec3 y = cross( viewDir, x );\n\tvec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5;\n\t#ifdef USE_MATCAP\n\t\tvec4 matcapColor = texture2D( matcap, uv );\n\t\tmatcapColor = matcapTexelToLinear( matcapColor );\n\t#else\n\t\tvec4 matcapColor = vec4( 1.0 );\n\t#endif\n\tvec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

	var meshmatcap_vert = "#define MATCAP\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include <common>\n#include <uv_pars_vertex>\n#include <color_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <color_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#ifndef FLAT_SHADED\n\t\tvNormal = normalize( transformedNormal );\n\t#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <fog_vertex>\n\tvViewPosition = - mvPosition.xyz;\n}";

	var meshtoon_frag = "#define TOON\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include <common>\n#include <packing>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <uv2_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <emissivemap_pars_fragment>\n#include <gradientmap_pars_fragment>\n#include <fog_pars_fragment>\n#include <bsdfs>\n#include <lights_pars_begin>\n#include <lights_toon_pars_fragment>\n#include <shadowmap_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\t#include <emissivemap_fragment>\n\t#include <lights_toon_fragment>\n\t#include <lights_fragment_begin>\n\t#include <lights_fragment_maps>\n\t#include <lights_fragment_end>\n\t#include <aomap_fragment>\n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

	var meshtoon_vert = "#define TOON\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include <common>\n#include <uv_pars_vertex>\n#include <uv2_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <shadowmap_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <uv2_vertex>\n\t#include <color_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\tvViewPosition = - mvPosition.xyz;\n\t#include <worldpos_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n}";

	var meshphong_frag = "#define PHONG\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform vec3 specular;\nuniform float shininess;\nuniform float opacity;\n#include <common>\n#include <packing>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <uv2_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <emissivemap_pars_fragment>\n#include <envmap_common_pars_fragment>\n#include <envmap_pars_fragment>\n#include <cube_uv_reflection_fragment>\n#include <fog_pars_fragment>\n#include <bsdfs>\n#include <lights_pars_begin>\n#include <lights_phong_pars_fragment>\n#include <shadowmap_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <specularmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <specularmap_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\t#include <emissivemap_fragment>\n\t#include <lights_phong_fragment>\n\t#include <lights_fragment_begin>\n\t#include <lights_fragment_maps>\n\t#include <lights_fragment_end>\n\t#include <aomap_fragment>\n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#include <envmap_fragment>\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

	var meshphong_vert = "#define PHONG\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include <common>\n#include <uv_pars_vertex>\n#include <uv2_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <envmap_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <shadowmap_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <uv2_vertex>\n\t#include <color_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\tvViewPosition = - mvPosition.xyz;\n\t#include <worldpos_vertex>\n\t#include <envmap_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n}";

	var meshphysical_frag = "#define STANDARD\n#ifdef PHYSICAL\n\t#define REFLECTIVITY\n\t#define CLEARCOAT\n\t#define TRANSMISSION\n#endif\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\n#ifdef TRANSMISSION\n\tuniform float transmission;\n#endif\n#ifdef REFLECTIVITY\n\tuniform float reflectivity;\n#endif\n#ifdef CLEARCOAT\n\tuniform float clearcoat;\n\tuniform float clearcoatRoughness;\n#endif\n#ifdef USE_SHEEN\n\tuniform vec3 sheen;\n#endif\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif\n#include <common>\n#include <packing>\n#include <dithering_pars_fragment>\n#include <color_pars_fragment>\n#include <uv_pars_fragment>\n#include <uv2_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <aomap_pars_fragment>\n#include <lightmap_pars_fragment>\n#include <emissivemap_pars_fragment>\n#include <transmissionmap_pars_fragment>\n#include <bsdfs>\n#include <cube_uv_reflection_fragment>\n#include <envmap_common_pars_fragment>\n#include <envmap_physical_pars_fragment>\n#include <fog_pars_fragment>\n#include <lights_pars_begin>\n#include <lights_physical_pars_fragment>\n#include <shadowmap_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <clearcoat_pars_fragment>\n#include <roughnessmap_pars_fragment>\n#include <metalnessmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#ifdef TRANSMISSION\n\t\tfloat totalTransmission = transmission;\n\t#endif\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <color_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\t#include <roughnessmap_fragment>\n\t#include <metalnessmap_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\t#include <clearcoat_normal_fragment_begin>\n\t#include <clearcoat_normal_fragment_maps>\n\t#include <emissivemap_fragment>\n\t#include <transmissionmap_fragment>\n\t#include <lights_physical_fragment>\n\t#include <lights_fragment_begin>\n\t#include <lights_fragment_maps>\n\t#include <lights_fragment_end>\n\t#include <aomap_fragment>\n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#ifdef TRANSMISSION\n\t\tdiffuseColor.a *= mix( saturate( 1. - totalTransmission + linearToRelativeLuminance( reflectedLight.directSpecular + reflectedLight.indirectSpecular ) ), 1.0, metalness );\n\t#endif\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n\t#include <dithering_fragment>\n}";

	var meshphysical_vert = "#define STANDARD\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif\n#include <common>\n#include <uv_pars_vertex>\n#include <uv2_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <shadowmap_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <uv2_vertex>\n\t#include <color_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n\t#ifdef USE_TANGENT\n\t\tvTangent = normalize( transformedTangent );\n\t\tvBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );\n\t#endif\n#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\tvViewPosition = - mvPosition.xyz;\n\t#include <worldpos_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n}";

	var normal_frag = "#define NORMAL\nuniform float opacity;\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP )\n\tvarying vec3 vViewPosition;\n#endif\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif\n#include <packing>\n#include <uv_pars_fragment>\n#include <bumpmap_pars_fragment>\n#include <normalmap_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\t#include <logdepthbuf_fragment>\n\t#include <normal_fragment_begin>\n\t#include <normal_fragment_maps>\n\tgl_FragColor = vec4( packNormalToRGB( normal ), opacity );\n}";

	var normal_vert = "#define NORMAL\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP )\n\tvarying vec3 vViewPosition;\n#endif\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif\n#include <common>\n#include <uv_pars_vertex>\n#include <displacementmap_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <skinning_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n\t#ifdef USE_TANGENT\n\t\tvTangent = normalize( transformedTangent );\n\t\tvBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );\n\t#endif\n#endif\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <skinning_vertex>\n\t#include <displacementmap_vertex>\n\t#include <project_vertex>\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP )\n\tvViewPosition = - mvPosition.xyz;\n#endif\n}";

	var points_frag = "uniform vec3 diffuse;\nuniform float opacity;\n#include <common>\n#include <color_pars_fragment>\n#include <map_particle_pars_fragment>\n#include <fog_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <logdepthbuf_fragment>\n\t#include <map_particle_fragment>\n\t#include <color_fragment>\n\t#include <alphatest_fragment>\n\toutgoingLight = diffuseColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n\t#include <premultiplied_alpha_fragment>\n}";

	var points_vert = "uniform float size;\nuniform float scale;\n#include <common>\n#include <color_pars_vertex>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <color_vertex>\n\t#include <begin_vertex>\n\t#include <morphtarget_vertex>\n\t#include <project_vertex>\n\tgl_PointSize = size;\n\t#ifdef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );\n\t#endif\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <worldpos_vertex>\n\t#include <fog_vertex>\n}";

	var shadow_frag = "uniform vec3 color;\nuniform float opacity;\n#include <common>\n#include <packing>\n#include <fog_pars_fragment>\n#include <bsdfs>\n#include <lights_pars_begin>\n#include <shadowmap_pars_fragment>\n#include <shadowmask_pars_fragment>\nvoid main() {\n\tgl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n}";

	var shadow_vert = "#include <common>\n#include <fog_pars_vertex>\n#include <shadowmap_pars_vertex>\nvoid main() {\n\t#include <begin_vertex>\n\t#include <project_vertex>\n\t#include <worldpos_vertex>\n\t#include <beginnormal_vertex>\n\t#include <morphnormal_vertex>\n\t#include <skinbase_vertex>\n\t#include <skinnormal_vertex>\n\t#include <defaultnormal_vertex>\n\t#include <shadowmap_vertex>\n\t#include <fog_vertex>\n}";

	var sprite_frag = "uniform vec3 diffuse;\nuniform float opacity;\n#include <common>\n#include <uv_pars_fragment>\n#include <map_pars_fragment>\n#include <alphamap_pars_fragment>\n#include <fog_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\nvoid main() {\n\t#include <clipping_planes_fragment>\n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include <logdepthbuf_fragment>\n\t#include <map_fragment>\n\t#include <alphamap_fragment>\n\t#include <alphatest_fragment>\n\toutgoingLight = diffuseColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include <tonemapping_fragment>\n\t#include <encodings_fragment>\n\t#include <fog_fragment>\n}";

	var sprite_vert = "uniform float rotation;\nuniform vec2 center;\n#include <common>\n#include <uv_pars_vertex>\n#include <fog_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvoid main() {\n\t#include <uv_vertex>\n\tvec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );\n\tvec2 scale;\n\tscale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );\n\tscale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );\n\t#ifndef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) scale *= - mvPosition.z;\n\t#endif\n\tvec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;\n\tvec2 rotatedPosition;\n\trotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;\n\trotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;\n\tmvPosition.xy += rotatedPosition;\n\tgl_Position = projectionMatrix * mvPosition;\n\t#include <logdepthbuf_vertex>\n\t#include <clipping_planes_vertex>\n\t#include <fog_vertex>\n}";

	const ShaderChunk = {
		alphamap_fragment: alphamap_fragment,
		alphamap_pars_fragment: alphamap_pars_fragment,
		alphatest_fragment: alphatest_fragment,
		aomap_fragment: aomap_fragment,
		aomap_pars_fragment: aomap_pars_fragment,
		begin_vertex: begin_vertex,
		beginnormal_vertex: beginnormal_vertex,
		bsdfs: bsdfs,
		bumpmap_pars_fragment: bumpmap_pars_fragment,
		clipping_planes_fragment: clipping_planes_fragment,
		clipping_planes_pars_fragment: clipping_planes_pars_fragment,
		clipping_planes_pars_vertex: clipping_planes_pars_vertex,
		clipping_planes_vertex: clipping_planes_vertex,
		color_fragment: color_fragment,
		color_pars_fragment: color_pars_fragment,
		color_pars_vertex: color_pars_vertex,
		color_vertex: color_vertex,
		common: common,
		cube_uv_reflection_fragment: cube_uv_reflection_fragment,
		defaultnormal_vertex: defaultnormal_vertex,
		displacementmap_pars_vertex: displacementmap_pars_vertex,
		displacementmap_vertex: displacementmap_vertex,
		emissivemap_fragment: emissivemap_fragment,
		emissivemap_pars_fragment: emissivemap_pars_fragment,
		encodings_fragment: encodings_fragment,
		encodings_pars_fragment: encodings_pars_fragment,
		envmap_fragment: envmap_fragment,
		envmap_common_pars_fragment: envmap_common_pars_fragment,
		envmap_pars_fragment: envmap_pars_fragment,
		envmap_pars_vertex: envmap_pars_vertex,
		envmap_physical_pars_fragment: envmap_physical_pars_fragment,
		envmap_vertex: envmap_vertex,
		fog_vertex: fog_vertex,
		fog_pars_vertex: fog_pars_vertex,
		fog_fragment: fog_fragment,
		fog_pars_fragment: fog_pars_fragment,
		gradientmap_pars_fragment: gradientmap_pars_fragment,
		lightmap_fragment: lightmap_fragment,
		lightmap_pars_fragment: lightmap_pars_fragment,
		lights_lambert_vertex: lights_lambert_vertex,
		lights_pars_begin: lights_pars_begin,
		lights_toon_fragment: lights_toon_fragment,
		lights_toon_pars_fragment: lights_toon_pars_fragment,
		lights_phong_fragment: lights_phong_fragment,
		lights_phong_pars_fragment: lights_phong_pars_fragment,
		lights_physical_fragment: lights_physical_fragment,
		lights_physical_pars_fragment: lights_physical_pars_fragment,
		lights_fragment_begin: lights_fragment_begin,
		lights_fragment_maps: lights_fragment_maps,
		lights_fragment_end: lights_fragment_end,
		logdepthbuf_fragment: logdepthbuf_fragment,
		logdepthbuf_pars_fragment: logdepthbuf_pars_fragment,
		logdepthbuf_pars_vertex: logdepthbuf_pars_vertex,
		logdepthbuf_vertex: logdepthbuf_vertex,
		map_fragment: map_fragment,
		map_pars_fragment: map_pars_fragment,
		map_particle_fragment: map_particle_fragment,
		map_particle_pars_fragment: map_particle_pars_fragment,
		metalnessmap_fragment: metalnessmap_fragment,
		metalnessmap_pars_fragment: metalnessmap_pars_fragment,
		morphnormal_vertex: morphnormal_vertex,
		morphtarget_pars_vertex: morphtarget_pars_vertex,
		morphtarget_vertex: morphtarget_vertex,
		normal_fragment_begin: normal_fragment_begin,
		normal_fragment_maps: normal_fragment_maps,
		normalmap_pars_fragment: normalmap_pars_fragment,
		clearcoat_normal_fragment_begin: clearcoat_normal_fragment_begin,
		clearcoat_normal_fragment_maps: clearcoat_normal_fragment_maps,
		clearcoat_pars_fragment: clearcoat_pars_fragment,
		packing: packing,
		premultiplied_alpha_fragment: premultiplied_alpha_fragment,
		project_vertex: project_vertex,
		dithering_fragment: dithering_fragment,
		dithering_pars_fragment: dithering_pars_fragment,
		roughnessmap_fragment: roughnessmap_fragment,
		roughnessmap_pars_fragment: roughnessmap_pars_fragment,
		shadowmap_pars_fragment: shadowmap_pars_fragment,
		shadowmap_pars_vertex: shadowmap_pars_vertex,
		shadowmap_vertex: shadowmap_vertex,
		shadowmask_pars_fragment: shadowmask_pars_fragment,
		skinbase_vertex: skinbase_vertex,
		skinning_pars_vertex: skinning_pars_vertex,
		skinning_vertex: skinning_vertex,
		skinnormal_vertex: skinnormal_vertex,
		specularmap_fragment: specularmap_fragment,
		specularmap_pars_fragment: specularmap_pars_fragment,
		tonemapping_fragment: tonemapping_fragment,
		tonemapping_pars_fragment: tonemapping_pars_fragment,
		transmissionmap_fragment: transmissionmap_fragment,
		transmissionmap_pars_fragment: transmissionmap_pars_fragment,
		uv_pars_fragment: uv_pars_fragment,
		uv_pars_vertex: uv_pars_vertex,
		uv_vertex: uv_vertex,
		uv2_pars_fragment: uv2_pars_fragment,
		uv2_pars_vertex: uv2_pars_vertex,
		uv2_vertex: uv2_vertex,
		worldpos_vertex: worldpos_vertex,

		background_frag: background_frag,
		background_vert: background_vert,
		cube_frag: cube_frag,
		cube_vert: cube_vert,
		depth_frag: depth_frag,
		depth_vert: depth_vert,
		distanceRGBA_frag: distanceRGBA_frag,
		distanceRGBA_vert: distanceRGBA_vert,
		equirect_frag: equirect_frag,
		equirect_vert: equirect_vert,
		linedashed_frag: linedashed_frag,
		linedashed_vert: linedashed_vert,
		meshbasic_frag: meshbasic_frag,
		meshbasic_vert: meshbasic_vert,
		meshlambert_frag: meshlambert_frag,
		meshlambert_vert: meshlambert_vert,
		meshmatcap_frag: meshmatcap_frag,
		meshmatcap_vert: meshmatcap_vert,
		meshtoon_frag: meshtoon_frag,
		meshtoon_vert: meshtoon_vert,
		meshphong_frag: meshphong_frag,
		meshphong_vert: meshphong_vert,
		meshphysical_frag: meshphysical_frag,
		meshphysical_vert: meshphysical_vert,
		normal_frag: normal_frag,
		normal_vert: normal_vert,
		points_frag: points_frag,
		points_vert: points_vert,
		shadow_frag: shadow_frag,
		shadow_vert: shadow_vert,
		sprite_frag: sprite_frag,
		sprite_vert: sprite_vert
	};

	/**
	 * Uniforms library for shared webgl shaders
	 */

	const UniformsLib = {

		common: {

			diffuse: { value: new Color( 0xeeeeee ) },
			opacity: { value: 1.0 },

			map: { value: null },
			uvTransform: { value: new Matrix3() },
			uv2Transform: { value: new Matrix3() },

			alphaMap: { value: null },

		},

		specularmap: {

			specularMap: { value: null },

		},

		envmap: {

			envMap: { value: null },
			flipEnvMap: { value: - 1 },
			reflectivity: { value: 1.0 },
			refractionRatio: { value: 0.98 },
			maxMipLevel: { value: 0 }

		},

		aomap: {

			aoMap: { value: null },
			aoMapIntensity: { value: 1 }

		},

		lightmap: {

			lightMap: { value: null },
			lightMapIntensity: { value: 1 }

		},

		emissivemap: {

			emissiveMap: { value: null }

		},

		bumpmap: {

			bumpMap: { value: null },
			bumpScale: { value: 1 }

		},

		normalmap: {

			normalMap: { value: null },
			normalScale: { value: new Vector2( 1, 1 ) }

		},

		displacementmap: {

			displacementMap: { value: null },
			displacementScale: { value: 1 },
			displacementBias: { value: 0 }

		},

		roughnessmap: {

			roughnessMap: { value: null }

		},

		metalnessmap: {

			metalnessMap: { value: null }

		},

		gradientmap: {

			gradientMap: { value: null }

		},

		fog: {

			fogDensity: { value: 0.00025 },
			fogNear: { value: 1 },
			fogFar: { value: 2000 },
			fogColor: { value: new Color( 0xffffff ) }

		},

		lights: {

			ambientLightColor: { value: [] },

			lightProbe: { value: [] },

			directionalLights: { value: [], properties: {
				direction: {},
				color: {}
			} },

			directionalLightShadows: { value: [], properties: {
				shadowBias: {},
				shadowNormalBias: {},
				shadowRadius: {},
				shadowMapSize: {}
			} },

			directionalShadowMap: { value: [] },
			directionalShadowMatrix: { value: [] },

			spotLights: { value: [], properties: {
				color: {},
				position: {},
				direction: {},
				distance: {},
				coneCos: {},
				penumbraCos: {},
				decay: {}
			} },

			spotLightShadows: { value: [], properties: {
				shadowBias: {},
				shadowNormalBias: {},
				shadowRadius: {},
				shadowMapSize: {}
			} },

			spotShadowMap: { value: [] },
			spotShadowMatrix: { value: [] },

			pointLights: { value: [], properties: {
				color: {},
				position: {},
				decay: {},
				distance: {}
			} },

			pointLightShadows: { value: [], properties: {
				shadowBias: {},
				shadowNormalBias: {},
				shadowRadius: {},
				shadowMapSize: {},
				shadowCameraNear: {},
				shadowCameraFar: {}
			} },

			pointShadowMap: { value: [] },
			pointShadowMatrix: { value: [] },

			hemisphereLights: { value: [], properties: {
				direction: {},
				skyColor: {},
				groundColor: {}
			} },

			// TODO (abelnation): RectAreaLight BRDF data needs to be moved from example to main src
			rectAreaLights: { value: [], properties: {
				color: {},
				position: {},
				width: {},
				height: {}
			} },

			ltc_1: { value: null },
			ltc_2: { value: null }

		},

		points: {

			diffuse: { value: new Color( 0xeeeeee ) },
			opacity: { value: 1.0 },
			size: { value: 1.0 },
			scale: { value: 1.0 },
			map: { value: null },
			alphaMap: { value: null },
			uvTransform: { value: new Matrix3() }

		},

		sprite: {

			diffuse: { value: new Color( 0xeeeeee ) },
			opacity: { value: 1.0 },
			center: { value: new Vector2( 0.5, 0.5 ) },
			rotation: { value: 0.0 },
			map: { value: null },
			alphaMap: { value: null },
			uvTransform: { value: new Matrix3() }

		}

	};

	const ShaderLib = {

		basic: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.specularmap,
				UniformsLib.envmap,
				UniformsLib.aomap,
				UniformsLib.lightmap,
				UniformsLib.fog
			] ),

			vertexShader: ShaderChunk.meshbasic_vert,
			fragmentShader: ShaderChunk.meshbasic_frag

		},

		lambert: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.specularmap,
				UniformsLib.envmap,
				UniformsLib.aomap,
				UniformsLib.lightmap,
				UniformsLib.emissivemap,
				UniformsLib.fog,
				UniformsLib.lights,
				{
					emissive: { value: new Color( 0x000000 ) }
				}
			] ),

			vertexShader: ShaderChunk.meshlambert_vert,
			fragmentShader: ShaderChunk.meshlambert_frag

		},

		phong: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.specularmap,
				UniformsLib.envmap,
				UniformsLib.aomap,
				UniformsLib.lightmap,
				UniformsLib.emissivemap,
				UniformsLib.bumpmap,
				UniformsLib.normalmap,
				UniformsLib.displacementmap,
				UniformsLib.fog,
				UniformsLib.lights,
				{
					emissive: { value: new Color( 0x000000 ) },
					specular: { value: new Color( 0x111111 ) },
					shininess: { value: 30 }
				}
			] ),

			vertexShader: ShaderChunk.meshphong_vert,
			fragmentShader: ShaderChunk.meshphong_frag

		},

		standard: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.envmap,
				UniformsLib.aomap,
				UniformsLib.lightmap,
				UniformsLib.emissivemap,
				UniformsLib.bumpmap,
				UniformsLib.normalmap,
				UniformsLib.displacementmap,
				UniformsLib.roughnessmap,
				UniformsLib.metalnessmap,
				UniformsLib.fog,
				UniformsLib.lights,
				{
					emissive: { value: new Color( 0x000000 ) },
					roughness: { value: 1.0 },
					metalness: { value: 0.0 },
					envMapIntensity: { value: 1 } // temporary
				}
			] ),

			vertexShader: ShaderChunk.meshphysical_vert,
			fragmentShader: ShaderChunk.meshphysical_frag

		},

		toon: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.aomap,
				UniformsLib.lightmap,
				UniformsLib.emissivemap,
				UniformsLib.bumpmap,
				UniformsLib.normalmap,
				UniformsLib.displacementmap,
				UniformsLib.gradientmap,
				UniformsLib.fog,
				UniformsLib.lights,
				{
					emissive: { value: new Color( 0x000000 ) }
				}
			] ),

			vertexShader: ShaderChunk.meshtoon_vert,
			fragmentShader: ShaderChunk.meshtoon_frag

		},

		matcap: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.bumpmap,
				UniformsLib.normalmap,
				UniformsLib.displacementmap,
				UniformsLib.fog,
				{
					matcap: { value: null }
				}
			] ),

			vertexShader: ShaderChunk.meshmatcap_vert,
			fragmentShader: ShaderChunk.meshmatcap_frag

		},

		points: {

			uniforms: mergeUniforms( [
				UniformsLib.points,
				UniformsLib.fog
			] ),

			vertexShader: ShaderChunk.points_vert,
			fragmentShader: ShaderChunk.points_frag

		},

		dashed: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.fog,
				{
					scale: { value: 1 },
					dashSize: { value: 1 },
					totalSize: { value: 2 }
				}
			] ),

			vertexShader: ShaderChunk.linedashed_vert,
			fragmentShader: ShaderChunk.linedashed_frag

		},

		depth: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.displacementmap
			] ),

			vertexShader: ShaderChunk.depth_vert,
			fragmentShader: ShaderChunk.depth_frag

		},

		normal: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.bumpmap,
				UniformsLib.normalmap,
				UniformsLib.displacementmap,
				{
					opacity: { value: 1.0 }
				}
			] ),

			vertexShader: ShaderChunk.normal_vert,
			fragmentShader: ShaderChunk.normal_frag

		},

		sprite: {

			uniforms: mergeUniforms( [
				UniformsLib.sprite,
				UniformsLib.fog
			] ),

			vertexShader: ShaderChunk.sprite_vert,
			fragmentShader: ShaderChunk.sprite_frag

		},

		background: {

			uniforms: {
				uvTransform: { value: new Matrix3() },
				t2D: { value: null },
			},

			vertexShader: ShaderChunk.background_vert,
			fragmentShader: ShaderChunk.background_frag

		},
		/* -------------------------------------------------------------------------
		//	Cube map shader
		 ------------------------------------------------------------------------- */

		cube: {

			uniforms: mergeUniforms( [
				UniformsLib.envmap,
				{
					opacity: { value: 1.0 }
				}
			] ),

			vertexShader: ShaderChunk.cube_vert,
			fragmentShader: ShaderChunk.cube_frag

		},

		equirect: {

			uniforms: {
				tEquirect: { value: null },
			},

			vertexShader: ShaderChunk.equirect_vert,
			fragmentShader: ShaderChunk.equirect_frag

		},

		distanceRGBA: {

			uniforms: mergeUniforms( [
				UniformsLib.common,
				UniformsLib.displacementmap,
				{
					referencePosition: { value: new Vector3() },
					nearDistance: { value: 1 },
					farDistance: { value: 1000 }
				}
			] ),

			vertexShader: ShaderChunk.distanceRGBA_vert,
			fragmentShader: ShaderChunk.distanceRGBA_frag

		},

		shadow: {

			uniforms: mergeUniforms( [
				UniformsLib.lights,
				UniformsLib.fog,
				{
					color: { value: new Color( 0x00000 ) },
					opacity: { value: 1.0 }
				},
			] ),

			vertexShader: ShaderChunk.shadow_vert,
			fragmentShader: ShaderChunk.shadow_frag

		}

	};

	ShaderLib.physical = {

		uniforms: mergeUniforms( [
			ShaderLib.standard.uniforms,
			{
				clearcoat: { value: 0 },
				clearcoatMap: { value: null },
				clearcoatRoughness: { value: 0 },
				clearcoatRoughnessMap: { value: null },
				clearcoatNormalScale: { value: new Vector2( 1, 1 ) },
				clearcoatNormalMap: { value: null },
				sheen: { value: new Color( 0x000000 ) },
				transmission: { value: 0 },
				transmissionMap: { value: null },
			}
		] ),

		vertexShader: ShaderChunk.meshphysical_vert,
		fragmentShader: ShaderChunk.meshphysical_frag

	};

	function WebGLBackground( renderer, cubemaps, state, objects, premultipliedAlpha ) {

		const clearColor = new Color( 0x000000 );
		let clearAlpha = 0;

		let planeMesh;
		let boxMesh;

		let currentBackground = null;
		let currentBackgroundVersion = 0;
		let currentTonemapping = null;

		function render( renderList, scene, camera, forceClear ) {

			let background = scene.isScene === true ? scene.background : null;

			if ( background && background.isTexture ) {

				background = cubemaps.get( background );

			}

			// Ignore background in AR
			// TODO: Reconsider this.

			const xr = renderer.xr;
			const session = xr.getSession && xr.getSession();

			if ( session && session.environmentBlendMode === 'additive' ) {

				background = null;

			}

			if ( background === null ) {

				setClear( clearColor, clearAlpha );

			} else if ( background && background.isColor ) {

				setClear( background, 1 );
				forceClear = true;

			}

			if ( renderer.autoClear || forceClear ) {

				renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );

			}

			if ( background && ( background.isCubeTexture || background.isWebGLCubeRenderTarget || background.mapping === CubeUVReflectionMapping ) ) {

				if ( boxMesh === undefined ) {

					boxMesh = new Mesh(
						new BoxBufferGeometry( 1, 1, 1 ),
						new ShaderMaterial( {
							name: 'BackgroundCubeMaterial',
							uniforms: cloneUniforms( ShaderLib.cube.uniforms ),
							vertexShader: ShaderLib.cube.vertexShader,
							fragmentShader: ShaderLib.cube.fragmentShader,
							side: BackSide,
							depthTest: false,
							depthWrite: false,
							fog: false
						} )
					);

					boxMesh.geometry.deleteAttribute( 'normal' );
					boxMesh.geometry.deleteAttribute( 'uv' );

					boxMesh.onBeforeRender = function ( renderer, scene, camera ) {

						this.matrixWorld.copyPosition( camera.matrixWorld );

					};

					// enable code injection for non-built-in material
					Object.defineProperty( boxMesh.material, 'envMap', {

						get: function () {

							return this.uniforms.envMap.value;

						}

					} );

					objects.update( boxMesh );

				}

				if ( background.isWebGLCubeRenderTarget ) {

					// TODO Deprecate

					background = background.texture;

				}

				boxMesh.material.uniforms.envMap.value = background;
				boxMesh.material.uniforms.flipEnvMap.value = ( background.isCubeTexture && background._needsFlipEnvMap ) ? - 1 : 1;

				if ( currentBackground !== background ||
					currentBackgroundVersion !== background.version ||
					currentTonemapping !== renderer.toneMapping ) {

					boxMesh.material.needsUpdate = true;

					currentBackground = background;
					currentBackgroundVersion = background.version;
					currentTonemapping = renderer.toneMapping;

				}

				// push to the pre-sorted opaque render list
				renderList.unshift( boxMesh, boxMesh.geometry, boxMesh.material, 0, 0, null );

			} else if ( background && background.isTexture ) {

				if ( planeMesh === undefined ) {

					planeMesh = new Mesh(
						new PlaneBufferGeometry( 2, 2 ),
						new ShaderMaterial( {
							name: 'BackgroundMaterial',
							uniforms: cloneUniforms( ShaderLib.background.uniforms ),
							vertexShader: ShaderLib.background.vertexShader,
							fragmentShader: ShaderLib.background.fragmentShader,
							side: FrontSide,
							depthTest: false,
							depthWrite: false,
							fog: false
						} )
					);

					planeMesh.geometry.deleteAttribute( 'normal' );

					// enable code injection for non-built-in material
					Object.defineProperty( planeMesh.material, 'map', {

						get: function () {

							return this.uniforms.t2D.value;

						}

					} );

					objects.update( planeMesh );

				}

				planeMesh.material.uniforms.t2D.value = background;

				if ( background.matrixAutoUpdate === true ) {

					background.updateMatrix();

				}

				planeMesh.material.uniforms.uvTransform.value.copy( background.matrix );

				if ( currentBackground !== background ||
					currentBackgroundVersion !== background.version ||
					currentTonemapping !== renderer.toneMapping ) {

					planeMesh.material.needsUpdate = true;

					currentBackground = background;
					currentBackgroundVersion = background.version;
					currentTonemapping = renderer.toneMapping;

				}


				// push to the pre-sorted opaque render list
				renderList.unshift( planeMesh, planeMesh.geometry, planeMesh.material, 0, 0, null );

			}

		}

		function setClear( color, alpha ) {

			state.buffers.color.setClear( color.r, color.g, color.b, alpha, premultipliedAlpha );

		}

		return {

			getClearColor: function () {

				return clearColor;

			},
			setClearColor: function ( color, alpha = 1 ) {

				clearColor.set( color );
				clearAlpha = alpha;
				setClear( clearColor, clearAlpha );

			},
			getClearAlpha: function () {

				return clearAlpha;

			},
			setClearAlpha: function ( alpha ) {

				clearAlpha = alpha;
				setClear( clearColor, clearAlpha );

			},
			render: render

		};

	}

	function WebGLBindingStates( gl, extensions, attributes, capabilities ) {

		const maxVertexAttributes = gl.getParameter( 34921 );

		const extension = capabilities.isWebGL2 ? null : extensions.get( 'OES_vertex_array_object' );
		const vaoAvailable = capabilities.isWebGL2 || extension !== null;

		const bindingStates = {};

		const defaultState = createBindingState( null );
		let currentState = defaultState;

		function setup( object, material, program, geometry, index ) {

			let updateBuffers = false;

			if ( vaoAvailable ) {

				const state = getBindingState( geometry, program, material );

				if ( currentState !== state ) {

					currentState = state;
					bindVertexArrayObject( currentState.object );

				}

				updateBuffers = needsUpdate( geometry, index );

				if ( updateBuffers ) saveCache( geometry, index );

			} else {

				const wireframe = ( material.wireframe === true );

				if ( currentState.geometry !== geometry.id ||
					currentState.program !== program.id ||
					currentState.wireframe !== wireframe ) {

					currentState.geometry = geometry.id;
					currentState.program = program.id;
					currentState.wireframe = wireframe;

					updateBuffers = true;

				}

			}

			if ( object.isInstancedMesh === true ) {

				updateBuffers = true;

			}

			if ( index !== null ) {

				attributes.update( index, 34963 );

			}

			if ( updateBuffers ) {

				setupVertexAttributes( object, material, program, geometry );

				if ( index !== null ) {

					gl.bindBuffer( 34963, attributes.get( index ).buffer );

				}

			}

		}

		function createVertexArrayObject() {

			if ( capabilities.isWebGL2 ) return gl.createVertexArray();

			return extension.createVertexArrayOES();

		}

		function bindVertexArrayObject( vao ) {

			if ( capabilities.isWebGL2 ) return gl.bindVertexArray( vao );

			return extension.bindVertexArrayOES( vao );

		}

		function deleteVertexArrayObject( vao ) {

			if ( capabilities.isWebGL2 ) return gl.deleteVertexArray( vao );

			return extension.deleteVertexArrayOES( vao );

		}

		function getBindingState( geometry, program, material ) {

			const wireframe = ( material.wireframe === true );

			let programMap = bindingStates[ geometry.id ];

			if ( programMap === undefined ) {

				programMap = {};
				bindingStates[ geometry.id ] = programMap;

			}

			let stateMap = programMap[ program.id ];

			if ( stateMap === undefined ) {

				stateMap = {};
				programMap[ program.id ] = stateMap;

			}

			let state = stateMap[ wireframe ];

			if ( state === undefined ) {

				state = createBindingState( createVertexArrayObject() );
				stateMap[ wireframe ] = state;

			}

			return state;

		}

		function createBindingState( vao ) {

			const newAttributes = [];
			const enabledAttributes = [];
			const attributeDivisors = [];

			for ( let i = 0; i < maxVertexAttributes; i ++ ) {

				newAttributes[ i ] = 0;
				enabledAttributes[ i ] = 0;
				attributeDivisors[ i ] = 0;

			}

			return {

				// for backward compatibility on non-VAO support browser
				geometry: null,
				program: null,
				wireframe: false,

				newAttributes: newAttributes,
				enabledAttributes: enabledAttributes,
				attributeDivisors: attributeDivisors,
				object: vao,
				attributes: {},
				index: null

			};

		}

		function needsUpdate( geometry, index ) {

			const cachedAttributes = currentState.attributes;
			const geometryAttributes = geometry.attributes;

			let attributesNum = 0;

			for ( const key in geometryAttributes ) {

				const cachedAttribute = cachedAttributes[ key ];
				const geometryAttribute = geometryAttributes[ key ];

				if ( cachedAttribute === undefined ) return true;

				if ( cachedAttribute.attribute !== geometryAttribute ) return true;

				if ( cachedAttribute.data !== geometryAttribute.data ) return true;

				attributesNum ++;

			}

			if ( currentState.attributesNum !== attributesNum ) return true;

			if ( currentState.index !== index ) return true;

			return false;

		}

		function saveCache( geometry, index ) {

			const cache = {};
			const attributes = geometry.attributes;
			let attributesNum = 0;

			for ( const key in attributes ) {

				const attribute = attributes[ key ];

				const data = {};
				data.attribute = attribute;

				if ( attribute.data ) {

					data.data = attribute.data;

				}

				cache[ key ] = data;

				attributesNum ++;

			}

			currentState.attributes = cache;
			currentState.attributesNum = attributesNum;

			currentState.index = index;

		}

		function initAttributes() {

			const newAttributes = currentState.newAttributes;

			for ( let i = 0, il = newAttributes.length; i < il; i ++ ) {

				newAttributes[ i ] = 0;

			}

		}

		function enableAttribute( attribute ) {

			enableAttributeAndDivisor( attribute, 0 );

		}

		function enableAttributeAndDivisor( attribute, meshPerAttribute ) {

			const newAttributes = currentState.newAttributes;
			const enabledAttributes = currentState.enabledAttributes;
			const attributeDivisors = currentState.attributeDivisors;

			newAttributes[ attribute ] = 1;

			if ( enabledAttributes[ attribute ] === 0 ) {

				gl.enableVertexAttribArray( attribute );
				enabledAttributes[ attribute ] = 1;

			}

			if ( attributeDivisors[ attribute ] !== meshPerAttribute ) {

				const extension = capabilities.isWebGL2 ? gl : extensions.get( 'ANGLE_instanced_arrays' );

				extension[ capabilities.isWebGL2 ? 'vertexAttribDivisor' : 'vertexAttribDivisorANGLE' ]( attribute, meshPerAttribute );
				attributeDivisors[ attribute ] = meshPerAttribute;

			}

		}

		function disableUnusedAttributes() {

			const newAttributes = currentState.newAttributes;
			const enabledAttributes = currentState.enabledAttributes;

			for ( let i = 0, il = enabledAttributes.length; i < il; i ++ ) {

				if ( enabledAttributes[ i ] !== newAttributes[ i ] ) {

					gl.disableVertexAttribArray( i );
					enabledAttributes[ i ] = 0;

				}

			}

		}

		function vertexAttribPointer( index, size, type, normalized, stride, offset ) {

			if ( capabilities.isWebGL2 === true && ( type === 5124 || type === 5125 ) ) {

				gl.vertexAttribIPointer( index, size, type, stride, offset );

			} else {

				gl.vertexAttribPointer( index, size, type, normalized, stride, offset );

			}

		}

		function setupVertexAttributes( object, material, program, geometry ) {

			if ( capabilities.isWebGL2 === false && ( object.isInstancedMesh || geometry.isInstancedBufferGeometry ) ) {

				if ( extensions.get( 'ANGLE_instanced_arrays' ) === null ) return;

			}

			initAttributes();

			const geometryAttributes = geometry.attributes;

			const programAttributes = program.getAttributes();

			const materialDefaultAttributeValues = material.defaultAttributeValues;

			for ( const name in programAttributes ) {

				const programAttribute = programAttributes[ name ];

				if ( programAttribute >= 0 ) {

					const geometryAttribute = geometryAttributes[ name ];

					if ( geometryAttribute !== undefined ) {

						const normalized = geometryAttribute.normalized;
						const size = geometryAttribute.itemSize;

						const attribute = attributes.get( geometryAttribute );

						// TODO Attribute may not be available on context restore

						if ( attribute === undefined ) continue;

						const buffer = attribute.buffer;
						const type = attribute.type;
						const bytesPerElement = attribute.bytesPerElement;

						if ( geometryAttribute.isInterleavedBufferAttribute ) {

							const data = geometryAttribute.data;
							const stride = data.stride;
							const offset = geometryAttribute.offset;

							if ( data && data.isInstancedInterleavedBuffer ) {

								enableAttributeAndDivisor( programAttribute, data.meshPerAttribute );

								if ( geometry._maxInstanceCount === undefined ) {

									geometry._maxInstanceCount = data.meshPerAttribute * data.count;

								}

							} else {

								enableAttribute( programAttribute );

							}

							gl.bindBuffer( 34962, buffer );
							vertexAttribPointer( programAttribute, size, type, normalized, stride * bytesPerElement, offset * bytesPerElement );

						} else {

							if ( geometryAttribute.isInstancedBufferAttribute ) {

								enableAttributeAndDivisor( programAttribute, geometryAttribute.meshPerAttribute );

								if ( geometry._maxInstanceCount === undefined ) {

									geometry._maxInstanceCount = geometryAttribute.meshPerAttribute * geometryAttribute.count;

								}

							} else {

								enableAttribute( programAttribute );

							}

							gl.bindBuffer( 34962, buffer );
							vertexAttribPointer( programAttribute, size, type, normalized, 0, 0 );

						}

					} else if ( name === 'instanceMatrix' ) {

						const attribute = attributes.get( object.instanceMatrix );

						// TODO Attribute may not be available on context restore

						if ( attribute === undefined ) continue;

						const buffer = attribute.buffer;
						const type = attribute.type;

						enableAttributeAndDivisor( programAttribute + 0, 1 );
						enableAttributeAndDivisor( programAttribute + 1, 1 );
						enableAttributeAndDivisor( programAttribute + 2, 1 );
						enableAttributeAndDivisor( programAttribute + 3, 1 );

						gl.bindBuffer( 34962, buffer );

						gl.vertexAttribPointer( programAttribute + 0, 4, type, false, 64, 0 );
						gl.vertexAttribPointer( programAttribute + 1, 4, type, false, 64, 16 );
						gl.vertexAttribPointer( programAttribute + 2, 4, type, false, 64, 32 );
						gl.vertexAttribPointer( programAttribute + 3, 4, type, false, 64, 48 );

					} else if ( name === 'instanceColor' ) {

						const attribute = attributes.get( object.instanceColor );

						// TODO Attribute may not be available on context restore

						if ( attribute === undefined ) continue;

						const buffer = attribute.buffer;
						const type = attribute.type;

						enableAttributeAndDivisor( programAttribute, 1 );

						gl.bindBuffer( 34962, buffer );

						gl.vertexAttribPointer( programAttribute, 3, type, false, 12, 0 );

					} else if ( materialDefaultAttributeValues !== undefined ) {

						const value = materialDefaultAttributeValues[ name ];

						if ( value !== undefined ) {

							switch ( value.length ) {

								case 2:
									gl.vertexAttrib2fv( programAttribute, value );
									break;

								case 3:
									gl.vertexAttrib3fv( programAttribute, value );
									break;

								case 4:
									gl.vertexAttrib4fv( programAttribute, value );
									break;

								default:
									gl.vertexAttrib1fv( programAttribute, value );

							}

						}

					}

				}

			}

			disableUnusedAttributes();

		}

		function dispose() {

			reset();

			for ( const geometryId in bindingStates ) {

				const programMap = bindingStates[ geometryId ];

				for ( const programId in programMap ) {

					const stateMap = programMap[ programId ];

					for ( const wireframe in stateMap ) {

						deleteVertexArrayObject( stateMap[ wireframe ].object );

						delete stateMap[ wireframe ];

					}

					delete programMap[ programId ];

				}

				delete bindingStates[ geometryId ];

			}

		}

		function releaseStatesOfGeometry( geometry ) {

			if ( bindingStates[ geometry.id ] === undefined ) return;

			const programMap = bindingStates[ geometry.id ];

			for ( const programId in programMap ) {

				const stateMap = programMap[ programId ];

				for ( const wireframe in stateMap ) {

					deleteVertexArrayObject( stateMap[ wireframe ].object );

					delete stateMap[ wireframe ];

				}

				delete programMap[ programId ];

			}

			delete bindingStates[ geometry.id ];

		}

		function releaseStatesOfProgram( program ) {

			for ( const geometryId in bindingStates ) {

				const programMap = bindingStates[ geometryId ];

				if ( programMap[ program.id ] === undefined ) continue;

				const stateMap = programMap[ program.id ];

				for ( const wireframe in stateMap ) {

					deleteVertexArrayObject( stateMap[ wireframe ].object );

					delete stateMap[ wireframe ];

				}

				delete programMap[ program.id ];

			}

		}

		function reset() {

			resetDefaultState();

			if ( currentState === defaultState ) return;

			currentState = defaultState;
			bindVertexArrayObject( currentState.object );

		}

		// for backward-compatilibity

		function resetDefaultState() {

			defaultState.geometry = null;
			defaultState.program = null;
			defaultState.wireframe = false;

		}

		return {

			setup: setup,
			reset: reset,
			resetDefaultState: resetDefaultState,
			dispose: dispose,
			releaseStatesOfGeometry: releaseStatesOfGeometry,
			releaseStatesOfProgram: releaseStatesOfProgram,

			initAttributes: initAttributes,
			enableAttribute: enableAttribute,
			disableUnusedAttributes: disableUnusedAttributes

		};

	}

	function WebGLBufferRenderer( gl, extensions, info, capabilities ) {

		const isWebGL2 = capabilities.isWebGL2;

		let mode;

		function setMode( value ) {

			mode = value;

		}

		function render( start, count ) {

			gl.drawArrays( mode, start, count );

			info.update( count, mode, 1 );

		}

		function renderInstances( start, count, primcount ) {

			if ( primcount === 0 ) return;

			let extension, methodName;

			if ( isWebGL2 ) {

				extension = gl;
				methodName = 'drawArraysInstanced';

			} else {

				extension = extensions.get( 'ANGLE_instanced_arrays' );
				methodName = 'drawArraysInstancedANGLE';

				if ( extension === null ) {

					console.error( 'THREE.WebGLBufferRenderer: using THREE.InstancedBufferGeometry but hardware does not support extension ANGLE_instanced_arrays.' );
					return;

				}

			}

			extension[ methodName ]( mode, start, count, primcount );

			info.update( count, mode, primcount );

		}

		//

		this.setMode = setMode;
		this.render = render;
		this.renderInstances = renderInstances;

	}

	function WebGLCapabilities( gl, extensions, parameters ) {

		let maxAnisotropy;

		function getMaxAnisotropy() {

			if ( maxAnisotropy !== undefined ) return maxAnisotropy;

			const extension = extensions.get( 'EXT_texture_filter_anisotropic' );

			if ( extension !== null ) {

				maxAnisotropy = gl.getParameter( extension.MAX_TEXTURE_MAX_ANISOTROPY_EXT );

			} else {

				maxAnisotropy = 0;

			}

			return maxAnisotropy;

		}

		function getMaxPrecision( precision ) {

			if ( precision === 'highp' ) {

				if ( gl.getShaderPrecisionFormat( 35633, 36338 ).precision > 0 &&
					gl.getShaderPrecisionFormat( 35632, 36338 ).precision > 0 ) {

					return 'highp';

				}

				precision = 'mediump';

			}

			if ( precision === 'mediump' ) {

				if ( gl.getShaderPrecisionFormat( 35633, 36337 ).precision > 0 &&
					gl.getShaderPrecisionFormat( 35632, 36337 ).precision > 0 ) {

					return 'mediump';

				}

			}

			return 'lowp';

		}

		/* eslint-disable no-undef */
		const isWebGL2 = ( typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext ) ||
			( typeof WebGL2ComputeRenderingContext !== 'undefined' && gl instanceof WebGL2ComputeRenderingContext );
		/* eslint-enable no-undef */

		let precision = parameters.precision !== undefined ? parameters.precision : 'highp';
		const maxPrecision = getMaxPrecision( precision );

		if ( maxPrecision !== precision ) {

			console.warn( 'THREE.WebGLRenderer:', precision, 'not supported, using', maxPrecision, 'instead.' );
			precision = maxPrecision;

		}

		const logarithmicDepthBuffer = parameters.logarithmicDepthBuffer === true;

		const maxTextures = gl.getParameter( 34930 );
		const maxVertexTextures = gl.getParameter( 35660 );
		const maxTextureSize = gl.getParameter( 3379 );
		const maxCubemapSize = gl.getParameter( 34076 );

		const maxAttributes = gl.getParameter( 34921 );
		const maxVertexUniforms = gl.getParameter( 36347 );
		const maxVaryings = gl.getParameter( 36348 );
		const maxFragmentUniforms = gl.getParameter( 36349 );

		const vertexTextures = maxVertexTextures > 0;
		const floatFragmentTextures = isWebGL2 || !! extensions.get( 'OES_texture_float' );
		const floatVertexTextures = vertexTextures && floatFragmentTextures;

		const maxSamples = isWebGL2 ? gl.getParameter( 36183 ) : 0;

		return {

			isWebGL2: isWebGL2,

			getMaxAnisotropy: getMaxAnisotropy,
			getMaxPrecision: getMaxPrecision,

			precision: precision,
			logarithmicDepthBuffer: logarithmicDepthBuffer,

			maxTextures: maxTextures,
			maxVertexTextures: maxVertexTextures,
			maxTextureSize: maxTextureSize,
			maxCubemapSize: maxCubemapSize,

			maxAttributes: maxAttributes,
			maxVertexUniforms: maxVertexUniforms,
			maxVaryings: maxVaryings,
			maxFragmentUniforms: maxFragmentUniforms,

			vertexTextures: vertexTextures,
			floatFragmentTextures: floatFragmentTextures,
			floatVertexTextures: floatVertexTextures,

			maxSamples: maxSamples

		};

	}

	function WebGLClipping( properties ) {

		const scope = this;

		let globalState = null,
			numGlobalPlanes = 0,
			localClippingEnabled = false,
			renderingShadows = false;

		const plane = new Plane(),
			viewNormalMatrix = new Matrix3(),

			uniform = { value: null, needsUpdate: false };

		this.uniform = uniform;
		this.numPlanes = 0;
		this.numIntersection = 0;

		this.init = function ( planes, enableLocalClipping, camera ) {

			const enabled =
				planes.length !== 0 ||
				enableLocalClipping ||
				// enable state of previous frame - the clipping code has to
				// run another frame in order to reset the state:
				numGlobalPlanes !== 0 ||
				localClippingEnabled;

			localClippingEnabled = enableLocalClipping;

			globalState = projectPlanes( planes, camera, 0 );
			numGlobalPlanes = planes.length;

			return enabled;

		};

		this.beginShadows = function () {

			renderingShadows = true;
			projectPlanes( null );

		};

		this.endShadows = function () {

			renderingShadows = false;
			resetGlobalState();

		};

		this.setState = function ( material, camera, useCache ) {

			const planes = material.clippingPlanes,
				clipIntersection = material.clipIntersection,
				clipShadows = material.clipShadows;

			const materialProperties = properties.get( material );

			if ( ! localClippingEnabled || planes === null || planes.length === 0 || renderingShadows && ! clipShadows ) {

				// there's no local clipping

				if ( renderingShadows ) {

					// there's no global clipping

					projectPlanes( null );

				} else {

					resetGlobalState();

				}

			} else {

				const nGlobal = renderingShadows ? 0 : numGlobalPlanes,
					lGlobal = nGlobal * 4;

				let dstArray = materialProperties.clippingState || null;

				uniform.value = dstArray; // ensure unique state

				dstArray = projectPlanes( planes, camera, lGlobal, useCache );

				for ( let i = 0; i !== lGlobal; ++ i ) {

					dstArray[ i ] = globalState[ i ];

				}

				materialProperties.clippingState = dstArray;
				this.numIntersection = clipIntersection ? this.numPlanes : 0;
				this.numPlanes += nGlobal;

			}


		};

		function resetGlobalState() {

			if ( uniform.value !== globalState ) {

				uniform.value = globalState;
				uniform.needsUpdate = numGlobalPlanes > 0;

			}

			scope.numPlanes = numGlobalPlanes;
			scope.numIntersection = 0;

		}

		function projectPlanes( planes, camera, dstOffset, skipTransform ) {

			const nPlanes = planes !== null ? planes.length : 0;
			let dstArray = null;

			if ( nPlanes !== 0 ) {

				dstArray = uniform.value;

				if ( skipTransform !== true || dstArray === null ) {

					const flatSize = dstOffset + nPlanes * 4,
						viewMatrix = camera.matrixWorldInverse;

					viewNormalMatrix.getNormalMatrix( viewMatrix );

					if ( dstArray === null || dstArray.length < flatSize ) {

						dstArray = new Float32Array( flatSize );

					}

					for ( let i = 0, i4 = dstOffset; i !== nPlanes; ++ i, i4 += 4 ) {

						plane.copy( planes[ i ] ).applyMatrix4( viewMatrix, viewNormalMatrix );

						plane.normal.toArray( dstArray, i4 );
						dstArray[ i4 + 3 ] = plane.constant;

					}

				}

				uniform.value = dstArray;
				uniform.needsUpdate = true;

			}

			scope.numPlanes = nPlanes;
			scope.numIntersection = 0;

			return dstArray;

		}

	}

	function WebGLCubeMaps( renderer ) {

		let cubemaps = new WeakMap();

		function mapTextureMapping( texture, mapping ) {

			if ( mapping === EquirectangularReflectionMapping ) {

				texture.mapping = CubeReflectionMapping;

			} else if ( mapping === EquirectangularRefractionMapping ) {

				texture.mapping = CubeRefractionMapping;

			}

			return texture;

		}

		function get( texture ) {

			if ( texture && texture.isTexture ) {

				const mapping = texture.mapping;

				if ( mapping === EquirectangularReflectionMapping || mapping === EquirectangularRefractionMapping ) {

					if ( cubemaps.has( texture ) ) {

						const cubemap = cubemaps.get( texture ).texture;
						return mapTextureMapping( cubemap, texture.mapping );

					} else {

						const image = texture.image;

						if ( image && image.height > 0 ) {

							const currentRenderList = renderer.getRenderList();
							const currentRenderTarget = renderer.getRenderTarget();

							const renderTarget = new WebGLCubeRenderTarget( image.height / 2 );
							renderTarget.fromEquirectangularTexture( renderer, texture );
							cubemaps.set( texture, renderTarget );

							renderer.setRenderTarget( currentRenderTarget );
							renderer.setRenderList( currentRenderList );

							texture.addEventListener( 'dispose', onTextureDispose );

							return mapTextureMapping( renderTarget.texture, texture.mapping );

						} else {

							// image not yet ready. try the conversion next frame

							return null;

						}

					}

				}

			}

			return texture;

		}

		function onTextureDispose( event ) {

			const texture = event.target;

			texture.removeEventListener( 'dispose', onTextureDispose );

			const cubemap = cubemaps.get( texture );

			if ( cubemap !== undefined ) {

				cubemaps.delete( texture );
				cubemap.dispose();

			}

		}

		function dispose() {

			cubemaps = new WeakMap();

		}

		return {
			get: get,
			dispose: dispose
		};

	}

	function WebGLExtensions( gl ) {

		const extensions = {};

		return {

			has: function ( name ) {

				if ( extensions[ name ] !== undefined ) {

					return extensions[ name ] !== null;

				}

				let extension;

				switch ( name ) {

					case 'WEBGL_depth_texture':
						extension = gl.getExtension( 'WEBGL_depth_texture' ) || gl.getExtension( 'MOZ_WEBGL_depth_texture' ) || gl.getExtension( 'WEBKIT_WEBGL_depth_texture' );
						break;

					case 'EXT_texture_filter_anisotropic':
						extension = gl.getExtension( 'EXT_texture_filter_anisotropic' ) || gl.getExtension( 'MOZ_EXT_texture_filter_anisotropic' ) || gl.getExtension( 'WEBKIT_EXT_texture_filter_anisotropic' );
						break;

					case 'WEBGL_compressed_texture_s3tc':
						extension = gl.getExtension( 'WEBGL_compressed_texture_s3tc' ) || gl.getExtension( 'MOZ_WEBGL_compressed_texture_s3tc' ) || gl.getExtension( 'WEBKIT_WEBGL_compressed_texture_s3tc' );
						break;

					case 'WEBGL_compressed_texture_pvrtc':
						extension = gl.getExtension( 'WEBGL_compressed_texture_pvrtc' ) || gl.getExtension( 'WEBKIT_WEBGL_compressed_texture_pvrtc' );
						break;

					default:
						extension = gl.getExtension( name );

				}

				extensions[ name ] = extension;

				return extension !== null;

			},

			get: function ( name ) {

				if ( ! this.has( name ) ) {

					console.warn( 'THREE.WebGLRenderer: ' + name + ' extension not supported.' );

				}

				return extensions[ name ];

			}

		};

	}

	function WebGLGeometries( gl, attributes, info, bindingStates ) {

		const geometries = new WeakMap();
		const wireframeAttributes = new WeakMap();

		function onGeometryDispose( event ) {

			const geometry = event.target;
			const buffergeometry = geometries.get( geometry );

			if ( buffergeometry.index !== null ) {

				attributes.remove( buffergeometry.index );

			}

			for ( const name in buffergeometry.attributes ) {

				attributes.remove( buffergeometry.attributes[ name ] );

			}

			geometry.removeEventListener( 'dispose', onGeometryDispose );

			geometries.delete( geometry );

			const attribute = wireframeAttributes.get( buffergeometry );

			if ( attribute ) {

				attributes.remove( attribute );
				wireframeAttributes.delete( buffergeometry );

			}

			bindingStates.releaseStatesOfGeometry( buffergeometry );

			if ( geometry.isInstancedBufferGeometry === true ) {

				delete geometry._maxInstanceCount;

			}

			//

			info.memory.geometries --;

		}

		function get( object, geometry ) {

			let buffergeometry = geometries.get( geometry );

			if ( buffergeometry ) return buffergeometry;

			geometry.addEventListener( 'dispose', onGeometryDispose );

			if ( geometry.isBufferGeometry ) {

				buffergeometry = geometry;

			} else if ( geometry.isGeometry ) {

				if ( geometry._bufferGeometry === undefined ) {

					geometry._bufferGeometry = new BufferGeometry().setFromObject( object );

				}

				buffergeometry = geometry._bufferGeometry;

			}

			geometries.set( geometry, buffergeometry );

			info.memory.geometries ++;

			return buffergeometry;

		}

		function update( geometry ) {

			const geometryAttributes = geometry.attributes;

			// Updating index buffer in VAO now. See WebGLBindingStates.

			for ( const name in geometryAttributes ) {

				attributes.update( geometryAttributes[ name ], 34962 );

			}

			// morph targets

			const morphAttributes = geometry.morphAttributes;

			for ( const name in morphAttributes ) {

				const array = morphAttributes[ name ];

				for ( let i = 0, l = array.length; i < l; i ++ ) {

					attributes.update( array[ i ], 34962 );

				}

			}

		}

		function updateWireframeAttribute( geometry ) {

			const indices = [];

			const geometryIndex = geometry.index;
			const geometryPosition = geometry.attributes.position;
			let version = 0;

			if ( geometryIndex !== null ) {

				const array = geometryIndex.array;
				version = geometryIndex.version;

				for ( let i = 0, l = array.length; i < l; i += 3 ) {

					const a = array[ i + 0 ];
					const b = array[ i + 1 ];
					const c = array[ i + 2 ];

					indices.push( a, b, b, c, c, a );

				}

			} else {

				const array = geometryPosition.array;
				version = geometryPosition.version;

				for ( let i = 0, l = ( array.length / 3 ) - 1; i < l; i += 3 ) {

					const a = i + 0;
					const b = i + 1;
					const c = i + 2;

					indices.push( a, b, b, c, c, a );

				}

			}

			const attribute = new ( arrayMax( indices ) > 65535 ? Uint32BufferAttribute : Uint16BufferAttribute )( indices, 1 );
			attribute.version = version;

			// Updating index buffer in VAO now. See WebGLBindingStates

			//

			const previousAttribute = wireframeAttributes.get( geometry );

			if ( previousAttribute ) attributes.remove( previousAttribute );

			//

			wireframeAttributes.set( geometry, attribute );

		}

		function getWireframeAttribute( geometry ) {

			const currentAttribute = wireframeAttributes.get( geometry );

			if ( currentAttribute ) {

				const geometryIndex = geometry.index;

				if ( geometryIndex !== null ) {

					// if the attribute is obsolete, create a new one

					if ( currentAttribute.version < geometryIndex.version ) {

						updateWireframeAttribute( geometry );

					}

				}

			} else {

				updateWireframeAttribute( geometry );

			}

			return wireframeAttributes.get( geometry );

		}

		return {

			get: get,
			update: update,

			getWireframeAttribute: getWireframeAttribute

		};

	}

	function WebGLIndexedBufferRenderer( gl, extensions, info, capabilities ) {

		const isWebGL2 = capabilities.isWebGL2;

		let mode;

		function setMode( value ) {

			mode = value;

		}

		let type, bytesPerElement;

		function setIndex( value ) {

			type = value.type;
			bytesPerElement = value.bytesPerElement;

		}

		function render( start, count ) {

			gl.drawElements( mode, count, type, start * bytesPerElement );

			info.update( count, mode, 1 );

		}

		function renderInstances( start, count, primcount ) {

			if ( primcount === 0 ) return;

			let extension, methodName;

			if ( isWebGL2 ) {

				extension = gl;
				methodName = 'drawElementsInstanced';

			} else {

				extension = extensions.get( 'ANGLE_instanced_arrays' );
				methodName = 'drawElementsInstancedANGLE';

				if ( extension === null ) {

					console.error( 'THREE.WebGLIndexedBufferRenderer: using THREE.InstancedBufferGeometry but hardware does not support extension ANGLE_instanced_arrays.' );
					return;

				}

			}

			extension[ methodName ]( mode, count, type, start * bytesPerElement, primcount );

			info.update( count, mode, primcount );

		}

		//

		this.setMode = setMode;
		this.setIndex = setIndex;
		this.render = render;
		this.renderInstances = renderInstances;

	}

	function WebGLInfo( gl ) {

		const memory = {
			geometries: 0,
			textures: 0
		};

		const render = {
			frame: 0,
			calls: 0,
			triangles: 0,
			points: 0,
			lines: 0
		};

		function update( count, mode, instanceCount ) {

			render.calls ++;

			switch ( mode ) {

				case 4:
					render.triangles += instanceCount * ( count / 3 );
					break;

				case 1:
					render.lines += instanceCount * ( count / 2 );
					break;

				case 3:
					render.lines += instanceCount * ( count - 1 );
					break;

				case 2:
					render.lines += instanceCount * count;
					break;

				case 0:
					render.points += instanceCount * count;
					break;

				default:
					console.error( 'THREE.WebGLInfo: Unknown draw mode:', mode );
					break;

			}

		}

		function reset() {

			render.frame ++;
			render.calls = 0;
			render.triangles = 0;
			render.points = 0;
			render.lines = 0;

		}

		return {
			memory: memory,
			render: render,
			programs: null,
			autoReset: true,
			reset: reset,
			update: update
		};

	}

	function numericalSort( a, b ) {

		return a[ 0 ] - b[ 0 ];

	}

	function absNumericalSort( a, b ) {

		return Math.abs( b[ 1 ] ) - Math.abs( a[ 1 ] );

	}

	function WebGLMorphtargets( gl ) {

		const influencesList = {};
		const morphInfluences = new Float32Array( 8 );

		const workInfluences = [];

		for ( let i = 0; i < 8; i ++ ) {

			workInfluences[ i ] = [ i, 0 ];

		}

		function update( object, geometry, material, program ) {

			const objectInfluences = object.morphTargetInfluences;

			// When object doesn't have morph target influences defined, we treat it as a 0-length array
			// This is important to make sure we set up morphTargetBaseInfluence / morphTargetInfluences

			const length = objectInfluences === undefined ? 0 : objectInfluences.length;

			let influences = influencesList[ geometry.id ];

			if ( influences === undefined ) {

				// initialise list

				influences = [];

				for ( let i = 0; i < length; i ++ ) {

					influences[ i ] = [ i, 0 ];

				}

				influencesList[ geometry.id ] = influences;

			}

			// Collect influences

			for ( let i = 0; i < length; i ++ ) {

				const influence = influences[ i ];

				influence[ 0 ] = i;
				influence[ 1 ] = objectInfluences[ i ];

			}

			influences.sort( absNumericalSort );

			for ( let i = 0; i < 8; i ++ ) {

				if ( i < length && influences[ i ][ 1 ] ) {

					workInfluences[ i ][ 0 ] = influences[ i ][ 0 ];
					workInfluences[ i ][ 1 ] = influences[ i ][ 1 ];

				} else {

					workInfluences[ i ][ 0 ] = Number.MAX_SAFE_INTEGER;
					workInfluences[ i ][ 1 ] = 0;

				}

			}

			workInfluences.sort( numericalSort );

			const morphTargets = material.morphTargets && geometry.morphAttributes.position;
			const morphNormals = material.morphNormals && geometry.morphAttributes.normal;

			let morphInfluencesSum = 0;

			for ( let i = 0; i < 8; i ++ ) {

				const influence = workInfluences[ i ];
				const index = influence[ 0 ];
				const value = influence[ 1 ];

				if ( index !== Number.MAX_SAFE_INTEGER && value ) {

					if ( morphTargets && geometry.getAttribute( 'morphTarget' + i ) !== morphTargets[ index ] ) {

						geometry.setAttribute( 'morphTarget' + i, morphTargets[ index ] );

					}

					if ( morphNormals && geometry.getAttribute( 'morphNormal' + i ) !== morphNormals[ index ] ) {

						geometry.setAttribute( 'morphNormal' + i, morphNormals[ index ] );

					}

					morphInfluences[ i ] = value;
					morphInfluencesSum += value;

				} else {

					if ( morphTargets && geometry.hasAttribute( 'morphTarget' + i ) === true ) {

						geometry.deleteAttribute( 'morphTarget' + i );

					}

					if ( morphNormals && geometry.hasAttribute( 'morphNormal' + i ) === true ) {

						geometry.deleteAttribute( 'morphNormal' + i );

					}

					morphInfluences[ i ] = 0;

				}

			}

			// GLSL shader uses formula baseinfluence * base + sum(target * influence)
			// This allows us to switch between absolute morphs and relative morphs without changing shader code
			// When baseinfluence = 1 - sum(influence), the above is equivalent to sum((target - base) * influence)
			const morphBaseInfluence = geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;

			program.getUniforms().setValue( gl, 'morphTargetBaseInfluence', morphBaseInfluence );
			program.getUniforms().setValue( gl, 'morphTargetInfluences', morphInfluences );

		}

		return {

			update: update

		};

	}

	function WebGLObjects( gl, geometries, attributes, info ) {

		let updateMap = new WeakMap();

		function update( object ) {

			const frame = info.render.frame;

			const geometry = object.geometry;
			const buffergeometry = geometries.get( object, geometry );

			// Update once per frame

			if ( updateMap.get( buffergeometry ) !== frame ) {

				if ( geometry.isGeometry ) {

					buffergeometry.updateFromObject( object );

				}

				geometries.update( buffergeometry );

				updateMap.set( buffergeometry, frame );

			}

			if ( object.isInstancedMesh ) {

				if ( object.hasEventListener( 'dispose', onInstancedMeshDispose ) === false ) {

					object.addEventListener( 'dispose', onInstancedMeshDispose );

				}

				attributes.update( object.instanceMatrix, 34962 );

				if ( object.instanceColor !== null ) {

					attributes.update( object.instanceColor, 34962 );

				}

			}

			return buffergeometry;

		}

		function dispose() {

			updateMap = new WeakMap();

		}

		function onInstancedMeshDispose( event ) {

			const instancedMesh = event.target;

			instancedMesh.removeEventListener( 'dispose', onInstancedMeshDispose );

			attributes.remove( instancedMesh.instanceMatrix );

			if ( instancedMesh.instanceColor !== null ) attributes.remove( instancedMesh.instanceColor );

		}

		return {

			update: update,
			dispose: dispose

		};

	}

	function DataTexture2DArray( data = null, width = 1, height = 1, depth = 1 ) {

		Texture.call( this, null );

		this.image = { data, width, height, depth };

		this.magFilter = NearestFilter;
		this.minFilter = NearestFilter;

		this.wrapR = ClampToEdgeWrapping;

		this.generateMipmaps = false;
		this.flipY = false;

		this.needsUpdate = true;

	}

	DataTexture2DArray.prototype = Object.create( Texture.prototype );
	DataTexture2DArray.prototype.constructor = DataTexture2DArray;
	DataTexture2DArray.prototype.isDataTexture2DArray = true;

	function DataTexture3D( data = null, width = 1, height = 1, depth = 1 ) {

		// We're going to add .setXXX() methods for setting properties later.
		// Users can still set in DataTexture3D directly.
		//
		//	const texture = new THREE.DataTexture3D( data, width, height, depth );
		// 	texture.anisotropy = 16;
		//
		// See #14839

		Texture.call( this, null );

		this.image = { data, width, height, depth };

		this.magFilter = NearestFilter;
		this.minFilter = NearestFilter;

		this.wrapR = ClampToEdgeWrapping;

		this.generateMipmaps = false;
		this.flipY = false;

		this.needsUpdate = true;


	}

	DataTexture3D.prototype = Object.create( Texture.prototype );
	DataTexture3D.prototype.constructor = DataTexture3D;
	DataTexture3D.prototype.isDataTexture3D = true;

	/**
	 * Uniforms of a program.
	 * Those form a tree structure with a special top-level container for the root,
	 * which you get by calling 'new WebGLUniforms( gl, program )'.
	 *
	 *
	 * Properties of inner nodes including the top-level container:
	 *
	 * .seq - array of nested uniforms
	 * .map - nested uniforms by name
	 *
	 *
	 * Methods of all nodes except the top-level container:
	 *
	 * .setValue( gl, value, [textures] )
	 *
	 * 		uploads a uniform value(s)
	 *  	the 'textures' parameter is needed for sampler uniforms
	 *
	 *
	 * Static methods of the top-level container (textures factorizations):
	 *
	 * .upload( gl, seq, values, textures )
	 *
	 * 		sets uniforms in 'seq' to 'values[id].value'
	 *
	 * .seqWithValue( seq, values ) : filteredSeq
	 *
	 * 		filters 'seq' entries with corresponding entry in values
	 *
	 *
	 * Methods of the top-level container (textures factorizations):
	 *
	 * .setValue( gl, name, value, textures )
	 *
	 * 		sets uniform with  name 'name' to 'value'
	 *
	 * .setOptional( gl, obj, prop )
	 *
	 * 		like .set for an optional property of the object
	 *
	 */

	const emptyTexture = new Texture();
	const emptyTexture2dArray = new DataTexture2DArray();
	const emptyTexture3d = new DataTexture3D();
	const emptyCubeTexture = new CubeTexture();

	// --- Utilities ---

	// Array Caches (provide typed arrays for temporary by size)

	const arrayCacheF32 = [];
	const arrayCacheI32 = [];

	// Float32Array caches used for uploading Matrix uniforms

	const mat4array = new Float32Array( 16 );
	const mat3array = new Float32Array( 9 );
	const mat2array = new Float32Array( 4 );

	// Flattening for arrays of vectors and matrices

	function flatten( array, nBlocks, blockSize ) {

		const firstElem = array[ 0 ];

		if ( firstElem <= 0 || firstElem > 0 ) return array;
		// unoptimized: ! isNaN( firstElem )
		// see http://jacksondunstan.com/articles/983

		const n = nBlocks * blockSize;
		let r = arrayCacheF32[ n ];

		if ( r === undefined ) {

			r = new Float32Array( n );
			arrayCacheF32[ n ] = r;

		}

		if ( nBlocks !== 0 ) {

			firstElem.toArray( r, 0 );

			for ( let i = 1, offset = 0; i !== nBlocks; ++ i ) {

				offset += blockSize;
				array[ i ].toArray( r, offset );

			}

		}

		return r;

	}

	function arraysEqual( a, b ) {

		if ( a.length !== b.length ) return false;

		for ( let i = 0, l = a.length; i < l; i ++ ) {

			if ( a[ i ] !== b[ i ] ) return false;

		}

		return true;

	}

	function copyArray( a, b ) {

		for ( let i = 0, l = b.length; i < l; i ++ ) {

			a[ i ] = b[ i ];

		}

	}

	// Texture unit allocation

	function allocTexUnits( textures, n ) {

		let r = arrayCacheI32[ n ];

		if ( r === undefined ) {

			r = new Int32Array( n );
			arrayCacheI32[ n ] = r;

		}

		for ( let i = 0; i !== n; ++ i ) {

			r[ i ] = textures.allocateTextureUnit();

		}

		return r;

	}

	// --- Setters ---

	// Note: Defining these methods externally, because they come in a bunch
	// and this way their names minify.

	// Single scalar

	function setValueV1f( gl, v ) {

		const cache = this.cache;

		if ( cache[ 0 ] === v ) return;

		gl.uniform1f( this.addr, v );

		cache[ 0 ] = v;

	}

	// Single float vector (from flat array or THREE.VectorN)

	function setValueV2f( gl, v ) {

		const cache = this.cache;

		if ( v.x !== undefined ) {

			if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y ) {

				gl.uniform2f( this.addr, v.x, v.y );

				cache[ 0 ] = v.x;
				cache[ 1 ] = v.y;

			}

		} else {

			if ( arraysEqual( cache, v ) ) return;

			gl.uniform2fv( this.addr, v );

			copyArray( cache, v );

		}

	}

	function setValueV3f( gl, v ) {

		const cache = this.cache;

		if ( v.x !== undefined ) {

			if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z ) {

				gl.uniform3f( this.addr, v.x, v.y, v.z );

				cache[ 0 ] = v.x;
				cache[ 1 ] = v.y;
				cache[ 2 ] = v.z;

			}

		} else if ( v.r !== undefined ) {

			if ( cache[ 0 ] !== v.r || cache[ 1 ] !== v.g || cache[ 2 ] !== v.b ) {

				gl.uniform3f( this.addr, v.r, v.g, v.b );

				cache[ 0 ] = v.r;
				cache[ 1 ] = v.g;
				cache[ 2 ] = v.b;

			}

		} else {

			if ( arraysEqual( cache, v ) ) return;

			gl.uniform3fv( this.addr, v );

			copyArray( cache, v );

		}

	}

	function setValueV4f( gl, v ) {

		const cache = this.cache;

		if ( v.x !== undefined ) {

			if ( cache[ 0 ] !== v.x || cache[ 1 ] !== v.y || cache[ 2 ] !== v.z || cache[ 3 ] !== v.w ) {

				gl.uniform4f( this.addr, v.x, v.y, v.z, v.w );

				cache[ 0 ] = v.x;
				cache[ 1 ] = v.y;
				cache[ 2 ] = v.z;
				cache[ 3 ] = v.w;

			}

		} else {

			if ( arraysEqual( cache, v ) ) return;

			gl.uniform4fv( this.addr, v );

			copyArray( cache, v );

		}

	}

	// Single matrix (from flat array or MatrixN)

	function setValueM2( gl, v ) {

		const cache = this.cache;
		const elements = v.elements;

		if ( elements === undefined ) {

			if ( arraysEqual( cache, v ) ) return;

			gl.uniformMatrix2fv( this.addr, false, v );

			copyArray( cache, v );

		} else {

			if ( arraysEqual( cache, elements ) ) return;

			mat2array.set( elements );

			gl.uniformMatrix2fv( this.addr, false, mat2array );

			copyArray( cache, elements );

		}

	}

	function setValueM3( gl, v ) {

		const cache = this.cache;
		const elements = v.elements;

		if ( elements === undefined ) {

			if ( arraysEqual( cache, v ) ) return;

			gl.uniformMatrix3fv( this.addr, false, v );

			copyArray( cache, v );

		} else {

			if ( arraysEqual( cache, elements ) ) return;

			mat3array.set( elements );

			gl.uniformMatrix3fv( this.addr, false, mat3array );

			copyArray( cache, elements );

		}

	}

	function setValueM4( gl, v ) {

		const cache = this.cache;
		const elements = v.elements;

		if ( elements === undefined ) {

			if ( arraysEqual( cache, v ) ) return;

			gl.uniformMatrix4fv( this.addr, false, v );

			copyArray( cache, v );

		} else {

			if ( arraysEqual( cache, elements ) ) return;

			mat4array.set( elements );

			gl.uniformMatrix4fv( this.addr, false, mat4array );

			copyArray( cache, elements );

		}

	}

	// Single texture (2D / Cube)

	function setValueT1( gl, v, textures ) {

		const cache = this.cache;
		const unit = textures.allocateTextureUnit();

		if ( cache[ 0 ] !== unit ) {

			gl.uniform1i( this.addr, unit );
			cache[ 0 ] = unit;

		}

		textures.safeSetTexture2D( v || emptyTexture, unit );

	}

	function setValueT2DArray1( gl, v, textures ) {

		const cache = this.cache;
		const unit = textures.allocateTextureUnit();

		if ( cache[ 0 ] !== unit ) {

			gl.uniform1i( this.addr, unit );
			cache[ 0 ] = unit;

		}

		textures.setTexture2DArray( v || emptyTexture2dArray, unit );

	}

	function setValueT3D1( gl, v, textures ) {

		const cache = this.cache;
		const unit = textures.allocateTextureUnit();

		if ( cache[ 0 ] !== unit ) {

			gl.uniform1i( this.addr, unit );
			cache[ 0 ] = unit;

		}

		textures.setTexture3D( v || emptyTexture3d, unit );

	}

	function setValueT6( gl, v, textures ) {

		const cache = this.cache;
		const unit = textures.allocateTextureUnit();

		if ( cache[ 0 ] !== unit ) {

			gl.uniform1i( this.addr, unit );
			cache[ 0 ] = unit;

		}

		textures.safeSetTextureCube( v || emptyCubeTexture, unit );

	}

	// Integer / Boolean vectors or arrays thereof (always flat arrays)

	function setValueV1i( gl, v ) {

		const cache = this.cache;

		if ( cache[ 0 ] === v ) return;

		gl.uniform1i( this.addr, v );

		cache[ 0 ] = v;

	}

	function setValueV2i( gl, v ) {

		const cache = this.cache;

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform2iv( this.addr, v );

		copyArray( cache, v );

	}

	function setValueV3i( gl, v ) {

		const cache = this.cache;

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform3iv( this.addr, v );

		copyArray( cache, v );

	}

	function setValueV4i( gl, v ) {

		const cache = this.cache;

		if ( arraysEqual( cache, v ) ) return;

		gl.uniform4iv( this.addr, v );

		copyArray( cache, v );

	}

	// uint

	function setValueV1ui( gl, v ) {

		const cache = this.cache;

		if ( cache[ 0 ] === v ) return;

		gl.uniform1ui( this.addr, v );

		cache[ 0 ] = v;

	}

	// Helper to pick the right setter for the singular case

	function getSingularSetter( type ) {

		switch ( type ) {

			case 0x1406: return setValueV1f; // FLOAT
			case 0x8b50: return setValueV2f; // _VEC2
			case 0x8b51: return setValueV3f; // _VEC3
			case 0x8b52: return setValueV4f; // _VEC4

			case 0x8b5a: return setValueM2; // _MAT2
			case 0x8b5b: return setValueM3; // _MAT3
			case 0x8b5c: return setValueM4; // _MAT4

			case 0x1404: case 0x8b56: return setValueV1i; // INT, BOOL
			case 0x8b53: case 0x8b57: return setValueV2i; // _VEC2
			case 0x8b54: case 0x8b58: return setValueV3i; // _VEC3
			case 0x8b55: case 0x8b59: return setValueV4i; // _VEC4

			case 0x1405: return setValueV1ui; // UINT

			case 0x8b5e: // SAMPLER_2D
			case 0x8d66: // SAMPLER_EXTERNAL_OES
			case 0x8dca: // INT_SAMPLER_2D
			case 0x8dd2: // UNSIGNED_INT_SAMPLER_2D
			case 0x8b62: // SAMPLER_2D_SHADOW
				return setValueT1;

			case 0x8b5f: // SAMPLER_3D
			case 0x8dcb: // INT_SAMPLER_3D
			case 0x8dd3: // UNSIGNED_INT_SAMPLER_3D
				return setValueT3D1;

			case 0x8b60: // SAMPLER_CUBE
			case 0x8dcc: // INT_SAMPLER_CUBE
			case 0x8dd4: // UNSIGNED_INT_SAMPLER_CUBE
			case 0x8dc5: // SAMPLER_CUBE_SHADOW
				return setValueT6;

			case 0x8dc1: // SAMPLER_2D_ARRAY
			case 0x8dcf: // INT_SAMPLER_2D_ARRAY
			case 0x8dd7: // UNSIGNED_INT_SAMPLER_2D_ARRAY
			case 0x8dc4: // SAMPLER_2D_ARRAY_SHADOW
				return setValueT2DArray1;

		}

	}

	// Array of scalars
	function setValueV1fArray( gl, v ) {

		gl.uniform1fv( this.addr, v );

	}

	// Integer / Boolean vectors or arrays thereof (always flat arrays)
	function setValueV1iArray( gl, v ) {

		gl.uniform1iv( this.addr, v );

	}

	function setValueV2iArray( gl, v ) {

		gl.uniform2iv( this.addr, v );

	}

	function setValueV3iArray( gl, v ) {

		gl.uniform3iv( this.addr, v );

	}

	function setValueV4iArray( gl, v ) {

		gl.uniform4iv( this.addr, v );

	}


	// Array of vectors (flat or from THREE classes)

	function setValueV2fArray( gl, v ) {

		const data = flatten( v, this.size, 2 );

		gl.uniform2fv( this.addr, data );

	}

	function setValueV3fArray( gl, v ) {

		const data = flatten( v, this.size, 3 );

		gl.uniform3fv( this.addr, data );

	}

	function setValueV4fArray( gl, v ) {

		const data = flatten( v, this.size, 4 );

		gl.uniform4fv( this.addr, data );

	}

	// Array of matrices (flat or from THREE clases)

	function setValueM2Array( gl, v ) {

		const data = flatten( v, this.size, 4 );

		gl.uniformMatrix2fv( this.addr, false, data );

	}

	function setValueM3Array( gl, v ) {

		const data = flatten( v, this.size, 9 );

		gl.uniformMatrix3fv( this.addr, false, data );

	}

	function setValueM4Array( gl, v ) {

		const data = flatten( v, this.size, 16 );

		gl.uniformMatrix4fv( this.addr, false, data );

	}

	// Array of textures (2D / Cube)

	function setValueT1Array( gl, v, textures ) {

		const n = v.length;

		const units = allocTexUnits( textures, n );

		gl.uniform1iv( this.addr, units );

		for ( let i = 0; i !== n; ++ i ) {

			textures.safeSetTexture2D( v[ i ] || emptyTexture, units[ i ] );

		}

	}

	function setValueT6Array( gl, v, textures ) {

		const n = v.length;

		const units = allocTexUnits( textures, n );

		gl.uniform1iv( this.addr, units );

		for ( let i = 0; i !== n; ++ i ) {

			textures.safeSetTextureCube( v[ i ] || emptyCubeTexture, units[ i ] );

		}

	}

	// Helper to pick the right setter for a pure (bottom-level) array

	function getPureArraySetter( type ) {

		switch ( type ) {

			case 0x1406: return setValueV1fArray; // FLOAT
			case 0x8b50: return setValueV2fArray; // _VEC2
			case 0x8b51: return setValueV3fArray; // _VEC3
			case 0x8b52: return setValueV4fArray; // _VEC4

			case 0x8b5a: return setValueM2Array; // _MAT2
			case 0x8b5b: return setValueM3Array; // _MAT3
			case 0x8b5c: return setValueM4Array; // _MAT4

			case 0x1404: case 0x8b56: return setValueV1iArray; // INT, BOOL
			case 0x8b53: case 0x8b57: return setValueV2iArray; // _VEC2
			case 0x8b54: case 0x8b58: return setValueV3iArray; // _VEC3
			case 0x8b55: case 0x8b59: return setValueV4iArray; // _VEC4

			case 0x8b5e: // SAMPLER_2D
			case 0x8d66: // SAMPLER_EXTERNAL_OES
			case 0x8dca: // INT_SAMPLER_2D
			case 0x8dd2: // UNSIGNED_INT_SAMPLER_2D
			case 0x8b62: // SAMPLER_2D_SHADOW
				return setValueT1Array;

			case 0x8b60: // SAMPLER_CUBE
			case 0x8dcc: // INT_SAMPLER_CUBE
			case 0x8dd4: // UNSIGNED_INT_SAMPLER_CUBE
			case 0x8dc5: // SAMPLER_CUBE_SHADOW
				return setValueT6Array;

		}

	}

	// --- Uniform Classes ---

	function SingleUniform( id, activeInfo, addr ) {

		this.id = id;
		this.addr = addr;
		this.cache = [];
		this.setValue = getSingularSetter( activeInfo.type );

		// this.path = activeInfo.name; // DEBUG

	}

	function PureArrayUniform( id, activeInfo, addr ) {

		this.id = id;
		this.addr = addr;
		this.cache = [];
		this.size = activeInfo.size;
		this.setValue = getPureArraySetter( activeInfo.type );

		// this.path = activeInfo.name; // DEBUG

	}

	PureArrayUniform.prototype.updateCache = function ( data ) {

		const cache = this.cache;

		if ( data instanceof Float32Array && cache.length !== data.length ) {

			this.cache = new Float32Array( data.length );

		}

		copyArray( cache, data );

	};

	function StructuredUniform( id ) {

		this.id = id;

		this.seq = [];
		this.map = {};

	}

	StructuredUniform.prototype.setValue = function ( gl, value, textures ) {

		const seq = this.seq;

		for ( let i = 0, n = seq.length; i !== n; ++ i ) {

			const u = seq[ i ];
			u.setValue( gl, value[ u.id ], textures );

		}

	};

	// --- Top-level ---

	// Parser - builds up the property tree from the path strings

	const RePathPart = /(\w+)(\])?(\[|\.)?/g;

	// extracts
	// 	- the identifier (member name or array index)
	//  - followed by an optional right bracket (found when array index)
	//  - followed by an optional left bracket or dot (type of subscript)
	//
	// Note: These portions can be read in a non-overlapping fashion and
	// allow straightforward parsing of the hierarchy that WebGL encodes
	// in the uniform names.

	function addUniform( container, uniformObject ) {

		container.seq.push( uniformObject );
		container.map[ uniformObject.id ] = uniformObject;

	}

	function parseUniform( activeInfo, addr, container ) {

		const path = activeInfo.name,
			pathLength = path.length;

		// reset RegExp object, because of the early exit of a previous run
		RePathPart.lastIndex = 0;

		while ( true ) {

			const match = RePathPart.exec( path ),
				matchEnd = RePathPart.lastIndex;

			let id = match[ 1 ];
			const idIsIndex = match[ 2 ] === ']',
				subscript = match[ 3 ];

			if ( idIsIndex ) id = id | 0; // convert to integer

			if ( subscript === undefined || subscript === '[' && matchEnd + 2 === pathLength ) {

				// bare name or "pure" bottom-level array "[0]" suffix

				addUniform( container, subscript === undefined ?
					new SingleUniform( id, activeInfo, addr ) :
					new PureArrayUniform( id, activeInfo, addr ) );

				break;

			} else {

				// step into inner node / create it in case it doesn't exist

				const map = container.map;
				let next = map[ id ];

				if ( next === undefined ) {

					next = new StructuredUniform( id );
					addUniform( container, next );

				}

				container = next;

			}

		}

	}

	// Root Container

	function WebGLUniforms( gl, program ) {

		this.seq = [];
		this.map = {};

		const n = gl.getProgramParameter( program, 35718 );

		for ( let i = 0; i < n; ++ i ) {

			const info = gl.getActiveUniform( program, i ),
				addr = gl.getUniformLocation( program, info.name );

			parseUniform( info, addr, this );

		}

	}

	WebGLUniforms.prototype.setValue = function ( gl, name, value, textures ) {

		const u = this.map[ name ];

		if ( u !== undefined ) u.setValue( gl, value, textures );

	};

	WebGLUniforms.prototype.setOptional = function ( gl, object, name ) {

		const v = object[ name ];

		if ( v !== undefined ) this.setValue( gl, name, v );

	};


	// Static interface

	WebGLUniforms.upload = function ( gl, seq, values, textures ) {

		for ( let i = 0, n = seq.length; i !== n; ++ i ) {

			const u = seq[ i ],
				v = values[ u.id ];

			if ( v.needsUpdate !== false ) {

				// note: always updating when .needsUpdate is undefined
				u.setValue( gl, v.value, textures );

			}

		}

	};

	WebGLUniforms.seqWithValue = function ( seq, values ) {

		const r = [];

		for ( let i = 0, n = seq.length; i !== n; ++ i ) {

			const u = seq[ i ];
			if ( u.id in values ) r.push( u );

		}

		return r;

	};

	function WebGLShader( gl, type, string ) {

		const shader = gl.createShader( type );

		gl.shaderSource( shader, string );
		gl.compileShader( shader );

		return shader;

	}

	let programIdCount = 0;

	function addLineNumbers( string ) {

		const lines = string.split( '\n' );

		for ( let i = 0; i < lines.length; i ++ ) {

			lines[ i ] = ( i + 1 ) + ': ' + lines[ i ];

		}

		return lines.join( '\n' );

	}

	function getEncodingComponents( encoding ) {

		switch ( encoding ) {

			case LinearEncoding:
				return [ 'Linear', '( value )' ];
			case sRGBEncoding:
				return [ 'sRGB', '( value )' ];
			case RGBEEncoding:
				return [ 'RGBE', '( value )' ];
			case RGBM7Encoding:
				return [ 'RGBM', '( value, 7.0 )' ];
			case RGBM16Encoding:
				return [ 'RGBM', '( value, 16.0 )' ];
			case RGBDEncoding:
				return [ 'RGBD', '( value, 256.0 )' ];
			case GammaEncoding:
				return [ 'Gamma', '( value, float( GAMMA_FACTOR ) )' ];
			case LogLuvEncoding:
				return [ 'LogLuv', '( value )' ];
			default:
				console.warn( 'THREE.WebGLProgram: Unsupported encoding:', encoding );
				return [ 'Linear', '( value )' ];

		}

	}

	function getShaderErrors( gl, shader, type ) {

		const status = gl.getShaderParameter( shader, 35713 );
		const log = gl.getShaderInfoLog( shader ).trim();

		if ( status && log === '' ) return '';

		// --enable-privileged-webgl-extension
		// console.log( '**' + type + '**', gl.getExtension( 'WEBGL_debug_shaders' ).getTranslatedShaderSource( shader ) );

		const source = gl.getShaderSource( shader );

		return 'THREE.WebGLShader: gl.getShaderInfoLog() ' + type + '\n' + log + addLineNumbers( source );

	}

	function getTexelDecodingFunction( functionName, encoding ) {

		const components = getEncodingComponents( encoding );
		return 'vec4 ' + functionName + '( vec4 value ) { return ' + components[ 0 ] + 'ToLinear' + components[ 1 ] + '; }';

	}

	function getTexelEncodingFunction( functionName, encoding ) {

		const components = getEncodingComponents( encoding );
		return 'vec4 ' + functionName + '( vec4 value ) { return LinearTo' + components[ 0 ] + components[ 1 ] + '; }';

	}

	function getToneMappingFunction( functionName, toneMapping ) {

		let toneMappingName;

		switch ( toneMapping ) {

			case LinearToneMapping:
				toneMappingName = 'Linear';
				break;

			case ReinhardToneMapping:
				toneMappingName = 'Reinhard';
				break;

			case CineonToneMapping:
				toneMappingName = 'OptimizedCineon';
				break;

			case ACESFilmicToneMapping:
				toneMappingName = 'ACESFilmic';
				break;

			case CustomToneMapping:
				toneMappingName = 'Custom';
				break;

			default:
				console.warn( 'THREE.WebGLProgram: Unsupported toneMapping:', toneMapping );
				toneMappingName = 'Linear';

		}

		return 'vec3 ' + functionName + '( vec3 color ) { return ' + toneMappingName + 'ToneMapping( color ); }';

	}

	function generateExtensions( parameters ) {

		const chunks = [
			( parameters.extensionDerivatives || parameters.envMapCubeUV || parameters.bumpMap || parameters.tangentSpaceNormalMap || parameters.clearcoatNormalMap || parameters.flatShading || parameters.shaderID === 'physical' ) ? '#extension GL_OES_standard_derivatives : enable' : '',
			( parameters.extensionFragDepth || parameters.logarithmicDepthBuffer ) && parameters.rendererExtensionFragDepth ? '#extension GL_EXT_frag_depth : enable' : '',
			( parameters.extensionDrawBuffers && parameters.rendererExtensionDrawBuffers ) ? '#extension GL_EXT_draw_buffers : require' : '',
			( parameters.extensionShaderTextureLOD || parameters.envMap ) && parameters.rendererExtensionShaderTextureLod ? '#extension GL_EXT_shader_texture_lod : enable' : ''
		];

		return chunks.filter( filterEmptyLine ).join( '\n' );

	}

	function generateDefines( defines ) {

		const chunks = [];

		for ( const name in defines ) {

			const value = defines[ name ];

			if ( value === false ) continue;

			chunks.push( '#define ' + name + ' ' + value );

		}

		return chunks.join( '\n' );

	}

	function fetchAttributeLocations( gl, program ) {

		const attributes = {};

		const n = gl.getProgramParameter( program, 35721 );

		for ( let i = 0; i < n; i ++ ) {

			const info = gl.getActiveAttrib( program, i );
			const name = info.name;

			// console.log( 'THREE.WebGLProgram: ACTIVE VERTEX ATTRIBUTE:', name, i );

			attributes[ name ] = gl.getAttribLocation( program, name );

		}

		return attributes;

	}

	function filterEmptyLine( string ) {

		return string !== '';

	}

	function replaceLightNums( string, parameters ) {

		return string
			.replace( /NUM_DIR_LIGHTS/g, parameters.numDirLights )
			.replace( /NUM_SPOT_LIGHTS/g, parameters.numSpotLights )
			.replace( /NUM_RECT_AREA_LIGHTS/g, parameters.numRectAreaLights )
			.replace( /NUM_POINT_LIGHTS/g, parameters.numPointLights )
			.replace( /NUM_HEMI_LIGHTS/g, parameters.numHemiLights )
			.replace( /NUM_DIR_LIGHT_SHADOWS/g, parameters.numDirLightShadows )
			.replace( /NUM_SPOT_LIGHT_SHADOWS/g, parameters.numSpotLightShadows )
			.replace( /NUM_POINT_LIGHT_SHADOWS/g, parameters.numPointLightShadows );

	}

	function replaceClippingPlaneNums( string, parameters ) {

		return string
			.replace( /NUM_CLIPPING_PLANES/g, parameters.numClippingPlanes )
			.replace( /UNION_CLIPPING_PLANES/g, ( parameters.numClippingPlanes - parameters.numClipIntersection ) );

	}

	// Resolve Includes

	const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm;

	function resolveIncludes( string ) {

		return string.replace( includePattern, includeReplacer );

	}

	function includeReplacer( match, include ) {

		const string = ShaderChunk[ include ];

		if ( string === undefined ) {

			throw new Error( 'Can not resolve #include <' + include + '>' );

		}

		return resolveIncludes( string );

	}

	// Unroll Loops

	const deprecatedUnrollLoopPattern = /#pragma unroll_loop[\s]+?for \( int i \= (\d+)\; i < (\d+)\; i \+\+ \) \{([\s\S]+?)(?=\})\}/g;
	const unrollLoopPattern = /#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;

	function unrollLoops( string ) {

		return string
			.replace( unrollLoopPattern, loopReplacer )
			.replace( deprecatedUnrollLoopPattern, deprecatedLoopReplacer );

	}

	function deprecatedLoopReplacer( match, start, end, snippet ) {

		console.warn( 'WebGLProgram: #pragma unroll_loop shader syntax is deprecated. Please use #pragma unroll_loop_start syntax instead.' );
		return loopReplacer( match, start, end, snippet );

	}

	function loopReplacer( match, start, end, snippet ) {

		let string = '';

		for ( let i = parseInt( start ); i < parseInt( end ); i ++ ) {

			string += snippet
				.replace( /\[\s*i\s*\]/g, '[ ' + i + ' ]' )
				.replace( /UNROLLED_LOOP_INDEX/g, i );

		}

		return string;

	}

	//

	function generatePrecision( parameters ) {

		let precisionstring = 'precision ' + parameters.precision + ' float;\nprecision ' + parameters.precision + ' int;';

		if ( parameters.precision === 'highp' ) {

			precisionstring += '\n#define HIGH_PRECISION';

		} else if ( parameters.precision === 'mediump' ) {

			precisionstring += '\n#define MEDIUM_PRECISION';

		} else if ( parameters.precision === 'lowp' ) {

			precisionstring += '\n#define LOW_PRECISION';

		}

		return precisionstring;

	}

	function generateShadowMapTypeDefine( parameters ) {

		let shadowMapTypeDefine = 'SHADOWMAP_TYPE_BASIC';

		if ( parameters.shadowMapType === PCFShadowMap ) {

			shadowMapTypeDefine = 'SHADOWMAP_TYPE_PCF';

		} else if ( parameters.shadowMapType === PCFSoftShadowMap ) {

			shadowMapTypeDefine = 'SHADOWMAP_TYPE_PCF_SOFT';

		} else if ( parameters.shadowMapType === VSMShadowMap ) {

			shadowMapTypeDefine = 'SHADOWMAP_TYPE_VSM';

		}

		return shadowMapTypeDefine;

	}

	function generateEnvMapTypeDefine( parameters ) {

		let envMapTypeDefine = 'ENVMAP_TYPE_CUBE';

		if ( parameters.envMap ) {

			switch ( parameters.envMapMode ) {

				case CubeReflectionMapping:
				case CubeRefractionMapping:
					envMapTypeDefine = 'ENVMAP_TYPE_CUBE';
					break;

				case CubeUVReflectionMapping:
				case CubeUVRefractionMapping:
					envMapTypeDefine = 'ENVMAP_TYPE_CUBE_UV';
					break;

			}

		}

		return envMapTypeDefine;

	}

	function generateEnvMapModeDefine( parameters ) {

		let envMapModeDefine = 'ENVMAP_MODE_REFLECTION';

		if ( parameters.envMap ) {

			switch ( parameters.envMapMode ) {

				case CubeRefractionMapping:
				case CubeUVRefractionMapping:

					envMapModeDefine = 'ENVMAP_MODE_REFRACTION';
					break;

			}

		}

		return envMapModeDefine;

	}

	function generateEnvMapBlendingDefine( parameters ) {

		let envMapBlendingDefine = 'ENVMAP_BLENDING_NONE';

		if ( parameters.envMap ) {

			switch ( parameters.combine ) {

				case MultiplyOperation:
					envMapBlendingDefine = 'ENVMAP_BLENDING_MULTIPLY';
					break;

				case MixOperation:
					envMapBlendingDefine = 'ENVMAP_BLENDING_MIX';
					break;

				case AddOperation:
					envMapBlendingDefine = 'ENVMAP_BLENDING_ADD';
					break;

			}

		}

		return envMapBlendingDefine;

	}

	function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {

		const gl = renderer.getContext();

		const defines = parameters.defines;

		let vertexShader = parameters.vertexShader;
		let fragmentShader = parameters.fragmentShader;

		const shadowMapTypeDefine = generateShadowMapTypeDefine( parameters );
		const envMapTypeDefine = generateEnvMapTypeDefine( parameters );
		const envMapModeDefine = generateEnvMapModeDefine( parameters );
		const envMapBlendingDefine = generateEnvMapBlendingDefine( parameters );


		const gammaFactorDefine = ( renderer.gammaFactor > 0 ) ? renderer.gammaFactor : 1.0;

		const customExtensions = parameters.isWebGL2 ? '' : generateExtensions( parameters );

		const customDefines = generateDefines( defines );

		const program = gl.createProgram();

		let prefixVertex, prefixFragment;
		let versionString = parameters.glslVersion ? '#version ' + parameters.glslVersion + '\n' : '';

		if ( parameters.isRawShaderMaterial ) {

			prefixVertex = [

				customDefines

			].filter( filterEmptyLine ).join( '\n' );

			if ( prefixVertex.length > 0 ) {

				prefixVertex += '\n';

			}

			prefixFragment = [

				customExtensions,
				customDefines

			].filter( filterEmptyLine ).join( '\n' );

			if ( prefixFragment.length > 0 ) {

				prefixFragment += '\n';

			}

		} else {

			prefixVertex = [

				generatePrecision( parameters ),

				'#define SHADER_NAME ' + parameters.shaderName,

				customDefines,

				parameters.instancing ? '#define USE_INSTANCING' : '',
				parameters.instancingColor ? '#define USE_INSTANCING_COLOR' : '',

				parameters.supportsVertexTextures ? '#define VERTEX_TEXTURES' : '',

				'#define GAMMA_FACTOR ' + gammaFactorDefine,

				'#define MAX_BONES ' + parameters.maxBones,
				( parameters.useFog && parameters.fog ) ? '#define USE_FOG' : '',
				( parameters.useFog && parameters.fogExp2 ) ? '#define FOG_EXP2' : '',

				parameters.map ? '#define USE_MAP' : '',
				parameters.envMap ? '#define USE_ENVMAP' : '',
				parameters.envMap ? '#define ' + envMapModeDefine : '',
				parameters.lightMap ? '#define USE_LIGHTMAP' : '',
				parameters.aoMap ? '#define USE_AOMAP' : '',
				parameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',
				parameters.bumpMap ? '#define USE_BUMPMAP' : '',
				parameters.normalMap ? '#define USE_NORMALMAP' : '',
				( parameters.normalMap && parameters.objectSpaceNormalMap ) ? '#define OBJECTSPACE_NORMALMAP' : '',
				( parameters.normalMap && parameters.tangentSpaceNormalMap ) ? '#define TANGENTSPACE_NORMALMAP' : '',

				parameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',
				parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
				parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',
				parameters.displacementMap && parameters.supportsVertexTextures ? '#define USE_DISPLACEMENTMAP' : '',
				parameters.specularMap ? '#define USE_SPECULARMAP' : '',
				parameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',
				parameters.metalnessMap ? '#define USE_METALNESSMAP' : '',
				parameters.alphaMap ? '#define USE_ALPHAMAP' : '',
				parameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',

				parameters.vertexTangents ? '#define USE_TANGENT' : '',
				parameters.vertexColors ? '#define USE_COLOR' : '',
				parameters.vertexUvs ? '#define USE_UV' : '',
				parameters.uvsVertexOnly ? '#define UVS_VERTEX_ONLY' : '',

				parameters.flatShading ? '#define FLAT_SHADED' : '',

				parameters.skinning ? '#define USE_SKINNING' : '',
				parameters.useVertexTexture ? '#define BONE_TEXTURE' : '',

				parameters.morphTargets ? '#define USE_MORPHTARGETS' : '',
				parameters.morphNormals && parameters.flatShading === false ? '#define USE_MORPHNORMALS' : '',
				parameters.doubleSided ? '#define DOUBLE_SIDED' : '',
				parameters.flipSided ? '#define FLIP_SIDED' : '',

				parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
				parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',

				parameters.sizeAttenuation ? '#define USE_SIZEATTENUATION' : '',

				parameters.logarithmicDepthBuffer ? '#define USE_LOGDEPTHBUF' : '',
				( parameters.logarithmicDepthBuffer && parameters.rendererExtensionFragDepth ) ? '#define USE_LOGDEPTHBUF_EXT' : '',

				'uniform mat4 modelMatrix;',
				'uniform mat4 modelViewMatrix;',
				'uniform mat4 projectionMatrix;',
				'uniform mat4 viewMatrix;',
				'uniform mat3 normalMatrix;',
				'uniform vec3 cameraPosition;',
				'uniform bool isOrthographic;',

				'#ifdef USE_INSTANCING',

				'	attribute mat4 instanceMatrix;',

				'#endif',

				'#ifdef USE_INSTANCING_COLOR',

				'	attribute vec3 instanceColor;',

				'#endif',

				'attribute vec3 position;',
				'attribute vec3 normal;',
				'attribute vec2 uv;',

				'#ifdef USE_TANGENT',

				'	attribute vec4 tangent;',

				'#endif',

				'#ifdef USE_COLOR',

				'	attribute vec3 color;',

				'#endif',

				'#ifdef USE_MORPHTARGETS',

				'	attribute vec3 morphTarget0;',
				'	attribute vec3 morphTarget1;',
				'	attribute vec3 morphTarget2;',
				'	attribute vec3 morphTarget3;',

				'	#ifdef USE_MORPHNORMALS',

				'		attribute vec3 morphNormal0;',
				'		attribute vec3 morphNormal1;',
				'		attribute vec3 morphNormal2;',
				'		attribute vec3 morphNormal3;',

				'	#else',

				'		attribute vec3 morphTarget4;',
				'		attribute vec3 morphTarget5;',
				'		attribute vec3 morphTarget6;',
				'		attribute vec3 morphTarget7;',

				'	#endif',

				'#endif',

				'#ifdef USE_SKINNING',

				'	attribute vec4 skinIndex;',
				'	attribute vec4 skinWeight;',

				'#endif',

				'\n'

			].filter( filterEmptyLine ).join( '\n' );

			prefixFragment = [

				customExtensions,

				generatePrecision( parameters ),

				'#define SHADER_NAME ' + parameters.shaderName,

				customDefines,

				parameters.alphaTest ? '#define ALPHATEST ' + parameters.alphaTest + ( parameters.alphaTest % 1 ? '' : '.0' ) : '', // add '.0' if integer

				'#define GAMMA_FACTOR ' + gammaFactorDefine,

				( parameters.useFog && parameters.fog ) ? '#define USE_FOG' : '',
				( parameters.useFog && parameters.fogExp2 ) ? '#define FOG_EXP2' : '',

				parameters.map ? '#define USE_MAP' : '',
				parameters.matcap ? '#define USE_MATCAP' : '',
				parameters.envMap ? '#define USE_ENVMAP' : '',
				parameters.envMap ? '#define ' + envMapTypeDefine : '',
				parameters.envMap ? '#define ' + envMapModeDefine : '',
				parameters.envMap ? '#define ' + envMapBlendingDefine : '',
				parameters.lightMap ? '#define USE_LIGHTMAP' : '',
				parameters.aoMap ? '#define USE_AOMAP' : '',
				parameters.emissiveMap ? '#define USE_EMISSIVEMAP' : '',
				parameters.bumpMap ? '#define USE_BUMPMAP' : '',
				parameters.normalMap ? '#define USE_NORMALMAP' : '',
				( parameters.normalMap && parameters.objectSpaceNormalMap ) ? '#define OBJECTSPACE_NORMALMAP' : '',
				( parameters.normalMap && parameters.tangentSpaceNormalMap ) ? '#define TANGENTSPACE_NORMALMAP' : '',
				parameters.clearcoatMap ? '#define USE_CLEARCOATMAP' : '',
				parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
				parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',
				parameters.specularMap ? '#define USE_SPECULARMAP' : '',
				parameters.roughnessMap ? '#define USE_ROUGHNESSMAP' : '',
				parameters.metalnessMap ? '#define USE_METALNESSMAP' : '',
				parameters.alphaMap ? '#define USE_ALPHAMAP' : '',

				parameters.sheen ? '#define USE_SHEEN' : '',
				parameters.transmissionMap ? '#define USE_TRANSMISSIONMAP' : '',

				parameters.vertexTangents ? '#define USE_TANGENT' : '',
				parameters.vertexColors || parameters.instancingColor ? '#define USE_COLOR' : '',
				parameters.vertexUvs ? '#define USE_UV' : '',
				parameters.uvsVertexOnly ? '#define UVS_VERTEX_ONLY' : '',

				parameters.gradientMap ? '#define USE_GRADIENTMAP' : '',

				parameters.flatShading ? '#define FLAT_SHADED' : '',

				parameters.doubleSided ? '#define DOUBLE_SIDED' : '',
				parameters.flipSided ? '#define FLIP_SIDED' : '',

				parameters.shadowMapEnabled ? '#define USE_SHADOWMAP' : '',
				parameters.shadowMapEnabled ? '#define ' + shadowMapTypeDefine : '',

				parameters.premultipliedAlpha ? '#define PREMULTIPLIED_ALPHA' : '',

				parameters.physicallyCorrectLights ? '#define PHYSICALLY_CORRECT_LIGHTS' : '',

				parameters.logarithmicDepthBuffer ? '#define USE_LOGDEPTHBUF' : '',
				( parameters.logarithmicDepthBuffer && parameters.rendererExtensionFragDepth ) ? '#define USE_LOGDEPTHBUF_EXT' : '',

				( ( parameters.extensionShaderTextureLOD || parameters.envMap ) && parameters.rendererExtensionShaderTextureLod ) ? '#define TEXTURE_LOD_EXT' : '',

				'uniform mat4 viewMatrix;',
				'uniform vec3 cameraPosition;',
				'uniform bool isOrthographic;',

				( parameters.toneMapping !== NoToneMapping ) ? '#define TONE_MAPPING' : '',
				( parameters.toneMapping !== NoToneMapping ) ? ShaderChunk[ 'tonemapping_pars_fragment' ] : '', // this code is required here because it is used by the toneMapping() function defined below
				( parameters.toneMapping !== NoToneMapping ) ? getToneMappingFunction( 'toneMapping', parameters.toneMapping ) : '',

				parameters.dithering ? '#define DITHERING' : '',

				ShaderChunk[ 'encodings_pars_fragment' ], // this code is required here because it is used by the various encoding/decoding function defined below
				parameters.map ? getTexelDecodingFunction( 'mapTexelToLinear', parameters.mapEncoding ) : '',
				parameters.matcap ? getTexelDecodingFunction( 'matcapTexelToLinear', parameters.matcapEncoding ) : '',
				parameters.envMap ? getTexelDecodingFunction( 'envMapTexelToLinear', parameters.envMapEncoding ) : '',
				parameters.emissiveMap ? getTexelDecodingFunction( 'emissiveMapTexelToLinear', parameters.emissiveMapEncoding ) : '',
				parameters.lightMap ? getTexelDecodingFunction( 'lightMapTexelToLinear', parameters.lightMapEncoding ) : '',
				getTexelEncodingFunction( 'linearToOutputTexel', parameters.outputEncoding ),

				parameters.depthPacking ? '#define DEPTH_PACKING ' + parameters.depthPacking : '',

				'\n'

			].filter( filterEmptyLine ).join( '\n' );

		}

		vertexShader = resolveIncludes( vertexShader );
		vertexShader = replaceLightNums( vertexShader, parameters );
		vertexShader = replaceClippingPlaneNums( vertexShader, parameters );

		fragmentShader = resolveIncludes( fragmentShader );
		fragmentShader = replaceLightNums( fragmentShader, parameters );
		fragmentShader = replaceClippingPlaneNums( fragmentShader, parameters );

		vertexShader = unrollLoops( vertexShader );
		fragmentShader = unrollLoops( fragmentShader );

		if ( parameters.isWebGL2 && parameters.isRawShaderMaterial !== true ) {

			// GLSL 3.0 conversion for built-in materials and ShaderMaterial

			versionString = '#version 300 es\n';

			prefixVertex = [
				'#define attribute in',
				'#define varying out',
				'#define texture2D texture'
			].join( '\n' ) + '\n' + prefixVertex;

			prefixFragment = [
				'#define varying in',
				( parameters.glslVersion === GLSL3 ) ? '' : 'out highp vec4 pc_fragColor;',
				( parameters.glslVersion === GLSL3 ) ? '' : '#define gl_FragColor pc_fragColor',
				'#define gl_FragDepthEXT gl_FragDepth',
				'#define texture2D texture',
				'#define textureCube texture',
				'#define texture2DProj textureProj',
				'#define texture2DLodEXT textureLod',
				'#define texture2DProjLodEXT textureProjLod',
				'#define textureCubeLodEXT textureLod',
				'#define texture2DGradEXT textureGrad',
				'#define texture2DProjGradEXT textureProjGrad',
				'#define textureCubeGradEXT textureGrad'
			].join( '\n' ) + '\n' + prefixFragment;

		}

		const vertexGlsl = versionString + prefixVertex + vertexShader;
		const fragmentGlsl = versionString + prefixFragment + fragmentShader;

		// console.log( '*VERTEX*', vertexGlsl );
		// console.log( '*FRAGMENT*', fragmentGlsl );

		const glVertexShader = WebGLShader( gl, 35633, vertexGlsl );
		const glFragmentShader = WebGLShader( gl, 35632, fragmentGlsl );

		gl.attachShader( program, glVertexShader );
		gl.attachShader( program, glFragmentShader );

		// Force a particular attribute to index 0.

		if ( parameters.index0AttributeName !== undefined ) {

			gl.bindAttribLocation( program, 0, parameters.index0AttributeName );

		} else if ( parameters.morphTargets === true ) {

			// programs with morphTargets displace position out of attribute 0
			gl.bindAttribLocation( program, 0, 'position' );

		}

		gl.linkProgram( program );

		// check for link errors
		if ( renderer.debug.checkShaderErrors ) {

			const programLog = gl.getProgramInfoLog( program ).trim();
			const vertexLog = gl.getShaderInfoLog( glVertexShader ).trim();
			const fragmentLog = gl.getShaderInfoLog( glFragmentShader ).trim();

			let runnable = true;
			let haveDiagnostics = true;

			if ( gl.getProgramParameter( program, 35714 ) === false ) {

				runnable = false;

				const vertexErrors = getShaderErrors( gl, glVertexShader, 'vertex' );
				const fragmentErrors = getShaderErrors( gl, glFragmentShader, 'fragment' );

				console.error( 'THREE.WebGLProgram: shader error: ', gl.getError(), '35715', gl.getProgramParameter( program, 35715 ), 'gl.getProgramInfoLog', programLog, vertexErrors, fragmentErrors );

			} else if ( programLog !== '' ) {

				console.warn( 'THREE.WebGLProgram: gl.getProgramInfoLog()', programLog );

			} else if ( vertexLog === '' || fragmentLog === '' ) {

				haveDiagnostics = false;

			}

			if ( haveDiagnostics ) {

				this.diagnostics = {

					runnable: runnable,

					programLog: programLog,

					vertexShader: {

						log: vertexLog,
						prefix: prefixVertex

					},

					fragmentShader: {

						log: fragmentLog,
						prefix: prefixFragment

					}

				};

			}

		}

		// Clean up

		// Crashes in iOS9 and iOS10. #18402
		// gl.detachShader( program, glVertexShader );
		// gl.detachShader( program, glFragmentShader );

		gl.deleteShader( glVertexShader );
		gl.deleteShader( glFragmentShader );

		// set up caching for uniform locations

		let cachedUniforms;

		this.getUniforms = function () {

			if ( cachedUniforms === undefined ) {

				cachedUniforms = new WebGLUniforms( gl, program );

			}

			return cachedUniforms;

		};

		// set up caching for attribute locations

		let cachedAttributes;

		this.getAttributes = function () {

			if ( cachedAttributes === undefined ) {

				cachedAttributes = fetchAttributeLocations( gl, program );

			}

			return cachedAttributes;

		};

		// free resource

		this.destroy = function () {

			bindingStates.releaseStatesOfProgram( this );

			gl.deleteProgram( program );
			this.program = undefined;

		};

		//

		this.name = parameters.shaderName;
		this.id = programIdCount ++;
		this.cacheKey = cacheKey;
		this.usedTimes = 1;
		this.program = program;
		this.vertexShader = glVertexShader;
		this.fragmentShader = glFragmentShader;

		return this;

	}

	function WebGLPrograms( renderer, cubemaps, extensions, capabilities, bindingStates, clipping ) {

		const programs = [];

		const isWebGL2 = capabilities.isWebGL2;
		const logarithmicDepthBuffer = capabilities.logarithmicDepthBuffer;
		const floatVertexTextures = capabilities.floatVertexTextures;
		const maxVertexUniforms = capabilities.maxVertexUniforms;
		const vertexTextures = capabilities.vertexTextures;

		let precision = capabilities.precision;

		const shaderIDs = {
			MeshDepthMaterial: 'depth',
			MeshDistanceMaterial: 'distanceRGBA',
			MeshNormalMaterial: 'normal',
			MeshBasicMaterial: 'basic',
			MeshLambertMaterial: 'lambert',
			MeshPhongMaterial: 'phong',
			MeshToonMaterial: 'toon',
			MeshStandardMaterial: 'physical',
			MeshPhysicalMaterial: 'physical',
			MeshMatcapMaterial: 'matcap',
			LineBasicMaterial: 'basic',
			LineDashedMaterial: 'dashed',
			PointsMaterial: 'points',
			ShadowMaterial: 'shadow',
			SpriteMaterial: 'sprite'
		};

		const parameterNames = [
			'precision', 'isWebGL2', 'supportsVertexTextures', 'outputEncoding', 'instancing', 'instancingColor',
			'map', 'mapEncoding', 'matcap', 'matcapEncoding', 'envMap', 'envMapMode', 'envMapEncoding', 'envMapCubeUV',
			'lightMap', 'lightMapEncoding', 'aoMap', 'emissiveMap', 'emissiveMapEncoding', 'bumpMap', 'normalMap', 'objectSpaceNormalMap', 'tangentSpaceNormalMap', 'clearcoatMap', 'clearcoatRoughnessMap', 'clearcoatNormalMap', 'displacementMap', 'specularMap',
			'roughnessMap', 'metalnessMap', 'gradientMap',
			'alphaMap', 'combine', 'vertexColors', 'vertexTangents', 'vertexUvs', 'uvsVertexOnly', 'fog', 'useFog', 'fogExp2',
			'flatShading', 'sizeAttenuation', 'logarithmicDepthBuffer', 'skinning',
			'maxBones', 'useVertexTexture', 'morphTargets', 'morphNormals',
			'maxMorphTargets', 'maxMorphNormals', 'premultipliedAlpha',
			'numDirLights', 'numPointLights', 'numSpotLights', 'numHemiLights', 'numRectAreaLights',
			'numDirLightShadows', 'numPointLightShadows', 'numSpotLightShadows',
			'shadowMapEnabled', 'shadowMapType', 'toneMapping', 'physicallyCorrectLights',
			'alphaTest', 'doubleSided', 'flipSided', 'numClippingPlanes', 'numClipIntersection', 'depthPacking', 'dithering',
			'sheen', 'transmissionMap'
		];

		function getMaxBones( object ) {

			const skeleton = object.skeleton;
			const bones = skeleton.bones;

			if ( floatVertexTextures ) {

				return 1024;

			} else {

				// default for when object is not specified
				// ( for example when prebuilding shader to be used with multiple objects )
				//
				//  - leave some extra space for other uniforms
				//  - limit here is ANGLE's 254 max uniform vectors
				//    (up to 54 should be safe)

				const nVertexUniforms = maxVertexUniforms;
				const nVertexMatrices = Math.floor( ( nVertexUniforms - 20 ) / 4 );

				const maxBones = Math.min( nVertexMatrices, bones.length );

				if ( maxBones < bones.length ) {

					console.warn( 'THREE.WebGLRenderer: Skeleton has ' + bones.length + ' bones. This GPU supports ' + maxBones + '.' );
					return 0;

				}

				return maxBones;

			}

		}

		function getTextureEncodingFromMap( map ) {

			let encoding;

			if ( map && map.isTexture ) {

				encoding = map.encoding;

			} else if ( map && map.isWebGLRenderTarget ) {

				console.warn( 'THREE.WebGLPrograms.getTextureEncodingFromMap: don\'t use render targets as textures. Use their .texture property instead.' );
				encoding = map.texture.encoding;

			} else {

				encoding = LinearEncoding;

			}

			return encoding;

		}

		function getParameters( material, lights, shadows, scene, object ) {

			const fog = scene.fog;
			const environment = material.isMeshStandardMaterial ? scene.environment : null;

			const envMap = cubemaps.get( material.envMap || environment );

			const shaderID = shaderIDs[ material.type ];

			// heuristics to create shader parameters according to lights in the scene
			// (not to blow over maxLights budget)

			const maxBones = object.isSkinnedMesh ? getMaxBones( object ) : 0;

			if ( material.precision !== null ) {

				precision = capabilities.getMaxPrecision( material.precision );

				if ( precision !== material.precision ) {

					console.warn( 'THREE.WebGLProgram.getParameters:', material.precision, 'not supported, using', precision, 'instead.' );

				}

			}

			let vertexShader, fragmentShader;

			if ( shaderID ) {

				const shader = ShaderLib[ shaderID ];

				vertexShader = shader.vertexShader;
				fragmentShader = shader.fragmentShader;

			} else {

				vertexShader = material.vertexShader;
				fragmentShader = material.fragmentShader;

			}

			const currentRenderTarget = renderer.getRenderTarget();

			const parameters = {

				isWebGL2: isWebGL2,

				shaderID: shaderID,
				shaderName: material.type,

				vertexShader: vertexShader,
				fragmentShader: fragmentShader,
				defines: material.defines,

				isRawShaderMaterial: material.isRawShaderMaterial === true,
				glslVersion: material.glslVersion,

				precision: precision,

				instancing: object.isInstancedMesh === true,
				instancingColor: object.isInstancedMesh === true && object.instanceColor !== null,

				supportsVertexTextures: vertexTextures,
				outputEncoding: ( currentRenderTarget !== null ) ? getTextureEncodingFromMap( currentRenderTarget.texture ) : renderer.outputEncoding,
				map: !! material.map,
				mapEncoding: getTextureEncodingFromMap( material.map ),
				matcap: !! material.matcap,
				matcapEncoding: getTextureEncodingFromMap( material.matcap ),
				envMap: !! envMap,
				envMapMode: envMap && envMap.mapping,
				envMapEncoding: getTextureEncodingFromMap( envMap ),
				envMapCubeUV: ( !! envMap ) && ( ( envMap.mapping === CubeUVReflectionMapping ) || ( envMap.mapping === CubeUVRefractionMapping ) ),
				lightMap: !! material.lightMap,
				lightMapEncoding: getTextureEncodingFromMap( material.lightMap ),
				aoMap: !! material.aoMap,
				emissiveMap: !! material.emissiveMap,
				emissiveMapEncoding: getTextureEncodingFromMap( material.emissiveMap ),
				bumpMap: !! material.bumpMap,
				normalMap: !! material.normalMap,
				objectSpaceNormalMap: material.normalMapType === ObjectSpaceNormalMap,
				tangentSpaceNormalMap: material.normalMapType === TangentSpaceNormalMap,
				clearcoatMap: !! material.clearcoatMap,
				clearcoatRoughnessMap: !! material.clearcoatRoughnessMap,
				clearcoatNormalMap: !! material.clearcoatNormalMap,
				displacementMap: !! material.displacementMap,
				roughnessMap: !! material.roughnessMap,
				metalnessMap: !! material.metalnessMap,
				specularMap: !! material.specularMap,
				alphaMap: !! material.alphaMap,

				gradientMap: !! material.gradientMap,

				sheen: !! material.sheen,

				transmissionMap: !! material.transmissionMap,

				combine: material.combine,

				vertexTangents: ( material.normalMap && material.vertexTangents ),
				vertexColors: material.vertexColors,
				vertexUvs: !! material.map || !! material.bumpMap || !! material.normalMap || !! material.specularMap || !! material.alphaMap || !! material.emissiveMap || !! material.roughnessMap || !! material.metalnessMap || !! material.clearcoatMap || !! material.clearcoatRoughnessMap || !! material.clearcoatNormalMap || !! material.displacementMap || !! material.transmissionMap,
				uvsVertexOnly: ! ( !! material.map || !! material.bumpMap || !! material.normalMap || !! material.specularMap || !! material.alphaMap || !! material.emissiveMap || !! material.roughnessMap || !! material.metalnessMap || !! material.clearcoatNormalMap || !! material.transmissionMap ) && !! material.displacementMap,

				fog: !! fog,
				useFog: material.fog,
				fogExp2: ( fog && fog.isFogExp2 ),

				flatShading: material.flatShading,

				sizeAttenuation: material.sizeAttenuation,
				logarithmicDepthBuffer: logarithmicDepthBuffer,

				skinning: material.skinning && maxBones > 0,
				maxBones: maxBones,
				useVertexTexture: floatVertexTextures,

				morphTargets: material.morphTargets,
				morphNormals: material.morphNormals,
				maxMorphTargets: renderer.maxMorphTargets,
				maxMorphNormals: renderer.maxMorphNormals,

				numDirLights: lights.directional.length,
				numPointLights: lights.point.length,
				numSpotLights: lights.spot.length,
				numRectAreaLights: lights.rectArea.length,
				numHemiLights: lights.hemi.length,

				numDirLightShadows: lights.directionalShadowMap.length,
				numPointLightShadows: lights.pointShadowMap.length,
				numSpotLightShadows: lights.spotShadowMap.length,

				numClippingPlanes: clipping.numPlanes,
				numClipIntersection: clipping.numIntersection,

				dithering: material.dithering,

				shadowMapEnabled: renderer.shadowMap.enabled && shadows.length > 0,
				shadowMapType: renderer.shadowMap.type,

				toneMapping: material.toneMapped ? renderer.toneMapping : NoToneMapping,
				physicallyCorrectLights: renderer.physicallyCorrectLights,

				premultipliedAlpha: material.premultipliedAlpha,

				alphaTest: material.alphaTest,
				doubleSided: material.side === DoubleSide,
				flipSided: material.side === BackSide,

				depthPacking: ( material.depthPacking !== undefined ) ? material.depthPacking : false,

				index0AttributeName: material.index0AttributeName,

				extensionDerivatives: material.extensions && material.extensions.derivatives,
				extensionFragDepth: material.extensions && material.extensions.fragDepth,
				extensionDrawBuffers: material.extensions && material.extensions.drawBuffers,
				extensionShaderTextureLOD: material.extensions && material.extensions.shaderTextureLOD,

				rendererExtensionFragDepth: isWebGL2 || extensions.has( 'EXT_frag_depth' ),
				rendererExtensionDrawBuffers: isWebGL2 || extensions.has( 'WEBGL_draw_buffers' ),
				rendererExtensionShaderTextureLod: isWebGL2 || extensions.has( 'EXT_shader_texture_lod' ),

				customProgramCacheKey: material.customProgramCacheKey()

			};

			return parameters;

		}

		function getProgramCacheKey( parameters ) {

			const array = [];

			if ( parameters.shaderID ) {

				array.push( parameters.shaderID );

			} else {

				array.push( parameters.fragmentShader );
				array.push( parameters.vertexShader );

			}

			if ( parameters.defines !== undefined ) {

				for ( const name in parameters.defines ) {

					array.push( name );
					array.push( parameters.defines[ name ] );

				}

			}

			if ( parameters.isRawShaderMaterial === false ) {

				for ( let i = 0; i < parameterNames.length; i ++ ) {

					array.push( parameters[ parameterNames[ i ] ] );

				}

				array.push( renderer.outputEncoding );
				array.push( renderer.gammaFactor );

			}

			array.push( parameters.customProgramCacheKey );

			return array.join();

		}

		function getUniforms( material ) {

			const shaderID = shaderIDs[ material.type ];
			let uniforms;

			if ( shaderID ) {

				const shader = ShaderLib[ shaderID ];
				uniforms = UniformsUtils.clone( shader.uniforms );

			} else {

				uniforms = material.uniforms;

			}

			return uniforms;

		}

		function acquireProgram( parameters, cacheKey ) {

			let program;

			// Check if code has been already compiled
			for ( let p = 0, pl = programs.length; p < pl; p ++ ) {

				const preexistingProgram = programs[ p ];

				if ( preexistingProgram.cacheKey === cacheKey ) {

					program = preexistingProgram;
					++ program.usedTimes;

					break;

				}

			}

			if ( program === undefined ) {

				program = new WebGLProgram( renderer, cacheKey, parameters, bindingStates );
				programs.push( program );

			}

			return program;

		}

		function releaseProgram( program ) {

			if ( -- program.usedTimes === 0 ) {

				// Remove from unordered set
				const i = programs.indexOf( program );
				programs[ i ] = programs[ programs.length - 1 ];
				programs.pop();

				// Free WebGL resources
				program.destroy();

			}

		}

		return {
			getParameters: getParameters,
			getProgramCacheKey: getProgramCacheKey,
			getUniforms: getUniforms,
			acquireProgram: acquireProgram,
			releaseProgram: releaseProgram,
			// Exposed for resource monitoring & error feedback via renderer.info:
			programs: programs
		};

	}

	function WebGLProperties() {

		let properties = new WeakMap();

		function get( object ) {

			let map = properties.get( object );

			if ( map === undefined ) {

				map = {};
				properties.set( object, map );

			}

			return map;

		}

		function remove( object ) {

			properties.delete( object );

		}

		function update( object, key, value ) {

			properties.get( object )[ key ] = value;

		}

		function dispose() {

			properties = new WeakMap();

		}

		return {
			get: get,
			remove: remove,
			update: update,
			dispose: dispose
		};

	}

	function painterSortStable( a, b ) {

		if ( a.groupOrder !== b.groupOrder ) {

			return a.groupOrder - b.groupOrder;

		} else if ( a.renderOrder !== b.renderOrder ) {

			return a.renderOrder - b.renderOrder;

		} else if ( a.program !== b.program ) {

			return a.program.id - b.program.id;

		} else if ( a.material.id !== b.material.id ) {

			return a.material.id - b.material.id;

		} else if ( a.z !== b.z ) {

			return a.z - b.z;

		} else {

			return a.id - b.id;

		}

	}

	function reversePainterSortStable( a, b ) {

		if ( a.groupOrder !== b.groupOrder ) {

			return a.groupOrder - b.groupOrder;

		} else if ( a.renderOrder !== b.renderOrder ) {

			return a.renderOrder - b.renderOrder;

		} else if ( a.z !== b.z ) {

			return b.z - a.z;

		} else {

			return a.id - b.id;

		}

	}


	function WebGLRenderList( properties ) {

		const renderItems = [];
		let renderItemsIndex = 0;

		const opaque = [];
		const transparent = [];

		const defaultProgram = { id: - 1 };

		function init() {

			renderItemsIndex = 0;

			opaque.length = 0;
			transparent.length = 0;

		}

		function getNextRenderItem( object, geometry, material, groupOrder, z, group ) {

			let renderItem = renderItems[ renderItemsIndex ];
			const materialProperties = properties.get( material );

			if ( renderItem === undefined ) {

				renderItem = {
					id: object.id,
					object: object,
					geometry: geometry,
					material: material,
					program: materialProperties.program || defaultProgram,
					groupOrder: groupOrder,
					renderOrder: object.renderOrder,
					z: z,
					group: group
				};

				renderItems[ renderItemsIndex ] = renderItem;

			} else {

				renderItem.id = object.id;
				renderItem.object = object;
				renderItem.geometry = geometry;
				renderItem.material = material;
				renderItem.program = materialProperties.program || defaultProgram;
				renderItem.groupOrder = groupOrder;
				renderItem.renderOrder = object.renderOrder;
				renderItem.z = z;
				renderItem.group = group;

			}

			renderItemsIndex ++;

			return renderItem;

		}

		function push( object, geometry, material, groupOrder, z, group ) {

			const renderItem = getNextRenderItem( object, geometry, material, groupOrder, z, group );

			( material.transparent === true ? transparent : opaque ).push( renderItem );

		}

		function unshift( object, geometry, material, groupOrder, z, group ) {

			const renderItem = getNextRenderItem( object, geometry, material, groupOrder, z, group );

			( material.transparent === true ? transparent : opaque ).unshift( renderItem );

		}

		function sort( customOpaqueSort, customTransparentSort ) {

			if ( opaque.length > 1 ) opaque.sort( customOpaqueSort || painterSortStable );
			if ( transparent.length > 1 ) transparent.sort( customTransparentSort || reversePainterSortStable );

		}

		function finish() {

			// Clear references from inactive renderItems in the list

			for ( let i = renderItemsIndex, il = renderItems.length; i < il; i ++ ) {

				const renderItem = renderItems[ i ];

				if ( renderItem.id === null ) break;

				renderItem.id = null;
				renderItem.object = null;
				renderItem.geometry = null;
				renderItem.material = null;
				renderItem.program = null;
				renderItem.group = null;

			}

		}

		return {

			opaque: opaque,
			transparent: transparent,

			init: init,
			push: push,
			unshift: unshift,
			finish: finish,

			sort: sort
		};

	}

	function WebGLRenderLists( properties ) {

		let lists = new WeakMap();

		function get( scene, camera ) {

			const cameras = lists.get( scene );
			let list;

			if ( cameras === undefined ) {

				list = new WebGLRenderList( properties );
				lists.set( scene, new WeakMap() );
				lists.get( scene ).set( camera, list );

			} else {

				list = cameras.get( camera );
				if ( list === undefined ) {

					list = new WebGLRenderList( properties );
					cameras.set( camera, list );

				}

			}

			return list;

		}

		function dispose() {

			lists = new WeakMap();

		}

		return {
			get: get,
			dispose: dispose
		};

	}

	function UniformsCache() {

		const lights = {};

		return {

			get: function ( light ) {

				if ( lights[ light.id ] !== undefined ) {

					return lights[ light.id ];

				}

				let uniforms;

				switch ( light.type ) {

					case 'DirectionalLight':
						uniforms = {
							direction: new Vector3(),
							color: new Color()
						};
						break;

					case 'SpotLight':
						uniforms = {
							position: new Vector3(),
							direction: new Vector3(),
							color: new Color(),
							distance: 0,
							coneCos: 0,
							penumbraCos: 0,
							decay: 0
						};
						break;

					case 'PointLight':
						uniforms = {
							position: new Vector3(),
							color: new Color(),
							distance: 0,
							decay: 0
						};
						break;

					case 'HemisphereLight':
						uniforms = {
							direction: new Vector3(),
							skyColor: new Color(),
							groundColor: new Color()
						};
						break;

					case 'RectAreaLight':
						uniforms = {
							color: new Color(),
							position: new Vector3(),
							halfWidth: new Vector3(),
							halfHeight: new Vector3()
						};
						break;

				}

				lights[ light.id ] = uniforms;

				return uniforms;

			}

		};

	}

	function ShadowUniformsCache() {

		const lights = {};

		return {

			get: function ( light ) {

				if ( lights[ light.id ] !== undefined ) {

					return lights[ light.id ];

				}

				let uniforms;

				switch ( light.type ) {

					case 'DirectionalLight':
						uniforms = {
							shadowBias: 0,
							shadowNormalBias: 0,
							shadowRadius: 1,
							shadowMapSize: new Vector2()
						};
						break;

					case 'SpotLight':
						uniforms = {
							shadowBias: 0,
							shadowNormalBias: 0,
							shadowRadius: 1,
							shadowMapSize: new Vector2()
						};
						break;

					case 'PointLight':
						uniforms = {
							shadowBias: 0,
							shadowNormalBias: 0,
							shadowRadius: 1,
							shadowMapSize: new Vector2(),
							shadowCameraNear: 1,
							shadowCameraFar: 1000
						};
						break;

					// TODO (abelnation): set RectAreaLight shadow uniforms

				}

				lights[ light.id ] = uniforms;

				return uniforms;

			}

		};

	}



	let nextVersion = 0;

	function shadowCastingLightsFirst( lightA, lightB ) {

		return ( lightB.castShadow ? 1 : 0 ) - ( lightA.castShadow ? 1 : 0 );

	}

	function WebGLLights( extensions, capabilities ) {

		const cache = new UniformsCache();

		const shadowCache = ShadowUniformsCache();

		const state = {

			version: 0,

			hash: {
				directionalLength: - 1,
				pointLength: - 1,
				spotLength: - 1,
				rectAreaLength: - 1,
				hemiLength: - 1,

				numDirectionalShadows: - 1,
				numPointShadows: - 1,
				numSpotShadows: - 1
			},

			ambient: [ 0, 0, 0 ],
			probe: [],
			directional: [],
			directionalShadow: [],
			directionalShadowMap: [],
			directionalShadowMatrix: [],
			spot: [],
			spotShadow: [],
			spotShadowMap: [],
			spotShadowMatrix: [],
			rectArea: [],
			rectAreaLTC1: null,
			rectAreaLTC2: null,
			point: [],
			pointShadow: [],
			pointShadowMap: [],
			pointShadowMatrix: [],
			hemi: []

		};

		for ( let i = 0; i < 9; i ++ ) state.probe.push( new Vector3() );

		const vector3 = new Vector3();
		const matrix4 = new Matrix4();
		const matrix42 = new Matrix4();

		function setup( lights ) {

			let r = 0, g = 0, b = 0;

			for ( let i = 0; i < 9; i ++ ) state.probe[ i ].set( 0, 0, 0 );

			let directionalLength = 0;
			let pointLength = 0;
			let spotLength = 0;
			let rectAreaLength = 0;
			let hemiLength = 0;

			let numDirectionalShadows = 0;
			let numPointShadows = 0;
			let numSpotShadows = 0;

			lights.sort( shadowCastingLightsFirst );

			for ( let i = 0, l = lights.length; i < l; i ++ ) {

				const light = lights[ i ];

				const color = light.color;
				const intensity = light.intensity;
				const distance = light.distance;

				const shadowMap = ( light.shadow && light.shadow.map ) ? light.shadow.map.texture : null;

				if ( light.isAmbientLight ) {

					r += color.r * intensity;
					g += color.g * intensity;
					b += color.b * intensity;

				} else if ( light.isLightProbe ) {

					for ( let j = 0; j < 9; j ++ ) {

						state.probe[ j ].addScaledVector( light.sh.coefficients[ j ], intensity );

					}

				} else if ( light.isDirectionalLight ) {

					const uniforms = cache.get( light );

					uniforms.color.copy( light.color ).multiplyScalar( light.intensity );

					if ( light.castShadow ) {

						const shadow = light.shadow;

						const shadowUniforms = shadowCache.get( light );

						shadowUniforms.shadowBias = shadow.bias;
						shadowUniforms.shadowNormalBias = shadow.normalBias;
						shadowUniforms.shadowRadius = shadow.radius;
						shadowUniforms.shadowMapSize = shadow.mapSize;

						state.directionalShadow[ directionalLength ] = shadowUniforms;
						state.directionalShadowMap[ directionalLength ] = shadowMap;
						state.directionalShadowMatrix[ directionalLength ] = light.shadow.matrix;

						numDirectionalShadows ++;

					}

					state.directional[ directionalLength ] = uniforms;

					directionalLength ++;

				} else if ( light.isSpotLight ) {

					const uniforms = cache.get( light );

					uniforms.position.setFromMatrixPosition( light.matrixWorld );

					uniforms.color.copy( color ).multiplyScalar( intensity );
					uniforms.distance = distance;

					uniforms.coneCos = Math.cos( light.angle );
					uniforms.penumbraCos = Math.cos( light.angle * ( 1 - light.penumbra ) );
					uniforms.decay = light.decay;

					if ( light.castShadow ) {

						const shadow = light.shadow;

						const shadowUniforms = shadowCache.get( light );

						shadowUniforms.shadowBias = shadow.bias;
						shadowUniforms.shadowNormalBias = shadow.normalBias;
						shadowUniforms.shadowRadius = shadow.radius;
						shadowUniforms.shadowMapSize = shadow.mapSize;

						state.spotShadow[ spotLength ] = shadowUniforms;
						state.spotShadowMap[ spotLength ] = shadowMap;
						state.spotShadowMatrix[ spotLength ] = light.shadow.matrix;

						numSpotShadows ++;

					}

					state.spot[ spotLength ] = uniforms;

					spotLength ++;

				} else if ( light.isRectAreaLight ) {

					const uniforms = cache.get( light );

					// (a) intensity is the total visible light emitted
					//uniforms.color.copy( color ).multiplyScalar( intensity / ( light.width * light.height * Math.PI ) );

					// (b) intensity is the brightness of the light
					uniforms.color.copy( color ).multiplyScalar( intensity );

					uniforms.halfWidth.set( light.width * 0.5, 0.0, 0.0 );
					uniforms.halfHeight.set( 0.0, light.height * 0.5, 0.0 );

					state.rectArea[ rectAreaLength ] = uniforms;

					rectAreaLength ++;

				} else if ( light.isPointLight ) {

					const uniforms = cache.get( light );

					uniforms.color.copy( light.color ).multiplyScalar( light.intensity );
					uniforms.distance = light.distance;
					uniforms.decay = light.decay;

					if ( light.castShadow ) {

						const shadow = light.shadow;

						const shadowUniforms = shadowCache.get( light );

						shadowUniforms.shadowBias = shadow.bias;
						shadowUniforms.shadowNormalBias = shadow.normalBias;
						shadowUniforms.shadowRadius = shadow.radius;
						shadowUniforms.shadowMapSize = shadow.mapSize;
						shadowUniforms.shadowCameraNear = shadow.camera.near;
						shadowUniforms.shadowCameraFar = shadow.camera.far;

						state.pointShadow[ pointLength ] = shadowUniforms;
						state.pointShadowMap[ pointLength ] = shadowMap;
						state.pointShadowMatrix[ pointLength ] = light.shadow.matrix;

						numPointShadows ++;

					}

					state.point[ pointLength ] = uniforms;

					pointLength ++;

				} else if ( light.isHemisphereLight ) {

					const uniforms = cache.get( light );

					uniforms.skyColor.copy( light.color ).multiplyScalar( intensity );
					uniforms.groundColor.copy( light.groundColor ).multiplyScalar( intensity );

					state.hemi[ hemiLength ] = uniforms;

					hemiLength ++;

				}

			}

			if ( rectAreaLength > 0 ) {

				if ( capabilities.isWebGL2 ) {

					// WebGL 2

					state.rectAreaLTC1 = UniformsLib.LTC_FLOAT_1;
					state.rectAreaLTC2 = UniformsLib.LTC_FLOAT_2;

				} else {

					// WebGL 1

					if ( extensions.has( 'OES_texture_float_linear' ) === true ) {

						state.rectAreaLTC1 = UniformsLib.LTC_FLOAT_1;
						state.rectAreaLTC2 = UniformsLib.LTC_FLOAT_2;

					} else if ( extensions.has( 'OES_texture_half_float_linear' ) === true ) {

						state.rectAreaLTC1 = UniformsLib.LTC_HALF_1;
						state.rectAreaLTC2 = UniformsLib.LTC_HALF_2;

					} else {

						console.error( 'THREE.WebGLRenderer: Unable to use RectAreaLight. Missing WebGL extensions.' );

					}

				}

			}

			state.ambient[ 0 ] = r;
			state.ambient[ 1 ] = g;
			state.ambient[ 2 ] = b;

			const hash = state.hash;

			if ( hash.directionalLength !== directionalLength ||
				hash.pointLength !== pointLength ||
				hash.spotLength !== spotLength ||
				hash.rectAreaLength !== rectAreaLength ||
				hash.hemiLength !== hemiLength ||
				hash.numDirectionalShadows !== numDirectionalShadows ||
				hash.numPointShadows !== numPointShadows ||
				hash.numSpotShadows !== numSpotShadows ) {

				state.directional.length = directionalLength;
				state.spot.length = spotLength;
				state.rectArea.length = rectAreaLength;
				state.point.length = pointLength;
				state.hemi.length = hemiLength;

				state.directionalShadow.length = numDirectionalShadows;
				state.directionalShadowMap.length = numDirectionalShadows;
				state.pointShadow.length = numPointShadows;
				state.pointShadowMap.length = numPointShadows;
				state.spotShadow.length = numSpotShadows;
				state.spotShadowMap.length = numSpotShadows;
				state.directionalShadowMatrix.length = numDirectionalShadows;
				state.pointShadowMatrix.length = numPointShadows;
				state.spotShadowMatrix.length = numSpotShadows;

				hash.directionalLength = directionalLength;
				hash.pointLength = pointLength;
				hash.spotLength = spotLength;
				hash.rectAreaLength = rectAreaLength;
				hash.hemiLength = hemiLength;

				hash.numDirectionalShadows = numDirectionalShadows;
				hash.numPointShadows = numPointShadows;
				hash.numSpotShadows = numSpotShadows;

				state.version = nextVersion ++;

			}

		}

		function setupView( lights, camera ) {

			let directionalLength = 0;
			let pointLength = 0;
			let spotLength = 0;
			let rectAreaLength = 0;
			let hemiLength = 0;

			const viewMatrix = camera.matrixWorldInverse;

			for ( let i = 0, l = lights.length; i < l; i ++ ) {

				const light = lights[ i ];

				if ( light.isDirectionalLight ) {

					const uniforms = state.directional[ directionalLength ];

					uniforms.direction.setFromMatrixPosition( light.matrixWorld );
					vector3.setFromMatrixPosition( light.target.matrixWorld );
					uniforms.direction.sub( vector3 );
					uniforms.direction.transformDirection( viewMatrix );

					directionalLength ++;

				} else if ( light.isSpotLight ) {

					const uniforms = state.spot[ spotLength ];

					uniforms.position.setFromMatrixPosition( light.matrixWorld );
					uniforms.position.applyMatrix4( viewMatrix );

					uniforms.direction.setFromMatrixPosition( light.matrixWorld );
					vector3.setFromMatrixPosition( light.target.matrixWorld );
					uniforms.direction.sub( vector3 );
					uniforms.direction.transformDirection( viewMatrix );

					spotLength ++;

				} else if ( light.isRectAreaLight ) {

					const uniforms = state.rectArea[ rectAreaLength ];

					uniforms.position.setFromMatrixPosition( light.matrixWorld );
					uniforms.position.applyMatrix4( viewMatrix );

					// extract local rotation of light to derive width/height half vectors
					matrix42.identity();
					matrix4.copy( light.matrixWorld );
					matrix4.premultiply( viewMatrix );
					matrix42.extractRotation( matrix4 );

					uniforms.halfWidth.set( light.width * 0.5, 0.0, 0.0 );
					uniforms.halfHeight.set( 0.0, light.height * 0.5, 0.0 );

					uniforms.halfWidth.applyMatrix4( matrix42 );
					uniforms.halfHeight.applyMatrix4( matrix42 );

					rectAreaLength ++;

				} else if ( light.isPointLight ) {

					const uniforms = state.point[ pointLength ];

					uniforms.position.setFromMatrixPosition( light.matrixWorld );
					uniforms.position.applyMatrix4( viewMatrix );

					pointLength ++;

				} else if ( light.isHemisphereLight ) {

					const uniforms = state.hemi[ hemiLength ];

					uniforms.direction.setFromMatrixPosition( light.matrixWorld );
					uniforms.direction.transformDirection( viewMatrix );
					uniforms.direction.normalize();

					hemiLength ++;

				}

			}

		}

		return {
			setup: setup,
			setupView: setupView,
			state: state
		};

	}

	function WebGLRenderState( extensions, capabilities ) {

		const lights = new WebGLLights( extensions, capabilities );

		const lightsArray = [];
		const shadowsArray = [];

		function init() {

			lightsArray.length = 0;
			shadowsArray.length = 0;

		}

		function pushLight( light ) {

			lightsArray.push( light );

		}

		function pushShadow( shadowLight ) {

			shadowsArray.push( shadowLight );

		}

		function setupLights() {

			lights.setup( lightsArray );

		}

		function setupLightsView( camera ) {

			lights.setupView( lightsArray, camera );

		}

		const state = {
			lightsArray: lightsArray,
			shadowsArray: shadowsArray,

			lights: lights
		};

		return {
			init: init,
			state: state,
			setupLights: setupLights,
			setupLightsView: setupLightsView,

			pushLight: pushLight,
			pushShadow: pushShadow
		};

	}

	function WebGLRenderStates( extensions, capabilities ) {

		let renderStates = new WeakMap();

		function get( scene, renderCallDepth = 0 ) {

			let renderState;

			if ( renderStates.has( scene ) === false ) {

				renderState = new WebGLRenderState( extensions, capabilities );
				renderStates.set( scene, [] );
				renderStates.get( scene ).push( renderState );

			} else {

				if ( renderCallDepth >= renderStates.get( scene ).length ) {

					renderState = new WebGLRenderState( extensions, capabilities );
					renderStates.get( scene ).push( renderState );

				} else {

					renderState = renderStates.get( scene )[ renderCallDepth ];

				}

			}

			return renderState;

		}

		function dispose() {

			renderStates = new WeakMap();

		}

		return {
			get: get,
			dispose: dispose
		};

	}

	/**
	 * parameters = {
	 *
	 *  opacity: <float>,
	 *
	 *  map: new THREE.Texture( <Image> ),
	 *
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *
	 *  displacementMap: new THREE.Texture( <Image> ),
	 *  displacementScale: <float>,
	 *  displacementBias: <float>,
	 *
	 *  wireframe: <boolean>,
	 *  wireframeLinewidth: <float>
	 * }
	 */

	function MeshDepthMaterial( parameters ) {

		Material.call( this );

		this.type = 'MeshDepthMaterial';

		this.depthPacking = BasicDepthPacking;

		this.skinning = false;
		this.morphTargets = false;

		this.map = null;

		this.alphaMap = null;

		this.displacementMap = null;
		this.displacementScale = 1;
		this.displacementBias = 0;

		this.wireframe = false;
		this.wireframeLinewidth = 1;

		this.fog = false;

		this.setValues( parameters );

	}

	MeshDepthMaterial.prototype = Object.create( Material.prototype );
	MeshDepthMaterial.prototype.constructor = MeshDepthMaterial;

	MeshDepthMaterial.prototype.isMeshDepthMaterial = true;

	MeshDepthMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.depthPacking = source.depthPacking;

		this.skinning = source.skinning;
		this.morphTargets = source.morphTargets;

		this.map = source.map;

		this.alphaMap = source.alphaMap;

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;

		return this;

	};

	/**
	 * parameters = {
	 *
	 *  referencePosition: <float>,
	 *  nearDistance: <float>,
	 *  farDistance: <float>,
	 *
	 *  skinning: <bool>,
	 *  morphTargets: <bool>,
	 *
	 *  map: new THREE.Texture( <Image> ),
	 *
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *
	 *  displacementMap: new THREE.Texture( <Image> ),
	 *  displacementScale: <float>,
	 *  displacementBias: <float>
	 *
	 * }
	 */

	function MeshDistanceMaterial( parameters ) {

		Material.call( this );

		this.type = 'MeshDistanceMaterial';

		this.referencePosition = new Vector3();
		this.nearDistance = 1;
		this.farDistance = 1000;

		this.skinning = false;
		this.morphTargets = false;

		this.map = null;

		this.alphaMap = null;

		this.displacementMap = null;
		this.displacementScale = 1;
		this.displacementBias = 0;

		this.fog = false;

		this.setValues( parameters );

	}

	MeshDistanceMaterial.prototype = Object.create( Material.prototype );
	MeshDistanceMaterial.prototype.constructor = MeshDistanceMaterial;

	MeshDistanceMaterial.prototype.isMeshDistanceMaterial = true;

	MeshDistanceMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.referencePosition.copy( source.referencePosition );
		this.nearDistance = source.nearDistance;
		this.farDistance = source.farDistance;

		this.skinning = source.skinning;
		this.morphTargets = source.morphTargets;

		this.map = source.map;

		this.alphaMap = source.alphaMap;

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		return this;

	};

	var vsm_frag = "uniform sampler2D shadow_pass;\nuniform vec2 resolution;\nuniform float radius;\n#include <packing>\nvoid main() {\n\tfloat mean = 0.0;\n\tfloat squared_mean = 0.0;\n\tfloat depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy ) / resolution ) );\n\tfor ( float i = -1.0; i < 1.0 ; i += SAMPLE_RATE) {\n\t\t#ifdef HORIZONTAL_PASS\n\t\t\tvec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( i, 0.0 ) * radius ) / resolution ) );\n\t\t\tmean += distribution.x;\n\t\t\tsquared_mean += distribution.y * distribution.y + distribution.x * distribution.x;\n\t\t#else\n\t\t\tfloat depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, i ) * radius ) / resolution ) );\n\t\t\tmean += depth;\n\t\t\tsquared_mean += depth * depth;\n\t\t#endif\n\t}\n\tmean = mean * HALF_SAMPLE_RATE;\n\tsquared_mean = squared_mean * HALF_SAMPLE_RATE;\n\tfloat std_dev = sqrt( squared_mean - mean * mean );\n\tgl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) );\n}";

	var vsm_vert = "void main() {\n\tgl_Position = vec4( position, 1.0 );\n}";

	function WebGLShadowMap( _renderer, _objects, maxTextureSize ) {

		let _frustum = new Frustum();

		const _shadowMapSize = new Vector2(),
			_viewportSize = new Vector2(),

			_viewport = new Vector4(),

			_depthMaterials = [],
			_distanceMaterials = [],

			_materialCache = {};

		const shadowSide = { 0: BackSide, 1: FrontSide, 2: DoubleSide };

		const shadowMaterialVertical = new ShaderMaterial( {

			defines: {
				SAMPLE_RATE: 2.0 / 8.0,
				HALF_SAMPLE_RATE: 1.0 / 8.0
			},

			uniforms: {
				shadow_pass: { value: null },
				resolution: { value: new Vector2() },
				radius: { value: 4.0 }
			},

			vertexShader: vsm_vert,

			fragmentShader: vsm_frag

		} );

		const shadowMaterialHorizontal = shadowMaterialVertical.clone();
		shadowMaterialHorizontal.defines.HORIZONTAL_PASS = 1;

		const fullScreenTri = new BufferGeometry();
		fullScreenTri.setAttribute(
			'position',
			new BufferAttribute(
				new Float32Array( [ - 1, - 1, 0.5, 3, - 1, 0.5, - 1, 3, 0.5 ] ),
				3
			)
		);

		const fullScreenMesh = new Mesh( fullScreenTri, shadowMaterialVertical );

		const scope = this;

		this.enabled = false;

		this.autoUpdate = true;
		this.needsUpdate = false;

		this.type = PCFShadowMap;

		this.render = function ( lights, scene, camera ) {

			if ( scope.enabled === false ) return;
			if ( scope.autoUpdate === false && scope.needsUpdate === false ) return;

			if ( lights.length === 0 ) return;

			const currentRenderTarget = _renderer.getRenderTarget();
			const activeCubeFace = _renderer.getActiveCubeFace();
			const activeMipmapLevel = _renderer.getActiveMipmapLevel();

			const _state = _renderer.state;

			// Set GL state for depth map.
			_state.setBlending( NoBlending );
			_state.buffers.color.setClear( 1, 1, 1, 1 );
			_state.buffers.depth.setTest( true );
			_state.setScissorTest( false );

			// render depth map

			for ( let i = 0, il = lights.length; i < il; i ++ ) {

				const light = lights[ i ];
				const shadow = light.shadow;

				if ( shadow === undefined ) {

					console.warn( 'THREE.WebGLShadowMap:', light, 'has no shadow.' );
					continue;

				}

				if ( shadow.autoUpdate === false && shadow.needsUpdate === false ) continue;

				_shadowMapSize.copy( shadow.mapSize );

				const shadowFrameExtents = shadow.getFrameExtents();

				_shadowMapSize.multiply( shadowFrameExtents );

				_viewportSize.copy( shadow.mapSize );

				if ( _shadowMapSize.x > maxTextureSize || _shadowMapSize.y > maxTextureSize ) {

					if ( _shadowMapSize.x > maxTextureSize ) {

						_viewportSize.x = Math.floor( maxTextureSize / shadowFrameExtents.x );
						_shadowMapSize.x = _viewportSize.x * shadowFrameExtents.x;
						shadow.mapSize.x = _viewportSize.x;

					}

					if ( _shadowMapSize.y > maxTextureSize ) {

						_viewportSize.y = Math.floor( maxTextureSize / shadowFrameExtents.y );
						_shadowMapSize.y = _viewportSize.y * shadowFrameExtents.y;
						shadow.mapSize.y = _viewportSize.y;

					}

				}

				if ( shadow.map === null && ! shadow.isPointLightShadow && this.type === VSMShadowMap ) {

					const pars = { minFilter: LinearFilter, magFilter: LinearFilter, format: RGBAFormat };

					shadow.map = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y, pars );
					shadow.map.texture.name = light.name + '.shadowMap';

					shadow.mapPass = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y, pars );

					shadow.camera.updateProjectionMatrix();

				}

				if ( shadow.map === null ) {

					const pars = { minFilter: NearestFilter, magFilter: NearestFilter, format: RGBAFormat };

					shadow.map = new WebGLRenderTarget( _shadowMapSize.x, _shadowMapSize.y, pars );
					shadow.map.texture.name = light.name + '.shadowMap';

					shadow.camera.updateProjectionMatrix();

				}

				_renderer.setRenderTarget( shadow.map );
				_renderer.clear();

				const viewportCount = shadow.getViewportCount();

				for ( let vp = 0; vp < viewportCount; vp ++ ) {

					const viewport = shadow.getViewport( vp );

					_viewport.set(
						_viewportSize.x * viewport.x,
						_viewportSize.y * viewport.y,
						_viewportSize.x * viewport.z,
						_viewportSize.y * viewport.w
					);

					_state.viewport( _viewport );

					shadow.updateMatrices( light, vp );

					_frustum = shadow.getFrustum();

					renderObject( scene, camera, shadow.camera, light, this.type );

				}

				// do blur pass for VSM

				if ( ! shadow.isPointLightShadow && this.type === VSMShadowMap ) {

					VSMPass( shadow, camera );

				}

				shadow.needsUpdate = false;

			}

			scope.needsUpdate = false;

			_renderer.setRenderTarget( currentRenderTarget, activeCubeFace, activeMipmapLevel );

		};

		function VSMPass( shadow, camera ) {

			const geometry = _objects.update( fullScreenMesh );

			// vertical pass

			shadowMaterialVertical.uniforms.shadow_pass.value = shadow.map.texture;
			shadowMaterialVertical.uniforms.resolution.value = shadow.mapSize;
			shadowMaterialVertical.uniforms.radius.value = shadow.radius;
			_renderer.setRenderTarget( shadow.mapPass );
			_renderer.clear();
			_renderer.renderBufferDirect( camera, null, geometry, shadowMaterialVertical, fullScreenMesh, null );

			// horizontal pass

			shadowMaterialHorizontal.uniforms.shadow_pass.value = shadow.mapPass.texture;
			shadowMaterialHorizontal.uniforms.resolution.value = shadow.mapSize;
			shadowMaterialHorizontal.uniforms.radius.value = shadow.radius;
			_renderer.setRenderTarget( shadow.map );
			_renderer.clear();
			_renderer.renderBufferDirect( camera, null, geometry, shadowMaterialHorizontal, fullScreenMesh, null );

		}

		function getDepthMaterialVariant( useMorphing, useSkinning, useInstancing ) {

			const index = useMorphing << 0 | useSkinning << 1 | useInstancing << 2;

			let material = _depthMaterials[ index ];

			if ( material === undefined ) {

				material = new MeshDepthMaterial( {

					depthPacking: RGBADepthPacking,

					morphTargets: useMorphing,
					skinning: useSkinning

				} );

				_depthMaterials[ index ] = material;

			}

			return material;

		}

		function getDistanceMaterialVariant( useMorphing, useSkinning, useInstancing ) {

			const index = useMorphing << 0 | useSkinning << 1 | useInstancing << 2;

			let material = _distanceMaterials[ index ];

			if ( material === undefined ) {

				material = new MeshDistanceMaterial( {

					morphTargets: useMorphing,
					skinning: useSkinning

				} );

				_distanceMaterials[ index ] = material;

			}

			return material;

		}

		function getDepthMaterial( object, geometry, material, light, shadowCameraNear, shadowCameraFar, type ) {

			let result = null;

			let getMaterialVariant = getDepthMaterialVariant;
			let customMaterial = object.customDepthMaterial;

			if ( light.isPointLight === true ) {

				getMaterialVariant = getDistanceMaterialVariant;
				customMaterial = object.customDistanceMaterial;

			}

			if ( customMaterial === undefined ) {

				let useMorphing = false;

				if ( material.morphTargets === true ) {

					useMorphing = geometry.morphAttributes && geometry.morphAttributes.position && geometry.morphAttributes.position.length > 0;

				}

				let useSkinning = false;

				if ( object.isSkinnedMesh === true ) {

					if ( material.skinning === true ) {

						useSkinning = true;

					} else {

						console.warn( 'THREE.WebGLShadowMap: THREE.SkinnedMesh with material.skinning set to false:', object );

					}

				}

				const useInstancing = object.isInstancedMesh === true;

				result = getMaterialVariant( useMorphing, useSkinning, useInstancing );

			} else {

				result = customMaterial;

			}

			if ( _renderer.localClippingEnabled &&
					material.clipShadows === true &&
					material.clippingPlanes.length !== 0 ) {

				// in this case we need a unique material instance reflecting the
				// appropriate state

				const keyA = result.uuid, keyB = material.uuid;

				let materialsForVariant = _materialCache[ keyA ];

				if ( materialsForVariant === undefined ) {

					materialsForVariant = {};
					_materialCache[ keyA ] = materialsForVariant;

				}

				let cachedMaterial = materialsForVariant[ keyB ];

				if ( cachedMaterial === undefined ) {

					cachedMaterial = result.clone();
					materialsForVariant[ keyB ] = cachedMaterial;

				}

				result = cachedMaterial;

			}

			result.visible = material.visible;
			result.wireframe = material.wireframe;

			if ( type === VSMShadowMap ) {

				result.side = ( material.shadowSide !== null ) ? material.shadowSide : material.side;

			} else {

				result.side = ( material.shadowSide !== null ) ? material.shadowSide : shadowSide[ material.side ];

			}

			result.clipShadows = material.clipShadows;
			result.clippingPlanes = material.clippingPlanes;
			result.clipIntersection = material.clipIntersection;

			result.wireframeLinewidth = material.wireframeLinewidth;
			result.linewidth = material.linewidth;

			if ( light.isPointLight === true && result.isMeshDistanceMaterial === true ) {

				result.referencePosition.setFromMatrixPosition( light.matrixWorld );
				result.nearDistance = shadowCameraNear;
				result.farDistance = shadowCameraFar;

			}

			return result;

		}

		function renderObject( object, camera, shadowCamera, light, type ) {

			if ( object.visible === false ) return;

			const visible = object.layers.test( camera.layers );

			if ( visible && ( object.isMesh || object.isLine || object.isPoints ) ) {

				if ( ( object.castShadow || ( object.receiveShadow && type === VSMShadowMap ) ) && ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) ) {

					object.modelViewMatrix.multiplyMatrices( shadowCamera.matrixWorldInverse, object.matrixWorld );

					const geometry = _objects.update( object );
					const material = object.material;

					if ( Array.isArray( material ) ) {

						const groups = geometry.groups;

						for ( let k = 0, kl = groups.length; k < kl; k ++ ) {

							const group = groups[ k ];
							const groupMaterial = material[ group.materialIndex ];

							if ( groupMaterial && groupMaterial.visible ) {

								const depthMaterial = getDepthMaterial( object, geometry, groupMaterial, light, shadowCamera.near, shadowCamera.far, type );

								_renderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, group );

							}

						}

					} else if ( material.visible ) {

						const depthMaterial = getDepthMaterial( object, geometry, material, light, shadowCamera.near, shadowCamera.far, type );

						_renderer.renderBufferDirect( shadowCamera, null, geometry, depthMaterial, object, null );

					}

				}

			}

			const children = object.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				renderObject( children[ i ], camera, shadowCamera, light, type );

			}

		}

	}

	function WebGLState( gl, extensions, capabilities ) {

		const isWebGL2 = capabilities.isWebGL2;

		function ColorBuffer() {

			let locked = false;

			const color = new Vector4();
			let currentColorMask = null;
			const currentColorClear = new Vector4( 0, 0, 0, 0 );

			return {

				setMask: function ( colorMask ) {

					if ( currentColorMask !== colorMask && ! locked ) {

						gl.colorMask( colorMask, colorMask, colorMask, colorMask );
						currentColorMask = colorMask;

					}

				},

				setLocked: function ( lock ) {

					locked = lock;

				},

				setClear: function ( r, g, b, a, premultipliedAlpha ) {

					if ( premultipliedAlpha === true ) {

						r *= a; g *= a; b *= a;

					}

					color.set( r, g, b, a );

					if ( currentColorClear.equals( color ) === false ) {

						gl.clearColor( r, g, b, a );
						currentColorClear.copy( color );

					}

				},

				reset: function () {

					locked = false;

					currentColorMask = null;
					currentColorClear.set( - 1, 0, 0, 0 ); // set to invalid state

				}

			};

		}

		function DepthBuffer() {

			let locked = false;

			let currentDepthMask = null;
			let currentDepthFunc = null;
			let currentDepthClear = null;

			return {

				setTest: function ( depthTest ) {

					if ( depthTest ) {

						enable( 2929 );

					} else {

						disable( 2929 );

					}

				},

				setMask: function ( depthMask ) {

					if ( currentDepthMask !== depthMask && ! locked ) {

						gl.depthMask( depthMask );
						currentDepthMask = depthMask;

					}

				},

				setFunc: function ( depthFunc ) {

					if ( currentDepthFunc !== depthFunc ) {

						if ( depthFunc ) {

							switch ( depthFunc ) {

								case NeverDepth:

									gl.depthFunc( 512 );
									break;

								case AlwaysDepth:

									gl.depthFunc( 519 );
									break;

								case LessDepth:

									gl.depthFunc( 513 );
									break;

								case LessEqualDepth:

									gl.depthFunc( 515 );
									break;

								case EqualDepth:

									gl.depthFunc( 514 );
									break;

								case GreaterEqualDepth:

									gl.depthFunc( 518 );
									break;

								case GreaterDepth:

									gl.depthFunc( 516 );
									break;

								case NotEqualDepth:

									gl.depthFunc( 517 );
									break;

								default:

									gl.depthFunc( 515 );

							}

						} else {

							gl.depthFunc( 515 );

						}

						currentDepthFunc = depthFunc;

					}

				},

				setLocked: function ( lock ) {

					locked = lock;

				},

				setClear: function ( depth ) {

					if ( currentDepthClear !== depth ) {

						gl.clearDepth( depth );
						currentDepthClear = depth;

					}

				},

				reset: function () {

					locked = false;

					currentDepthMask = null;
					currentDepthFunc = null;
					currentDepthClear = null;

				}

			};

		}

		function StencilBuffer() {

			let locked = false;

			let currentStencilMask = null;
			let currentStencilFunc = null;
			let currentStencilRef = null;
			let currentStencilFuncMask = null;
			let currentStencilFail = null;
			let currentStencilZFail = null;
			let currentStencilZPass = null;
			let currentStencilClear = null;

			return {

				setTest: function ( stencilTest ) {

					if ( ! locked ) {

						if ( stencilTest ) {

							enable( 2960 );

						} else {

							disable( 2960 );

						}

					}

				},

				setMask: function ( stencilMask ) {

					if ( currentStencilMask !== stencilMask && ! locked ) {

						gl.stencilMask( stencilMask );
						currentStencilMask = stencilMask;

					}

				},

				setFunc: function ( stencilFunc, stencilRef, stencilMask ) {

					if ( currentStencilFunc !== stencilFunc ||
					     currentStencilRef !== stencilRef ||
					     currentStencilFuncMask !== stencilMask ) {

						gl.stencilFunc( stencilFunc, stencilRef, stencilMask );

						currentStencilFunc = stencilFunc;
						currentStencilRef = stencilRef;
						currentStencilFuncMask = stencilMask;

					}

				},

				setOp: function ( stencilFail, stencilZFail, stencilZPass ) {

					if ( currentStencilFail !== stencilFail ||
					     currentStencilZFail !== stencilZFail ||
					     currentStencilZPass !== stencilZPass ) {

						gl.stencilOp( stencilFail, stencilZFail, stencilZPass );

						currentStencilFail = stencilFail;
						currentStencilZFail = stencilZFail;
						currentStencilZPass = stencilZPass;

					}

				},

				setLocked: function ( lock ) {

					locked = lock;

				},

				setClear: function ( stencil ) {

					if ( currentStencilClear !== stencil ) {

						gl.clearStencil( stencil );
						currentStencilClear = stencil;

					}

				},

				reset: function () {

					locked = false;

					currentStencilMask = null;
					currentStencilFunc = null;
					currentStencilRef = null;
					currentStencilFuncMask = null;
					currentStencilFail = null;
					currentStencilZFail = null;
					currentStencilZPass = null;
					currentStencilClear = null;

				}

			};

		}

		//

		const colorBuffer = new ColorBuffer();
		const depthBuffer = new DepthBuffer();
		const stencilBuffer = new StencilBuffer();

		let enabledCapabilities = {};

		let currentProgram = null;

		let currentBlendingEnabled = null;
		let currentBlending = null;
		let currentBlendEquation = null;
		let currentBlendSrc = null;
		let currentBlendDst = null;
		let currentBlendEquationAlpha = null;
		let currentBlendSrcAlpha = null;
		let currentBlendDstAlpha = null;
		let currentPremultipledAlpha = false;

		let currentFlipSided = null;
		let currentCullFace = null;

		let currentLineWidth = null;

		let currentPolygonOffsetFactor = null;
		let currentPolygonOffsetUnits = null;

		const maxTextures = gl.getParameter( 35661 );

		let lineWidthAvailable = false;
		let version = 0;
		const glVersion = gl.getParameter( 7938 );

		if ( glVersion.indexOf( 'WebGL' ) !== - 1 ) {

			version = parseFloat( /^WebGL (\d)/.exec( glVersion )[ 1 ] );
			lineWidthAvailable = ( version >= 1.0 );

		} else if ( glVersion.indexOf( 'OpenGL ES' ) !== - 1 ) {

			version = parseFloat( /^OpenGL ES (\d)/.exec( glVersion )[ 1 ] );
			lineWidthAvailable = ( version >= 2.0 );

		}

		let currentTextureSlot = null;
		let currentBoundTextures = {};

		const currentScissor = new Vector4();
		const currentViewport = new Vector4();

		function createTexture( type, target, count ) {

			const data = new Uint8Array( 4 ); // 4 is required to match default unpack alignment of 4.
			const texture = gl.createTexture();

			gl.bindTexture( type, texture );
			gl.texParameteri( type, 10241, 9728 );
			gl.texParameteri( type, 10240, 9728 );

			for ( let i = 0; i < count; i ++ ) {

				gl.texImage2D( target + i, 0, 6408, 1, 1, 0, 6408, 5121, data );

			}

			return texture;

		}

		const emptyTextures = {};
		emptyTextures[ 3553 ] = createTexture( 3553, 3553, 1 );
		emptyTextures[ 34067 ] = createTexture( 34067, 34069, 6 );

		// init

		colorBuffer.setClear( 0, 0, 0, 1 );
		depthBuffer.setClear( 1 );
		stencilBuffer.setClear( 0 );

		enable( 2929 );
		depthBuffer.setFunc( LessEqualDepth );

		setFlipSided( false );
		setCullFace( CullFaceBack );
		enable( 2884 );

		setBlending( NoBlending );

		//

		function enable( id ) {

			if ( enabledCapabilities[ id ] !== true ) {

				gl.enable( id );
				enabledCapabilities[ id ] = true;

			}

		}

		function disable( id ) {

			if ( enabledCapabilities[ id ] !== false ) {

				gl.disable( id );
				enabledCapabilities[ id ] = false;

			}

		}

		function useProgram( program ) {

			if ( currentProgram !== program ) {

				gl.useProgram( program );

				currentProgram = program;

				return true;

			}

			return false;

		}

		const equationToGL = {
			[ AddEquation ]: 32774,
			[ SubtractEquation ]: 32778,
			[ ReverseSubtractEquation ]: 32779
		};

		if ( isWebGL2 ) {

			equationToGL[ MinEquation ] = 32775;
			equationToGL[ MaxEquation ] = 32776;

		} else {

			const extension = extensions.get( 'EXT_blend_minmax' );

			if ( extension !== null ) {

				equationToGL[ MinEquation ] = extension.MIN_EXT;
				equationToGL[ MaxEquation ] = extension.MAX_EXT;

			}

		}

		const factorToGL = {
			[ ZeroFactor ]: 0,
			[ OneFactor ]: 1,
			[ SrcColorFactor ]: 768,
			[ SrcAlphaFactor ]: 770,
			[ SrcAlphaSaturateFactor ]: 776,
			[ DstColorFactor ]: 774,
			[ DstAlphaFactor ]: 772,
			[ OneMinusSrcColorFactor ]: 769,
			[ OneMinusSrcAlphaFactor ]: 771,
			[ OneMinusDstColorFactor ]: 775,
			[ OneMinusDstAlphaFactor ]: 773
		};

		function setBlending( blending, blendEquation, blendSrc, blendDst, blendEquationAlpha, blendSrcAlpha, blendDstAlpha, premultipliedAlpha ) {

			if ( blending === NoBlending ) {

				if ( currentBlendingEnabled ) {

					disable( 3042 );
					currentBlendingEnabled = false;

				}

				return;

			}

			if ( ! currentBlendingEnabled ) {

				enable( 3042 );
				currentBlendingEnabled = true;

			}

			if ( blending !== CustomBlending ) {

				if ( blending !== currentBlending || premultipliedAlpha !== currentPremultipledAlpha ) {

					if ( currentBlendEquation !== AddEquation || currentBlendEquationAlpha !== AddEquation ) {

						gl.blendEquation( 32774 );

						currentBlendEquation = AddEquation;
						currentBlendEquationAlpha = AddEquation;

					}

					if ( premultipliedAlpha ) {

						switch ( blending ) {

							case NormalBlending:
								gl.blendFuncSeparate( 1, 771, 1, 771 );
								break;

							case AdditiveBlending:
								gl.blendFunc( 1, 1 );
								break;

							case SubtractiveBlending:
								gl.blendFuncSeparate( 0, 0, 769, 771 );
								break;

							case MultiplyBlending:
								gl.blendFuncSeparate( 0, 768, 0, 770 );
								break;

							default:
								console.error( 'THREE.WebGLState: Invalid blending: ', blending );
								break;

						}

					} else {

						switch ( blending ) {

							case NormalBlending:
								gl.blendFuncSeparate( 770, 771, 1, 771 );
								break;

							case AdditiveBlending:
								gl.blendFunc( 770, 1 );
								break;

							case SubtractiveBlending:
								gl.blendFunc( 0, 769 );
								break;

							case MultiplyBlending:
								gl.blendFunc( 0, 768 );
								break;

							default:
								console.error( 'THREE.WebGLState: Invalid blending: ', blending );
								break;

						}

					}

					currentBlendSrc = null;
					currentBlendDst = null;
					currentBlendSrcAlpha = null;
					currentBlendDstAlpha = null;

					currentBlending = blending;
					currentPremultipledAlpha = premultipliedAlpha;

				}

				return;

			}

			// custom blending

			blendEquationAlpha = blendEquationAlpha || blendEquation;
			blendSrcAlpha = blendSrcAlpha || blendSrc;
			blendDstAlpha = blendDstAlpha || blendDst;

			if ( blendEquation !== currentBlendEquation || blendEquationAlpha !== currentBlendEquationAlpha ) {

				gl.blendEquationSeparate( equationToGL[ blendEquation ], equationToGL[ blendEquationAlpha ] );

				currentBlendEquation = blendEquation;
				currentBlendEquationAlpha = blendEquationAlpha;

			}

			if ( blendSrc !== currentBlendSrc || blendDst !== currentBlendDst || blendSrcAlpha !== currentBlendSrcAlpha || blendDstAlpha !== currentBlendDstAlpha ) {

				gl.blendFuncSeparate( factorToGL[ blendSrc ], factorToGL[ blendDst ], factorToGL[ blendSrcAlpha ], factorToGL[ blendDstAlpha ] );

				currentBlendSrc = blendSrc;
				currentBlendDst = blendDst;
				currentBlendSrcAlpha = blendSrcAlpha;
				currentBlendDstAlpha = blendDstAlpha;

			}

			currentBlending = blending;
			currentPremultipledAlpha = null;

		}

		function setMaterial( material, frontFaceCW ) {

			material.side === DoubleSide
				? disable( 2884 )
				: enable( 2884 );

			let flipSided = ( material.side === BackSide );
			if ( frontFaceCW ) flipSided = ! flipSided;

			setFlipSided( flipSided );

			( material.blending === NormalBlending && material.transparent === false )
				? setBlending( NoBlending )
				: setBlending( material.blending, material.blendEquation, material.blendSrc, material.blendDst, material.blendEquationAlpha, material.blendSrcAlpha, material.blendDstAlpha, material.premultipliedAlpha );

			depthBuffer.setFunc( material.depthFunc );
			depthBuffer.setTest( material.depthTest );
			depthBuffer.setMask( material.depthWrite );
			colorBuffer.setMask( material.colorWrite );

			const stencilWrite = material.stencilWrite;
			stencilBuffer.setTest( stencilWrite );
			if ( stencilWrite ) {

				stencilBuffer.setMask( material.stencilWriteMask );
				stencilBuffer.setFunc( material.stencilFunc, material.stencilRef, material.stencilFuncMask );
				stencilBuffer.setOp( material.stencilFail, material.stencilZFail, material.stencilZPass );

			}

			setPolygonOffset( material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits );

		}

		//

		function setFlipSided( flipSided ) {

			if ( currentFlipSided !== flipSided ) {

				if ( flipSided ) {

					gl.frontFace( 2304 );

				} else {

					gl.frontFace( 2305 );

				}

				currentFlipSided = flipSided;

			}

		}

		function setCullFace( cullFace ) {

			if ( cullFace !== CullFaceNone ) {

				enable( 2884 );

				if ( cullFace !== currentCullFace ) {

					if ( cullFace === CullFaceBack ) {

						gl.cullFace( 1029 );

					} else if ( cullFace === CullFaceFront ) {

						gl.cullFace( 1028 );

					} else {

						gl.cullFace( 1032 );

					}

				}

			} else {

				disable( 2884 );

			}

			currentCullFace = cullFace;

		}

		function setLineWidth( width ) {

			if ( width !== currentLineWidth ) {

				if ( lineWidthAvailable ) gl.lineWidth( width );

				currentLineWidth = width;

			}

		}

		function setPolygonOffset( polygonOffset, factor, units ) {

			if ( polygonOffset ) {

				enable( 32823 );

				if ( currentPolygonOffsetFactor !== factor || currentPolygonOffsetUnits !== units ) {

					gl.polygonOffset( factor, units );

					currentPolygonOffsetFactor = factor;
					currentPolygonOffsetUnits = units;

				}

			} else {

				disable( 32823 );

			}

		}

		function setScissorTest( scissorTest ) {

			if ( scissorTest ) {

				enable( 3089 );

			} else {

				disable( 3089 );

			}

		}

		// texture

		function activeTexture( webglSlot ) {

			if ( webglSlot === undefined ) webglSlot = 33984 + maxTextures - 1;

			if ( currentTextureSlot !== webglSlot ) {

				gl.activeTexture( webglSlot );
				currentTextureSlot = webglSlot;

			}

		}

		function bindTexture( webglType, webglTexture ) {

			if ( currentTextureSlot === null ) {

				activeTexture();

			}

			let boundTexture = currentBoundTextures[ currentTextureSlot ];

			if ( boundTexture === undefined ) {

				boundTexture = { type: undefined, texture: undefined };
				currentBoundTextures[ currentTextureSlot ] = boundTexture;

			}

			if ( boundTexture.type !== webglType || boundTexture.texture !== webglTexture ) {

				gl.bindTexture( webglType, webglTexture || emptyTextures[ webglType ] );

				boundTexture.type = webglType;
				boundTexture.texture = webglTexture;

			}

		}

		function unbindTexture() {

			const boundTexture = currentBoundTextures[ currentTextureSlot ];

			if ( boundTexture !== undefined && boundTexture.type !== undefined ) {

				gl.bindTexture( boundTexture.type, null );

				boundTexture.type = undefined;
				boundTexture.texture = undefined;

			}

		}

		function compressedTexImage2D() {

			try {

				gl.compressedTexImage2D.apply( gl, arguments );

			} catch ( error ) {

				console.error( 'THREE.WebGLState:', error );

			}

		}

		function texImage2D() {

			try {

				gl.texImage2D.apply( gl, arguments );

			} catch ( error ) {

				console.error( 'THREE.WebGLState:', error );

			}

		}

		function texImage3D() {

			try {

				gl.texImage3D.apply( gl, arguments );

			} catch ( error ) {

				console.error( 'THREE.WebGLState:', error );

			}

		}

		//

		function scissor( scissor ) {

			if ( currentScissor.equals( scissor ) === false ) {

				gl.scissor( scissor.x, scissor.y, scissor.z, scissor.w );
				currentScissor.copy( scissor );

			}

		}

		function viewport( viewport ) {

			if ( currentViewport.equals( viewport ) === false ) {

				gl.viewport( viewport.x, viewport.y, viewport.z, viewport.w );
				currentViewport.copy( viewport );

			}

		}

		//

		function reset() {

			enabledCapabilities = {};

			currentTextureSlot = null;
			currentBoundTextures = {};

			currentProgram = null;

			currentBlendingEnabled = null;
			currentBlending = null;
			currentBlendEquation = null;
			currentBlendSrc = null;
			currentBlendDst = null;
			currentBlendEquationAlpha = null;
			currentBlendSrcAlpha = null;
			currentBlendDstAlpha = null;
			currentPremultipledAlpha = false;

			currentFlipSided = null;
			currentCullFace = null;

			currentLineWidth = null;

			currentPolygonOffsetFactor = null;
			currentPolygonOffsetUnits = null;

			colorBuffer.reset();
			depthBuffer.reset();
			stencilBuffer.reset();

		}

		return {

			buffers: {
				color: colorBuffer,
				depth: depthBuffer,
				stencil: stencilBuffer
			},

			enable: enable,
			disable: disable,

			useProgram: useProgram,

			setBlending: setBlending,
			setMaterial: setMaterial,

			setFlipSided: setFlipSided,
			setCullFace: setCullFace,

			setLineWidth: setLineWidth,
			setPolygonOffset: setPolygonOffset,

			setScissorTest: setScissorTest,

			activeTexture: activeTexture,
			bindTexture: bindTexture,
			unbindTexture: unbindTexture,
			compressedTexImage2D: compressedTexImage2D,
			texImage2D: texImage2D,
			texImage3D: texImage3D,

			scissor: scissor,
			viewport: viewport,

			reset: reset

		};

	}

	function WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info ) {

		const isWebGL2 = capabilities.isWebGL2;
		const maxTextures = capabilities.maxTextures;
		const maxCubemapSize = capabilities.maxCubemapSize;
		const maxTextureSize = capabilities.maxTextureSize;
		const maxSamples = capabilities.maxSamples;

		const _videoTextures = new WeakMap();
		let _canvas;

		// cordova iOS (as of 5.0) still uses UIWebView, which provides OffscreenCanvas,
		// also OffscreenCanvas.getContext("webgl"), but not OffscreenCanvas.getContext("2d")!
		// Some implementations may only implement OffscreenCanvas partially (e.g. lacking 2d).

		let useOffscreenCanvas = false;

		try {

			useOffscreenCanvas = typeof OffscreenCanvas !== 'undefined'
				&& ( new OffscreenCanvas( 1, 1 ).getContext( '2d' ) ) !== null;

		} catch ( err ) {

			// Ignore any errors

		}

		function createCanvas( width, height ) {

			// Use OffscreenCanvas when available. Specially needed in web workers

			return useOffscreenCanvas ?
				new OffscreenCanvas( width, height ) :
				document.createElementNS( 'http://www.w3.org/1999/xhtml', 'canvas' );

		}

		function resizeImage( image, needsPowerOfTwo, needsNewCanvas, maxSize ) {

			let scale = 1;

			// handle case if texture exceeds max size

			if ( image.width > maxSize || image.height > maxSize ) {

				scale = maxSize / Math.max( image.width, image.height );

			}

			// only perform resize if necessary

			if ( scale < 1 || needsPowerOfTwo === true ) {

				// only perform resize for certain image types

				if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
					( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
					( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {

					const floor = needsPowerOfTwo ? MathUtils.floorPowerOfTwo : Math.floor;

					const width = floor( scale * image.width );
					const height = floor( scale * image.height );

					if ( _canvas === undefined ) _canvas = createCanvas( width, height );

					// cube textures can't reuse the same canvas

					const canvas = needsNewCanvas ? createCanvas( width, height ) : _canvas;

					canvas.width = width;
					canvas.height = height;

					const context = canvas.getContext( '2d' );
					context.drawImage( image, 0, 0, width, height );

					console.warn( 'THREE.WebGLRenderer: Texture has been resized from (' + image.width + 'x' + image.height + ') to (' + width + 'x' + height + ').' );

					return canvas;

				} else {

					if ( 'data' in image ) {

						console.warn( 'THREE.WebGLRenderer: Image in DataTexture is too big (' + image.width + 'x' + image.height + ').' );

					}

					return image;

				}

			}

			return image;

		}

		function isPowerOfTwo( image ) {

			return MathUtils.isPowerOfTwo( image.width ) && MathUtils.isPowerOfTwo( image.height );

		}

		function textureNeedsPowerOfTwo( texture ) {

			if ( isWebGL2 ) return false;

			return ( texture.wrapS !== ClampToEdgeWrapping || texture.wrapT !== ClampToEdgeWrapping ) ||
				( texture.minFilter !== NearestFilter && texture.minFilter !== LinearFilter );

		}

		function textureNeedsGenerateMipmaps( texture, supportsMips ) {

			return texture.generateMipmaps && supportsMips &&
				texture.minFilter !== NearestFilter && texture.minFilter !== LinearFilter;

		}

		function generateMipmap( target, texture, width, height ) {

			_gl.generateMipmap( target );

			const textureProperties = properties.get( texture );

			// Note: Math.log( x ) * Math.LOG2E used instead of Math.log2( x ) which is not supported by IE11
			textureProperties.__maxMipLevel = Math.log( Math.max( width, height ) ) * Math.LOG2E;

		}

		function getInternalFormat( internalFormatName, glFormat, glType ) {

			if ( isWebGL2 === false ) return glFormat;

			if ( internalFormatName !== null ) {

				if ( _gl[ internalFormatName ] !== undefined ) return _gl[ internalFormatName ];

				console.warn( 'THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format \'' + internalFormatName + '\'' );

			}

			let internalFormat = glFormat;

			if ( glFormat === 6403 ) {

				if ( glType === 5126 ) internalFormat = 33326;
				if ( glType === 5131 ) internalFormat = 33325;
				if ( glType === 5121 ) internalFormat = 33321;

			}

			if ( glFormat === 6407 ) {

				if ( glType === 5126 ) internalFormat = 34837;
				if ( glType === 5131 ) internalFormat = 34843;
				if ( glType === 5121 ) internalFormat = 32849;

			}

			if ( glFormat === 6408 ) {

				if ( glType === 5126 ) internalFormat = 34836;
				if ( glType === 5131 ) internalFormat = 34842;
				if ( glType === 5121 ) internalFormat = 32856;

			}

			if ( internalFormat === 33325 || internalFormat === 33326 ||
				internalFormat === 34842 || internalFormat === 34836 ) {

				extensions.get( 'EXT_color_buffer_float' );

			}

			return internalFormat;

		}

		// Fallback filters for non-power-of-2 textures

		function filterFallback( f ) {

			if ( f === NearestFilter || f === NearestMipmapNearestFilter || f === NearestMipmapLinearFilter ) {

				return 9728;

			}

			return 9729;

		}

		//

		function onTextureDispose( event ) {

			const texture = event.target;

			texture.removeEventListener( 'dispose', onTextureDispose );

			deallocateTexture( texture );

			if ( texture.isVideoTexture ) {

				_videoTextures.delete( texture );

			}

			info.memory.textures --;

		}

		function onRenderTargetDispose( event ) {

			const renderTarget = event.target;

			renderTarget.removeEventListener( 'dispose', onRenderTargetDispose );

			deallocateRenderTarget( renderTarget );

			info.memory.textures --;

		}

		//

		function deallocateTexture( texture ) {

			const textureProperties = properties.get( texture );

			if ( textureProperties.__webglInit === undefined ) return;

			_gl.deleteTexture( textureProperties.__webglTexture );

			properties.remove( texture );

		}

		function deallocateRenderTarget( renderTarget ) {

			const renderTargetProperties = properties.get( renderTarget );
			const textureProperties = properties.get( renderTarget.texture );

			if ( ! renderTarget ) return;

			if ( textureProperties.__webglTexture !== undefined ) {

				_gl.deleteTexture( textureProperties.__webglTexture );

			}

			if ( renderTarget.depthTexture ) {

				renderTarget.depthTexture.dispose();

			}

			if ( renderTarget.isWebGLCubeRenderTarget ) {

				for ( let i = 0; i < 6; i ++ ) {

					_gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer[ i ] );
					if ( renderTargetProperties.__webglDepthbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglDepthbuffer[ i ] );

				}

			} else {

				_gl.deleteFramebuffer( renderTargetProperties.__webglFramebuffer );
				if ( renderTargetProperties.__webglDepthbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglDepthbuffer );
				if ( renderTargetProperties.__webglMultisampledFramebuffer ) _gl.deleteFramebuffer( renderTargetProperties.__webglMultisampledFramebuffer );
				if ( renderTargetProperties.__webglColorRenderbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglColorRenderbuffer );
				if ( renderTargetProperties.__webglDepthRenderbuffer ) _gl.deleteRenderbuffer( renderTargetProperties.__webglDepthRenderbuffer );

			}

			properties.remove( renderTarget.texture );
			properties.remove( renderTarget );

		}

		//

		let textureUnits = 0;

		function resetTextureUnits() {

			textureUnits = 0;

		}

		function allocateTextureUnit() {

			const textureUnit = textureUnits;

			if ( textureUnit >= maxTextures ) {

				console.warn( 'THREE.WebGLTextures: Trying to use ' + textureUnit + ' texture units while this GPU supports only ' + maxTextures );

			}

			textureUnits += 1;

			return textureUnit;

		}

		//

		function setTexture2D( texture, slot ) {

			const textureProperties = properties.get( texture );

			if ( texture.isVideoTexture ) updateVideoTexture( texture );

			if ( texture.version > 0 && textureProperties.__version !== texture.version ) {

				const image = texture.image;

				if ( image === undefined ) {

					console.warn( 'THREE.WebGLRenderer: Texture marked for update but image is undefined' );

				} else if ( image.complete === false ) {

					console.warn( 'THREE.WebGLRenderer: Texture marked for update but image is incomplete' );

				} else {

					uploadTexture( textureProperties, texture, slot );
					return;

				}

			}

			state.activeTexture( 33984 + slot );
			state.bindTexture( 3553, textureProperties.__webglTexture );

		}

		function setTexture2DArray( texture, slot ) {

			const textureProperties = properties.get( texture );

			if ( texture.version > 0 && textureProperties.__version !== texture.version ) {

				uploadTexture( textureProperties, texture, slot );
				return;

			}

			state.activeTexture( 33984 + slot );
			state.bindTexture( 35866, textureProperties.__webglTexture );

		}

		function setTexture3D( texture, slot ) {

			const textureProperties = properties.get( texture );

			if ( texture.version > 0 && textureProperties.__version !== texture.version ) {

				uploadTexture( textureProperties, texture, slot );
				return;

			}

			state.activeTexture( 33984 + slot );
			state.bindTexture( 32879, textureProperties.__webglTexture );

		}

		function setTextureCube( texture, slot ) {

			const textureProperties = properties.get( texture );

			if ( texture.version > 0 && textureProperties.__version !== texture.version ) {

				uploadCubeTexture( textureProperties, texture, slot );
				return;

			}

			state.activeTexture( 33984 + slot );
			state.bindTexture( 34067, textureProperties.__webglTexture );

		}

		const wrappingToGL = {
			[ RepeatWrapping ]: 10497,
			[ ClampToEdgeWrapping ]: 33071,
			[ MirroredRepeatWrapping ]: 33648
		};

		const filterToGL = {
			[ NearestFilter ]: 9728,
			[ NearestMipmapNearestFilter ]: 9984,
			[ NearestMipmapLinearFilter ]: 9986,

			[ LinearFilter ]: 9729,
			[ LinearMipmapNearestFilter ]: 9985,
			[ LinearMipmapLinearFilter ]: 9987
		};

		function setTextureParameters( textureType, texture, supportsMips ) {

			if ( supportsMips ) {

				_gl.texParameteri( textureType, 10242, wrappingToGL[ texture.wrapS ] );
				_gl.texParameteri( textureType, 10243, wrappingToGL[ texture.wrapT ] );

				if ( textureType === 32879 || textureType === 35866 ) {

					_gl.texParameteri( textureType, 32882, wrappingToGL[ texture.wrapR ] );

				}

				_gl.texParameteri( textureType, 10240, filterToGL[ texture.magFilter ] );
				_gl.texParameteri( textureType, 10241, filterToGL[ texture.minFilter ] );

			} else {

				_gl.texParameteri( textureType, 10242, 33071 );
				_gl.texParameteri( textureType, 10243, 33071 );

				if ( textureType === 32879 || textureType === 35866 ) {

					_gl.texParameteri( textureType, 32882, 33071 );

				}

				if ( texture.wrapS !== ClampToEdgeWrapping || texture.wrapT !== ClampToEdgeWrapping ) {

					console.warn( 'THREE.WebGLRenderer: Texture is not power of two. Texture.wrapS and Texture.wrapT should be set to THREE.ClampToEdgeWrapping.' );

				}

				_gl.texParameteri( textureType, 10240, filterFallback( texture.magFilter ) );
				_gl.texParameteri( textureType, 10241, filterFallback( texture.minFilter ) );

				if ( texture.minFilter !== NearestFilter && texture.minFilter !== LinearFilter ) {

					console.warn( 'THREE.WebGLRenderer: Texture is not power of two. Texture.minFilter should be set to THREE.NearestFilter or THREE.LinearFilter.' );

				}

			}

			const extension = extensions.get( 'EXT_texture_filter_anisotropic' );

			if ( extension ) {

				if ( texture.type === FloatType && extensions.get( 'OES_texture_float_linear' ) === null ) return;
				if ( texture.type === HalfFloatType && ( isWebGL2 || extensions.get( 'OES_texture_half_float_linear' ) ) === null ) return;

				if ( texture.anisotropy > 1 || properties.get( texture ).__currentAnisotropy ) {

					_gl.texParameterf( textureType, extension.TEXTURE_MAX_ANISOTROPY_EXT, Math.min( texture.anisotropy, capabilities.getMaxAnisotropy() ) );
					properties.get( texture ).__currentAnisotropy = texture.anisotropy;

				}

			}

		}

		function initTexture( textureProperties, texture ) {

			if ( textureProperties.__webglInit === undefined ) {

				textureProperties.__webglInit = true;

				texture.addEventListener( 'dispose', onTextureDispose );

				textureProperties.__webglTexture = _gl.createTexture();

				info.memory.textures ++;

			}

		}

		function uploadTexture( textureProperties, texture, slot ) {

			let textureType = 3553;

			if ( texture.isDataTexture2DArray ) textureType = 35866;
			if ( texture.isDataTexture3D ) textureType = 32879;

			initTexture( textureProperties, texture );

			state.activeTexture( 33984 + slot );
			state.bindTexture( textureType, textureProperties.__webglTexture );

			_gl.pixelStorei( 37440, texture.flipY );
			_gl.pixelStorei( 37441, texture.premultiplyAlpha );
			_gl.pixelStorei( 3317, texture.unpackAlignment );

			const needsPowerOfTwo = textureNeedsPowerOfTwo( texture ) && isPowerOfTwo( texture.image ) === false;
			const image = resizeImage( texture.image, needsPowerOfTwo, false, maxTextureSize );

			const supportsMips = isPowerOfTwo( image ) || isWebGL2,
				glFormat = utils.convert( texture.format );

			let glType = utils.convert( texture.type ),
				glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType );

			setTextureParameters( textureType, texture, supportsMips );

			let mipmap;
			const mipmaps = texture.mipmaps;

			if ( texture.isDepthTexture ) {

				// populate depth texture with dummy data

				glInternalFormat = 6402;

				if ( isWebGL2 ) {

					if ( texture.type === FloatType ) {

						glInternalFormat = 36012;

					} else if ( texture.type === UnsignedIntType ) {

						glInternalFormat = 33190;

					} else if ( texture.type === UnsignedInt248Type$1 ) {

						glInternalFormat = 35056;

					} else {

						glInternalFormat = 33189; // WebGL2 requires sized internalformat for glTexImage2D

					}

				} else {

					if ( texture.type === FloatType ) {

						console.error( 'WebGLRenderer: Floating point depth texture requires WebGL2.' );

					}

				}

				// validation checks for WebGL 1

				if ( texture.format === DepthFormat && glInternalFormat === 6402 ) {

					// The error INVALID_OPERATION is generated by texImage2D if format and internalformat are
					// DEPTH_COMPONENT and type is not UNSIGNED_SHORT or UNSIGNED_INT
					// (https://www.khronos.org/registry/webgl/extensions/WEBGL_depth_texture/)
					if ( texture.type !== UnsignedShortType && texture.type !== UnsignedIntType ) {

						console.warn( 'THREE.WebGLRenderer: Use UnsignedShortType or UnsignedIntType for DepthFormat DepthTexture.' );

						texture.type = UnsignedShortType;
						glType = utils.convert( texture.type );

					}

				}

				if ( texture.format === DepthStencilFormat && glInternalFormat === 6402 ) {

					// Depth stencil textures need the DEPTH_STENCIL internal format
					// (https://www.khronos.org/registry/webgl/extensions/WEBGL_depth_texture/)
					glInternalFormat = 34041;

					// The error INVALID_OPERATION is generated by texImage2D if format and internalformat are
					// DEPTH_STENCIL and type is not UNSIGNED_INT_24_8_WEBGL.
					// (https://www.khronos.org/registry/webgl/extensions/WEBGL_depth_texture/)
					if ( texture.type !== UnsignedInt248Type$1 ) {

						console.warn( 'THREE.WebGLRenderer: Use UnsignedInt248Type for DepthStencilFormat DepthTexture.' );

						texture.type = UnsignedInt248Type$1;
						glType = utils.convert( texture.type );

					}

				}

				//

				state.texImage2D( 3553, 0, glInternalFormat, image.width, image.height, 0, glFormat, glType, null );

			} else if ( texture.isDataTexture ) {

				// use manually created mipmaps if available
				// if there are no manual mipmaps
				// set 0 level mipmap and then use GL to generate other mipmap levels

				if ( mipmaps.length > 0 && supportsMips ) {

					for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {

						mipmap = mipmaps[ i ];
						state.texImage2D( 3553, i, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data );

					}

					texture.generateMipmaps = false;
					textureProperties.__maxMipLevel = mipmaps.length - 1;

				} else {

					state.texImage2D( 3553, 0, glInternalFormat, image.width, image.height, 0, glFormat, glType, image.data );
					textureProperties.__maxMipLevel = 0;

				}

			} else if ( texture.isCompressedTexture ) {

				for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {

					mipmap = mipmaps[ i ];

					if ( texture.format !== RGBAFormat && texture.format !== RGBFormat ) {

						if ( glFormat !== null ) {

							state.compressedTexImage2D( 3553, i, glInternalFormat, mipmap.width, mipmap.height, 0, mipmap.data );

						} else {

							console.warn( 'THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()' );

						}

					} else {

						state.texImage2D( 3553, i, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data );

					}

				}

				textureProperties.__maxMipLevel = mipmaps.length - 1;

			} else if ( texture.isDataTexture2DArray ) {

				state.texImage3D( 35866, 0, glInternalFormat, image.width, image.height, image.depth, 0, glFormat, glType, image.data );
				textureProperties.__maxMipLevel = 0;

			} else if ( texture.isDataTexture3D ) {

				state.texImage3D( 32879, 0, glInternalFormat, image.width, image.height, image.depth, 0, glFormat, glType, image.data );
				textureProperties.__maxMipLevel = 0;

			} else {

				// regular Texture (image, video, canvas)

				// use manually created mipmaps if available
				// if there are no manual mipmaps
				// set 0 level mipmap and then use GL to generate other mipmap levels

				if ( mipmaps.length > 0 && supportsMips ) {

					for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {

						mipmap = mipmaps[ i ];
						state.texImage2D( 3553, i, glInternalFormat, glFormat, glType, mipmap );

					}

					texture.generateMipmaps = false;
					textureProperties.__maxMipLevel = mipmaps.length - 1;

				} else {

					state.texImage2D( 3553, 0, glInternalFormat, glFormat, glType, image );
					textureProperties.__maxMipLevel = 0;

				}

			}

			if ( textureNeedsGenerateMipmaps( texture, supportsMips ) ) {

				generateMipmap( textureType, texture, image.width, image.height );

			}

			textureProperties.__version = texture.version;

			if ( texture.onUpdate ) texture.onUpdate( texture );

		}

		function uploadCubeTexture( textureProperties, texture, slot ) {

			if ( texture.image.length !== 6 ) return;

			initTexture( textureProperties, texture );

			state.activeTexture( 33984 + slot );
			state.bindTexture( 34067, textureProperties.__webglTexture );

			_gl.pixelStorei( 37440, texture.flipY );

			const isCompressed = ( texture && ( texture.isCompressedTexture || texture.image[ 0 ].isCompressedTexture ) );
			const isDataTexture = ( texture.image[ 0 ] && texture.image[ 0 ].isDataTexture );

			const cubeImage = [];

			for ( let i = 0; i < 6; i ++ ) {

				if ( ! isCompressed && ! isDataTexture ) {

					cubeImage[ i ] = resizeImage( texture.image[ i ], false, true, maxCubemapSize );

				} else {

					cubeImage[ i ] = isDataTexture ? texture.image[ i ].image : texture.image[ i ];

				}

			}

			const image = cubeImage[ 0 ],
				supportsMips = isPowerOfTwo( image ) || isWebGL2,
				glFormat = utils.convert( texture.format ),
				glType = utils.convert( texture.type ),
				glInternalFormat = getInternalFormat( texture.internalFormat, glFormat, glType );

			setTextureParameters( 34067, texture, supportsMips );

			let mipmaps;

			if ( isCompressed ) {

				for ( let i = 0; i < 6; i ++ ) {

					mipmaps = cubeImage[ i ].mipmaps;

					for ( let j = 0; j < mipmaps.length; j ++ ) {

						const mipmap = mipmaps[ j ];

						if ( texture.format !== RGBAFormat && texture.format !== RGBFormat ) {

							if ( glFormat !== null ) {

								state.compressedTexImage2D( 34069 + i, j, glInternalFormat, mipmap.width, mipmap.height, 0, mipmap.data );

							} else {

								console.warn( 'THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .setTextureCube()' );

							}

						} else {

							state.texImage2D( 34069 + i, j, glInternalFormat, mipmap.width, mipmap.height, 0, glFormat, glType, mipmap.data );

						}

					}

				}

				textureProperties.__maxMipLevel = mipmaps.length - 1;

			} else {

				mipmaps = texture.mipmaps;

				for ( let i = 0; i < 6; i ++ ) {

					if ( isDataTexture ) {

						state.texImage2D( 34069 + i, 0, glInternalFormat, cubeImage[ i ].width, cubeImage[ i ].height, 0, glFormat, glType, cubeImage[ i ].data );

						for ( let j = 0; j < mipmaps.length; j ++ ) {

							const mipmap = mipmaps[ j ];
							const mipmapImage = mipmap.image[ i ].image;

							state.texImage2D( 34069 + i, j + 1, glInternalFormat, mipmapImage.width, mipmapImage.height, 0, glFormat, glType, mipmapImage.data );

						}

					} else {

						state.texImage2D( 34069 + i, 0, glInternalFormat, glFormat, glType, cubeImage[ i ] );

						for ( let j = 0; j < mipmaps.length; j ++ ) {

							const mipmap = mipmaps[ j ];

							state.texImage2D( 34069 + i, j + 1, glInternalFormat, glFormat, glType, mipmap.image[ i ] );

						}

					}

				}

				textureProperties.__maxMipLevel = mipmaps.length;

			}

			if ( textureNeedsGenerateMipmaps( texture, supportsMips ) ) {

				// We assume images for cube map have the same size.
				generateMipmap( 34067, texture, image.width, image.height );

			}

			textureProperties.__version = texture.version;

			if ( texture.onUpdate ) texture.onUpdate( texture );

		}

		// Render targets

		// Setup storage for target texture and bind it to correct framebuffer
		function setupFrameBufferTexture( framebuffer, renderTarget, attachment, textureTarget ) {

			const glFormat = utils.convert( renderTarget.texture.format );
			const glType = utils.convert( renderTarget.texture.type );
			const glInternalFormat = getInternalFormat( renderTarget.texture.internalFormat, glFormat, glType );
			state.texImage2D( textureTarget, 0, glInternalFormat, renderTarget.width, renderTarget.height, 0, glFormat, glType, null );
			_gl.bindFramebuffer( 36160, framebuffer );
			_gl.framebufferTexture2D( 36160, attachment, textureTarget, properties.get( renderTarget.texture ).__webglTexture, 0 );
			_gl.bindFramebuffer( 36160, null );

		}

		// Setup storage for internal depth/stencil buffers and bind to correct framebuffer
		function setupRenderBufferStorage( renderbuffer, renderTarget, isMultisample ) {

			_gl.bindRenderbuffer( 36161, renderbuffer );

			if ( renderTarget.depthBuffer && ! renderTarget.stencilBuffer ) {

				let glInternalFormat = 33189;

				if ( isMultisample ) {

					const depthTexture = renderTarget.depthTexture;

					if ( depthTexture && depthTexture.isDepthTexture ) {

						if ( depthTexture.type === FloatType ) {

							glInternalFormat = 36012;

						} else if ( depthTexture.type === UnsignedIntType ) {

							glInternalFormat = 33190;

						}

					}

					const samples = getRenderTargetSamples( renderTarget );

					_gl.renderbufferStorageMultisample( 36161, samples, glInternalFormat, renderTarget.width, renderTarget.height );

				} else {

					_gl.renderbufferStorage( 36161, glInternalFormat, renderTarget.width, renderTarget.height );

				}

				_gl.framebufferRenderbuffer( 36160, 36096, 36161, renderbuffer );

			} else if ( renderTarget.depthBuffer && renderTarget.stencilBuffer ) {

				if ( isMultisample ) {

					const samples = getRenderTargetSamples( renderTarget );

					_gl.renderbufferStorageMultisample( 36161, samples, 35056, renderTarget.width, renderTarget.height );

				} else {

					_gl.renderbufferStorage( 36161, 34041, renderTarget.width, renderTarget.height );

				}


				_gl.framebufferRenderbuffer( 36160, 33306, 36161, renderbuffer );

			} else {

				const glFormat = utils.convert( renderTarget.texture.format );
				const glType = utils.convert( renderTarget.texture.type );
				const glInternalFormat = getInternalFormat( renderTarget.texture.internalFormat, glFormat, glType );

				if ( isMultisample ) {

					const samples = getRenderTargetSamples( renderTarget );

					_gl.renderbufferStorageMultisample( 36161, samples, glInternalFormat, renderTarget.width, renderTarget.height );

				} else {

					_gl.renderbufferStorage( 36161, glInternalFormat, renderTarget.width, renderTarget.height );

				}

			}

			_gl.bindRenderbuffer( 36161, null );

		}

		// Setup resources for a Depth Texture for a FBO (needs an extension)
		function setupDepthTexture( framebuffer, renderTarget ) {

			const isCube = ( renderTarget && renderTarget.isWebGLCubeRenderTarget );
			if ( isCube ) throw new Error( 'Depth Texture with cube render targets is not supported' );

			_gl.bindFramebuffer( 36160, framebuffer );

			if ( ! ( renderTarget.depthTexture && renderTarget.depthTexture.isDepthTexture ) ) {

				throw new Error( 'renderTarget.depthTexture must be an instance of THREE.DepthTexture' );

			}

			// upload an empty depth texture with framebuffer size
			if ( ! properties.get( renderTarget.depthTexture ).__webglTexture ||
					renderTarget.depthTexture.image.width !== renderTarget.width ||
					renderTarget.depthTexture.image.height !== renderTarget.height ) {

				renderTarget.depthTexture.image.width = renderTarget.width;
				renderTarget.depthTexture.image.height = renderTarget.height;
				renderTarget.depthTexture.needsUpdate = true;

			}

			setTexture2D( renderTarget.depthTexture, 0 );

			const webglDepthTexture = properties.get( renderTarget.depthTexture ).__webglTexture;

			if ( renderTarget.depthTexture.format === DepthFormat ) {

				_gl.framebufferTexture2D( 36160, 36096, 3553, webglDepthTexture, 0 );

			} else if ( renderTarget.depthTexture.format === DepthStencilFormat ) {

				_gl.framebufferTexture2D( 36160, 33306, 3553, webglDepthTexture, 0 );

			} else {

				throw new Error( 'Unknown depthTexture format' );

			}

		}

		// Setup GL resources for a non-texture depth buffer
		function setupDepthRenderbuffer( renderTarget ) {

			const renderTargetProperties = properties.get( renderTarget );

			const isCube = ( renderTarget.isWebGLCubeRenderTarget === true );

			if ( renderTarget.depthTexture ) {

				if ( isCube ) throw new Error( 'target.depthTexture not supported in Cube render targets' );

				setupDepthTexture( renderTargetProperties.__webglFramebuffer, renderTarget );

			} else {

				if ( isCube ) {

					renderTargetProperties.__webglDepthbuffer = [];

					for ( let i = 0; i < 6; i ++ ) {

						_gl.bindFramebuffer( 36160, renderTargetProperties.__webglFramebuffer[ i ] );
						renderTargetProperties.__webglDepthbuffer[ i ] = _gl.createRenderbuffer();
						setupRenderBufferStorage( renderTargetProperties.__webglDepthbuffer[ i ], renderTarget, false );

					}

				} else {

					_gl.bindFramebuffer( 36160, renderTargetProperties.__webglFramebuffer );
					renderTargetProperties.__webglDepthbuffer = _gl.createRenderbuffer();
					setupRenderBufferStorage( renderTargetProperties.__webglDepthbuffer, renderTarget, false );

				}

			}

			_gl.bindFramebuffer( 36160, null );

		}

		// Set up GL resources for the render target
		function setupRenderTarget( renderTarget ) {

			const renderTargetProperties = properties.get( renderTarget );
			const textureProperties = properties.get( renderTarget.texture );

			renderTarget.addEventListener( 'dispose', onRenderTargetDispose );

			textureProperties.__webglTexture = _gl.createTexture();

			info.memory.textures ++;

			const isCube = ( renderTarget.isWebGLCubeRenderTarget === true );
			const isMultisample = ( renderTarget.isWebGLMultisampleRenderTarget === true );
			const supportsMips = isPowerOfTwo( renderTarget ) || isWebGL2;

			// Handles WebGL2 RGBFormat fallback - #18858

			if ( isWebGL2 && renderTarget.texture.format === RGBFormat && ( renderTarget.texture.type === FloatType || renderTarget.texture.type === HalfFloatType ) ) {

				renderTarget.texture.format = RGBAFormat;

				console.warn( 'THREE.WebGLRenderer: Rendering to textures with RGB format is not supported. Using RGBA format instead.' );

			}

			// Setup framebuffer

			if ( isCube ) {

				renderTargetProperties.__webglFramebuffer = [];

				for ( let i = 0; i < 6; i ++ ) {

					renderTargetProperties.__webglFramebuffer[ i ] = _gl.createFramebuffer();

				}

			} else {

				renderTargetProperties.__webglFramebuffer = _gl.createFramebuffer();

				if ( isMultisample ) {

					if ( isWebGL2 ) {

						renderTargetProperties.__webglMultisampledFramebuffer = _gl.createFramebuffer();
						renderTargetProperties.__webglColorRenderbuffer = _gl.createRenderbuffer();

						_gl.bindRenderbuffer( 36161, renderTargetProperties.__webglColorRenderbuffer );

						const glFormat = utils.convert( renderTarget.texture.format );
						const glType = utils.convert( renderTarget.texture.type );
						const glInternalFormat = getInternalFormat( renderTarget.texture.internalFormat, glFormat, glType );
						const samples = getRenderTargetSamples( renderTarget );
						_gl.renderbufferStorageMultisample( 36161, samples, glInternalFormat, renderTarget.width, renderTarget.height );

						_gl.bindFramebuffer( 36160, renderTargetProperties.__webglMultisampledFramebuffer );
						_gl.framebufferRenderbuffer( 36160, 36064, 36161, renderTargetProperties.__webglColorRenderbuffer );
						_gl.bindRenderbuffer( 36161, null );

						if ( renderTarget.depthBuffer ) {

							renderTargetProperties.__webglDepthRenderbuffer = _gl.createRenderbuffer();
							setupRenderBufferStorage( renderTargetProperties.__webglDepthRenderbuffer, renderTarget, true );

						}

						_gl.bindFramebuffer( 36160, null );


					} else {

						console.warn( 'THREE.WebGLRenderer: WebGLMultisampleRenderTarget can only be used with WebGL2.' );

					}

				}

			}

			// Setup color buffer

			if ( isCube ) {

				state.bindTexture( 34067, textureProperties.__webglTexture );
				setTextureParameters( 34067, renderTarget.texture, supportsMips );

				for ( let i = 0; i < 6; i ++ ) {

					setupFrameBufferTexture( renderTargetProperties.__webglFramebuffer[ i ], renderTarget, 36064, 34069 + i );

				}

				if ( textureNeedsGenerateMipmaps( renderTarget.texture, supportsMips ) ) {

					generateMipmap( 34067, renderTarget.texture, renderTarget.width, renderTarget.height );

				}

				state.bindTexture( 34067, null );

			} else {

				state.bindTexture( 3553, textureProperties.__webglTexture );
				setTextureParameters( 3553, renderTarget.texture, supportsMips );
				setupFrameBufferTexture( renderTargetProperties.__webglFramebuffer, renderTarget, 36064, 3553 );

				if ( textureNeedsGenerateMipmaps( renderTarget.texture, supportsMips ) ) {

					generateMipmap( 3553, renderTarget.texture, renderTarget.width, renderTarget.height );

				}

				state.bindTexture( 3553, null );

			}

			// Setup depth and stencil buffers

			if ( renderTarget.depthBuffer ) {

				setupDepthRenderbuffer( renderTarget );

			}

		}

		function updateRenderTargetMipmap( renderTarget ) {

			const texture = renderTarget.texture;
			const supportsMips = isPowerOfTwo( renderTarget ) || isWebGL2;

			if ( textureNeedsGenerateMipmaps( texture, supportsMips ) ) {

				const target = renderTarget.isWebGLCubeRenderTarget ? 34067 : 3553;
				const webglTexture = properties.get( texture ).__webglTexture;

				state.bindTexture( target, webglTexture );
				generateMipmap( target, texture, renderTarget.width, renderTarget.height );
				state.bindTexture( target, null );

			}

		}

		function updateMultisampleRenderTarget( renderTarget ) {

			if ( renderTarget.isWebGLMultisampleRenderTarget ) {

				if ( isWebGL2 ) {

					const renderTargetProperties = properties.get( renderTarget );

					_gl.bindFramebuffer( 36008, renderTargetProperties.__webglMultisampledFramebuffer );
					_gl.bindFramebuffer( 36009, renderTargetProperties.__webglFramebuffer );

					const width = renderTarget.width;
					const height = renderTarget.height;
					let mask = 16384;

					if ( renderTarget.depthBuffer ) mask |= 256;
					if ( renderTarget.stencilBuffer ) mask |= 1024;

					_gl.blitFramebuffer( 0, 0, width, height, 0, 0, width, height, mask, 9728 );

					_gl.bindFramebuffer( 36160, renderTargetProperties.__webglMultisampledFramebuffer ); // see #18905

				} else {

					console.warn( 'THREE.WebGLRenderer: WebGLMultisampleRenderTarget can only be used with WebGL2.' );

				}

			}

		}

		function getRenderTargetSamples( renderTarget ) {

			return ( isWebGL2 && renderTarget.isWebGLMultisampleRenderTarget ) ?
				Math.min( maxSamples, renderTarget.samples ) : 0;

		}

		function updateVideoTexture( texture ) {

			const frame = info.render.frame;

			// Check the last frame we updated the VideoTexture

			if ( _videoTextures.get( texture ) !== frame ) {

				_videoTextures.set( texture, frame );
				texture.update();

			}

		}

		// backwards compatibility

		let warnedTexture2D = false;
		let warnedTextureCube = false;

		function safeSetTexture2D( texture, slot ) {

			if ( texture && texture.isWebGLRenderTarget ) {

				if ( warnedTexture2D === false ) {

					console.warn( 'THREE.WebGLTextures.safeSetTexture2D: don\'t use render targets as textures. Use their .texture property instead.' );
					warnedTexture2D = true;

				}

				texture = texture.texture;

			}

			setTexture2D( texture, slot );

		}

		function safeSetTextureCube( texture, slot ) {

			if ( texture && texture.isWebGLCubeRenderTarget ) {

				if ( warnedTextureCube === false ) {

					console.warn( 'THREE.WebGLTextures.safeSetTextureCube: don\'t use cube render targets as textures. Use their .texture property instead.' );
					warnedTextureCube = true;

				}

				texture = texture.texture;

			}


			setTextureCube( texture, slot );

		}

		//

		this.allocateTextureUnit = allocateTextureUnit;
		this.resetTextureUnits = resetTextureUnits;

		this.setTexture2D = setTexture2D;
		this.setTexture2DArray = setTexture2DArray;
		this.setTexture3D = setTexture3D;
		this.setTextureCube = setTextureCube;
		this.setupRenderTarget = setupRenderTarget;
		this.updateRenderTargetMipmap = updateRenderTargetMipmap;
		this.updateMultisampleRenderTarget = updateMultisampleRenderTarget;

		this.safeSetTexture2D = safeSetTexture2D;
		this.safeSetTextureCube = safeSetTextureCube;

	}

	function WebGLUtils( gl, extensions, capabilities ) {

		const isWebGL2 = capabilities.isWebGL2;

		function convert( p ) {

			let extension;

			if ( p === UnsignedByteType ) return 5121;
			if ( p === UnsignedShort4444Type ) return 32819;
			if ( p === UnsignedShort5551Type ) return 32820;
			if ( p === UnsignedShort565Type ) return 33635;

			if ( p === ByteType ) return 5120;
			if ( p === ShortType ) return 5122;
			if ( p === UnsignedShortType ) return 5123;
			if ( p === IntType ) return 5124;
			if ( p === UnsignedIntType ) return 5125;
			if ( p === FloatType ) return 5126;

			if ( p === HalfFloatType ) {

				if ( isWebGL2 ) return 5131;

				extension = extensions.get( 'OES_texture_half_float' );

				if ( extension !== null ) {

					return extension.HALF_FLOAT_OES;

				} else {

					return null;

				}

			}

			if ( p === AlphaFormat ) return 6406;
			if ( p === RGBFormat ) return 6407;
			if ( p === RGBAFormat ) return 6408;
			if ( p === LuminanceFormat ) return 6409;
			if ( p === LuminanceAlphaFormat ) return 6410;
			if ( p === DepthFormat ) return 6402;
			if ( p === DepthStencilFormat ) return 34041;
			if ( p === RedFormat ) return 6403;

			// WebGL2 formats.

			if ( p === RedIntegerFormat ) return 36244;
			if ( p === RGFormat ) return 33319;
			if ( p === RGIntegerFormat ) return 33320;
			if ( p === RGBIntegerFormat ) return 36248;
			if ( p === RGBAIntegerFormat ) return 36249;

			if ( p === RGB_S3TC_DXT1_Format || p === RGBA_S3TC_DXT1_Format$1 ||
				p === RGBA_S3TC_DXT3_Format || p === RGBA_S3TC_DXT5_Format$1 ) {

				extension = extensions.get( 'WEBGL_compressed_texture_s3tc' );

				if ( extension !== null ) {

					if ( p === RGB_S3TC_DXT1_Format ) return extension.COMPRESSED_RGB_S3TC_DXT1_EXT;
					if ( p === RGBA_S3TC_DXT1_Format$1 ) return extension.COMPRESSED_RGBA_S3TC_DXT1_EXT;
					if ( p === RGBA_S3TC_DXT3_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT3_EXT;
					if ( p === RGBA_S3TC_DXT5_Format$1 ) return extension.COMPRESSED_RGBA_S3TC_DXT5_EXT;

				} else {

					return null;

				}

			}

			if ( p === RGB_PVRTC_4BPPV1_Format || p === RGB_PVRTC_2BPPV1_Format ||
				p === RGBA_PVRTC_4BPPV1_Format || p === RGBA_PVRTC_2BPPV1_Format ) {

				extension = extensions.get( 'WEBGL_compressed_texture_pvrtc' );

				if ( extension !== null ) {

					if ( p === RGB_PVRTC_4BPPV1_Format ) return extension.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
					if ( p === RGB_PVRTC_2BPPV1_Format ) return extension.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
					if ( p === RGBA_PVRTC_4BPPV1_Format ) return extension.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
					if ( p === RGBA_PVRTC_2BPPV1_Format ) return extension.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;

				} else {

					return null;

				}

			}

			if ( p === RGB_ETC1_Format ) {

				extension = extensions.get( 'WEBGL_compressed_texture_etc1' );

				if ( extension !== null ) {

					return extension.COMPRESSED_RGB_ETC1_WEBGL;

				} else {

					return null;

				}

			}

			if ( p === RGB_ETC2_Format || p === RGBA_ETC2_EAC_Format ) {

				extension = extensions.get( 'WEBGL_compressed_texture_etc' );

				if ( extension !== null ) {

					if ( p === RGB_ETC2_Format ) return extension.COMPRESSED_RGB8_ETC2;
					if ( p === RGBA_ETC2_EAC_Format ) return extension.COMPRESSED_RGBA8_ETC2_EAC;

				}

			}

			if ( p === RGBA_ASTC_4x4_Format || p === RGBA_ASTC_5x4_Format || p === RGBA_ASTC_5x5_Format ||
				p === RGBA_ASTC_6x5_Format || p === RGBA_ASTC_6x6_Format || p === RGBA_ASTC_8x5_Format ||
				p === RGBA_ASTC_8x6_Format || p === RGBA_ASTC_8x8_Format || p === RGBA_ASTC_10x5_Format ||
				p === RGBA_ASTC_10x6_Format || p === RGBA_ASTC_10x8_Format || p === RGBA_ASTC_10x10_Format ||
				p === RGBA_ASTC_12x10_Format || p === RGBA_ASTC_12x12_Format ||
				p === SRGB8_ALPHA8_ASTC_4x4_Format || p === SRGB8_ALPHA8_ASTC_5x4_Format || p === SRGB8_ALPHA8_ASTC_5x5_Format ||
				p === SRGB8_ALPHA8_ASTC_6x5_Format || p === SRGB8_ALPHA8_ASTC_6x6_Format || p === SRGB8_ALPHA8_ASTC_8x5_Format ||
				p === SRGB8_ALPHA8_ASTC_8x6_Format || p === SRGB8_ALPHA8_ASTC_8x8_Format || p === SRGB8_ALPHA8_ASTC_10x5_Format ||
				p === SRGB8_ALPHA8_ASTC_10x6_Format || p === SRGB8_ALPHA8_ASTC_10x8_Format || p === SRGB8_ALPHA8_ASTC_10x10_Format ||
				p === SRGB8_ALPHA8_ASTC_12x10_Format || p === SRGB8_ALPHA8_ASTC_12x12_Format ) {

				extension = extensions.get( 'WEBGL_compressed_texture_astc' );

				if ( extension !== null ) {

					// TODO Complete?

					return p;

				} else {

					return null;

				}

			}

			if ( p === RGBA_BPTC_Format ) {

				extension = extensions.get( 'EXT_texture_compression_bptc' );

				if ( extension !== null ) {

					// TODO Complete?

					return p;

				} else {

					return null;

				}

			}

			if ( p === UnsignedInt248Type$1 ) {

				if ( isWebGL2 ) return 34042;

				extension = extensions.get( 'WEBGL_depth_texture' );

				if ( extension !== null ) {

					return extension.UNSIGNED_INT_24_8_WEBGL;

				} else {

					return null;

				}

			}

		}

		return { convert: convert };

	}

	function ArrayCamera( array = [] ) {

		PerspectiveCamera.call( this );

		this.cameras = array;

	}

	ArrayCamera.prototype = Object.assign( Object.create( PerspectiveCamera.prototype ), {

		constructor: ArrayCamera,

		isArrayCamera: true

	} );

	function Group() {

		Object3D.call( this );

		this.type = 'Group';

	}

	Group.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: Group,

		isGroup: true

	} );

	function WebXRController() {

		this._targetRay = null;
		this._grip = null;
		this._hand = null;

	}

	Object.assign( WebXRController.prototype, {

		constructor: WebXRController,

		getHandSpace: function () {

			if ( this._hand === null ) {

				this._hand = new Group();
				this._hand.matrixAutoUpdate = false;
				this._hand.visible = false;

				this._hand.joints = [];
				this._hand.inputState = { pinching: false };

				if ( window.XRHand ) {

					for ( let i = 0; i <= window.XRHand.LITTLE_PHALANX_TIP; i ++ ) {

						// The transform of this joint will be updated with the joint pose on each frame
						const joint = new Group();
						joint.matrixAutoUpdate = false;
						joint.visible = false;
						this._hand.joints.push( joint );
						// ??
						this._hand.add( joint );

					}

				}

			}

			return this._hand;

		},

		getTargetRaySpace: function () {

			if ( this._targetRay === null ) {

				this._targetRay = new Group();
				this._targetRay.matrixAutoUpdate = false;
				this._targetRay.visible = false;

			}

			return this._targetRay;

		},

		getGripSpace: function () {

			if ( this._grip === null ) {

				this._grip = new Group();
				this._grip.matrixAutoUpdate = false;
				this._grip.visible = false;

			}

			return this._grip;

		},

		dispatchEvent: function ( event ) {

			if ( this._targetRay !== null ) {

				this._targetRay.dispatchEvent( event );

			}

			if ( this._grip !== null ) {

				this._grip.dispatchEvent( event );

			}

			if ( this._hand !== null ) {

				this._hand.dispatchEvent( event );

			}

			return this;

		},

		disconnect: function ( inputSource ) {

			this.dispatchEvent( { type: 'disconnected', data: inputSource } );

			if ( this._targetRay !== null ) {

				this._targetRay.visible = false;

			}

			if ( this._grip !== null ) {

				this._grip.visible = false;

			}

			if ( this._hand !== null ) {

				this._hand.visible = false;

			}

			return this;

		},

		update: function ( inputSource, frame, referenceSpace ) {

			let inputPose = null;
			let gripPose = null;
			let handPose = null;

			const targetRay = this._targetRay;
			const grip = this._grip;
			const hand = this._hand;

			if ( inputSource && frame.session.visibilityState !== 'visible-blurred' ) {

				if ( hand && inputSource.hand ) {

					handPose = true;

					for ( let i = 0; i <= window.XRHand.LITTLE_PHALANX_TIP; i ++ ) {

						if ( inputSource.hand[ i ] ) {

							// Update the joints groups with the XRJoint poses
							const jointPose = frame.getJointPose( inputSource.hand[ i ], referenceSpace );
							const joint = hand.joints[ i ];

							if ( jointPose !== null ) {

								joint.matrix.fromArray( jointPose.transform.matrix );
								joint.matrix.decompose( joint.position, joint.rotation, joint.scale );
								joint.jointRadius = jointPose.radius;

							}

							joint.visible = jointPose !== null;

							// Custom events

							// Check pinch
							const indexTip = hand.joints[ window.XRHand.INDEX_PHALANX_TIP ];
							const thumbTip = hand.joints[ window.XRHand.THUMB_PHALANX_TIP ];
							const distance = indexTip.position.distanceTo( thumbTip.position );

							const distanceToPinch = 0.02;
							const threshold = 0.005;

							if ( hand.inputState.pinching && distance > distanceToPinch + threshold ) {

								hand.inputState.pinching = false;
								this.dispatchEvent( {
									type: 'pinchend',
									handedness: inputSource.handedness,
									target: this
								} );

							} else if ( ! hand.inputState.pinching && distance <= distanceToPinch - threshold ) {

								hand.inputState.pinching = true;
								this.dispatchEvent( {
									type: 'pinchstart',
									handedness: inputSource.handedness,
									target: this
								} );

							}

						}

					}

				} else {

					if ( targetRay !== null ) {

						inputPose = frame.getPose( inputSource.targetRaySpace, referenceSpace );

						if ( inputPose !== null ) {

							targetRay.matrix.fromArray( inputPose.transform.matrix );
							targetRay.matrix.decompose( targetRay.position, targetRay.rotation, targetRay.scale );

						}

					}

					if ( grip !== null && inputSource.gripSpace ) {

						gripPose = frame.getPose( inputSource.gripSpace, referenceSpace );

						if ( gripPose !== null ) {

							grip.matrix.fromArray( gripPose.transform.matrix );
							grip.matrix.decompose( grip.position, grip.rotation, grip.scale );

						}

					}

				}

			}

			if ( targetRay !== null ) {

				targetRay.visible = ( inputPose !== null );

			}

			if ( grip !== null ) {

				grip.visible = ( gripPose !== null );

			}

			if ( hand !== null ) {

				hand.visible = ( handPose !== null );

			}

			return this;

		}

	} );

	function WebXRManager( renderer, gl ) {

		const scope = this;

		let session = null;

		let framebufferScaleFactor = 1.0;

		let referenceSpace = null;
		let referenceSpaceType = 'local-floor';

		let pose = null;

		const controllers = [];
		const inputSourcesMap = new Map();

		//

		const cameraL = new PerspectiveCamera();
		cameraL.layers.enable( 1 );
		cameraL.viewport = new Vector4();

		const cameraR = new PerspectiveCamera();
		cameraR.layers.enable( 2 );
		cameraR.viewport = new Vector4();

		const cameras = [ cameraL, cameraR ];

		const cameraVR = new ArrayCamera();
		cameraVR.layers.enable( 1 );
		cameraVR.layers.enable( 2 );

		let _currentDepthNear = null;
		let _currentDepthFar = null;

		//

		this.enabled = false;

		this.isPresenting = false;

		this.getController = function ( index ) {

			let controller = controllers[ index ];

			if ( controller === undefined ) {

				controller = new WebXRController();
				controllers[ index ] = controller;

			}

			return controller.getTargetRaySpace();

		};

		this.getControllerGrip = function ( index ) {

			let controller = controllers[ index ];

			if ( controller === undefined ) {

				controller = new WebXRController();
				controllers[ index ] = controller;

			}

			return controller.getGripSpace();

		};

		this.getHand = function ( index ) {

			let controller = controllers[ index ];

			if ( controller === undefined ) {

				controller = new WebXRController();
				controllers[ index ] = controller;

			}

			return controller.getHandSpace();

		};

		//

		function onSessionEvent( event ) {

			const controller = inputSourcesMap.get( event.inputSource );

			if ( controller ) {

				controller.dispatchEvent( { type: event.type, data: event.inputSource } );

			}

		}

		function onSessionEnd() {

			inputSourcesMap.forEach( function ( controller, inputSource ) {

				controller.disconnect( inputSource );

			} );

			inputSourcesMap.clear();

			//

			renderer.setFramebuffer( null );
			renderer.setRenderTarget( renderer.getRenderTarget() ); // Hack #15830
			animation.stop();

			scope.isPresenting = false;

			scope.dispatchEvent( { type: 'sessionend' } );

		}

		function onRequestReferenceSpace( value ) {

			referenceSpace = value;

			animation.setContext( session );
			animation.start();

			scope.isPresenting = true;

			scope.dispatchEvent( { type: 'sessionstart' } );

		}

		this.setFramebufferScaleFactor = function ( value ) {

			framebufferScaleFactor = value;

			if ( scope.isPresenting === true ) {

				console.warn( 'THREE.WebXRManager: Cannot change framebuffer scale while presenting.' );

			}

		};

		this.setReferenceSpaceType = function ( value ) {

			referenceSpaceType = value;

			if ( scope.isPresenting === true ) {

				console.warn( 'THREE.WebXRManager: Cannot change reference space type while presenting.' );

			}

		};

		this.getReferenceSpace = function () {

			return referenceSpace;

		};

		this.getSession = function () {

			return session;

		};

		this.setSession = function ( value ) {

			session = value;

			if ( session !== null ) {

				session.addEventListener( 'select', onSessionEvent );
				session.addEventListener( 'selectstart', onSessionEvent );
				session.addEventListener( 'selectend', onSessionEvent );
				session.addEventListener( 'squeeze', onSessionEvent );
				session.addEventListener( 'squeezestart', onSessionEvent );
				session.addEventListener( 'squeezeend', onSessionEvent );
				session.addEventListener( 'end', onSessionEnd );

				const attributes = gl.getContextAttributes();

				if ( attributes.xrCompatible !== true ) {

					gl.makeXRCompatible();

				}

				const layerInit = {
					antialias: attributes.antialias,
					alpha: attributes.alpha,
					depth: attributes.depth,
					stencil: attributes.stencil,
					framebufferScaleFactor: framebufferScaleFactor
				};

				// eslint-disable-next-line no-undef
				const baseLayer = new XRWebGLLayer( session, gl, layerInit );

				session.updateRenderState( { baseLayer: baseLayer } );

				session.requestReferenceSpace( referenceSpaceType ).then( onRequestReferenceSpace );

				//

				session.addEventListener( 'inputsourceschange', updateInputSources );

			}

		};

		function updateInputSources( event ) {

			const inputSources = session.inputSources;

			// Assign inputSources to available controllers

			for ( let i = 0; i < controllers.length; i ++ ) {

				inputSourcesMap.set( inputSources[ i ], controllers[ i ] );

			}

			// Notify disconnected

			for ( let i = 0; i < event.removed.length; i ++ ) {

				const inputSource = event.removed[ i ];
				const controller = inputSourcesMap.get( inputSource );

				if ( controller ) {

					controller.dispatchEvent( { type: 'disconnected', data: inputSource } );
					inputSourcesMap.delete( inputSource );

				}

			}

			// Notify connected

			for ( let i = 0; i < event.added.length; i ++ ) {

				const inputSource = event.added[ i ];
				const controller = inputSourcesMap.get( inputSource );

				if ( controller ) {

					controller.dispatchEvent( { type: 'connected', data: inputSource } );

				}

			}

		}

		//

		const cameraLPos = new Vector3();
		const cameraRPos = new Vector3();

		/**
		 * Assumes 2 cameras that are parallel and share an X-axis, and that
		 * the cameras' projection and world matrices have already been set.
		 * And that near and far planes are identical for both cameras.
		 * Visualization of this technique: https://computergraphics.stackexchange.com/a/4765
		 */
		function setProjectionFromUnion( camera, cameraL, cameraR ) {

			cameraLPos.setFromMatrixPosition( cameraL.matrixWorld );
			cameraRPos.setFromMatrixPosition( cameraR.matrixWorld );

			const ipd = cameraLPos.distanceTo( cameraRPos );

			const projL = cameraL.projectionMatrix.elements;
			const projR = cameraR.projectionMatrix.elements;

			// VR systems will have identical far and near planes, and
			// most likely identical top and bottom frustum extents.
			// Use the left camera for these values.
			const near = projL[ 14 ] / ( projL[ 10 ] - 1 );
			const far = projL[ 14 ] / ( projL[ 10 ] + 1 );
			const topFov = ( projL[ 9 ] + 1 ) / projL[ 5 ];
			const bottomFov = ( projL[ 9 ] - 1 ) / projL[ 5 ];

			const leftFov = ( projL[ 8 ] - 1 ) / projL[ 0 ];
			const rightFov = ( projR[ 8 ] + 1 ) / projR[ 0 ];
			const left = near * leftFov;
			const right = near * rightFov;

			// Calculate the new camera's position offset from the
			// left camera. xOffset should be roughly half `ipd`.
			const zOffset = ipd / ( - leftFov + rightFov );
			const xOffset = zOffset * - leftFov;

			// TODO: Better way to apply this offset?
			cameraL.matrixWorld.decompose( camera.position, camera.quaternion, camera.scale );
			camera.translateX( xOffset );
			camera.translateZ( zOffset );
			camera.matrixWorld.compose( camera.position, camera.quaternion, camera.scale );
			camera.matrixWorldInverse.copy( camera.matrixWorld ).invert();

			// Find the union of the frustum values of the cameras and scale
			// the values so that the near plane's position does not change in world space,
			// although must now be relative to the new union camera.
			const near2 = near + zOffset;
			const far2 = far + zOffset;
			const left2 = left - xOffset;
			const right2 = right + ( ipd - xOffset );
			const top2 = topFov * far / far2 * near2;
			const bottom2 = bottomFov * far / far2 * near2;

			camera.projectionMatrix.makePerspective( left2, right2, top2, bottom2, near2, far2 );

		}

		function updateCamera( camera, parent ) {

			if ( parent === null ) {

				camera.matrixWorld.copy( camera.matrix );

			} else {

				camera.matrixWorld.multiplyMatrices( parent.matrixWorld, camera.matrix );

			}

			camera.matrixWorldInverse.copy( camera.matrixWorld ).invert();

		}

		this.getCamera = function ( camera ) {

			cameraVR.near = cameraR.near = cameraL.near = camera.near;
			cameraVR.far = cameraR.far = cameraL.far = camera.far;

			if ( _currentDepthNear !== cameraVR.near || _currentDepthFar !== cameraVR.far ) {

				// Note that the new renderState won't apply until the next frame. See #18320

				session.updateRenderState( {
					depthNear: cameraVR.near,
					depthFar: cameraVR.far
				} );

				_currentDepthNear = cameraVR.near;
				_currentDepthFar = cameraVR.far;

			}

			const parent = camera.parent;
			const cameras = cameraVR.cameras;

			updateCamera( cameraVR, parent );

			for ( let i = 0; i < cameras.length; i ++ ) {

				updateCamera( cameras[ i ], parent );

			}

			// update camera and its children

			camera.matrixWorld.copy( cameraVR.matrixWorld );

			const children = camera.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				children[ i ].updateMatrixWorld( true );

			}

			// update projection matrix for proper view frustum culling

			if ( cameras.length === 2 ) {

				setProjectionFromUnion( cameraVR, cameraL, cameraR );

			} else {

				// assume single camera setup (AR)

				cameraVR.projectionMatrix.copy( cameraL.projectionMatrix );

			}

			return cameraVR;

		};

		// Animation Loop

		let onAnimationFrameCallback = null;

		function onAnimationFrame( time, frame ) {

			pose = frame.getViewerPose( referenceSpace );

			if ( pose !== null ) {

				const views = pose.views;
				const baseLayer = session.renderState.baseLayer;

				renderer.setFramebuffer( baseLayer.framebuffer );

				let cameraVRNeedsUpdate = false;

				// check if it's necessary to rebuild cameraVR's camera list

				if ( views.length !== cameraVR.cameras.length ) {

					cameraVR.cameras.length = 0;
					cameraVRNeedsUpdate = true;

				}

				for ( let i = 0; i < views.length; i ++ ) {

					const view = views[ i ];
					const viewport = baseLayer.getViewport( view );

					const camera = cameras[ i ];
					camera.matrix.fromArray( view.transform.matrix );
					camera.projectionMatrix.fromArray( view.projectionMatrix );
					camera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height );

					if ( i === 0 ) {

						cameraVR.matrix.copy( camera.matrix );

					}

					if ( cameraVRNeedsUpdate === true ) {

						cameraVR.cameras.push( camera );

					}

				}

			}

			//

			const inputSources = session.inputSources;

			for ( let i = 0; i < controllers.length; i ++ ) {

				const controller = controllers[ i ];
				const inputSource = inputSources[ i ];

				controller.update( inputSource, frame, referenceSpace );

			}

			if ( onAnimationFrameCallback ) onAnimationFrameCallback( time, frame );

		}

		const animation = new WebGLAnimation();
		animation.setAnimationLoop( onAnimationFrame );

		this.setAnimationLoop = function ( callback ) {

			onAnimationFrameCallback = callback;

		};

		this.dispose = function () {};

	}

	Object.assign( WebXRManager.prototype, EventDispatcher$1.prototype );

	function WebGLMaterials( properties ) {

		function refreshFogUniforms( uniforms, fog ) {

			uniforms.fogColor.value.copy( fog.color );

			if ( fog.isFog ) {

				uniforms.fogNear.value = fog.near;
				uniforms.fogFar.value = fog.far;

			} else if ( fog.isFogExp2 ) {

				uniforms.fogDensity.value = fog.density;

			}

		}

		function refreshMaterialUniforms( uniforms, material, pixelRatio, height ) {

			if ( material.isMeshBasicMaterial ) {

				refreshUniformsCommon( uniforms, material );

			} else if ( material.isMeshLambertMaterial ) {

				refreshUniformsCommon( uniforms, material );
				refreshUniformsLambert( uniforms, material );

			} else if ( material.isMeshToonMaterial ) {

				refreshUniformsCommon( uniforms, material );
				refreshUniformsToon( uniforms, material );

			} else if ( material.isMeshPhongMaterial ) {

				refreshUniformsCommon( uniforms, material );
				refreshUniformsPhong( uniforms, material );

			} else if ( material.isMeshStandardMaterial ) {

				refreshUniformsCommon( uniforms, material );

				if ( material.isMeshPhysicalMaterial ) {

					refreshUniformsPhysical( uniforms, material );

				} else {

					refreshUniformsStandard( uniforms, material );

				}

			} else if ( material.isMeshMatcapMaterial ) {

				refreshUniformsCommon( uniforms, material );
				refreshUniformsMatcap( uniforms, material );

			} else if ( material.isMeshDepthMaterial ) {

				refreshUniformsCommon( uniforms, material );
				refreshUniformsDepth( uniforms, material );

			} else if ( material.isMeshDistanceMaterial ) {

				refreshUniformsCommon( uniforms, material );
				refreshUniformsDistance( uniforms, material );

			} else if ( material.isMeshNormalMaterial ) {

				refreshUniformsCommon( uniforms, material );
				refreshUniformsNormal( uniforms, material );

			} else if ( material.isLineBasicMaterial ) {

				refreshUniformsLine( uniforms, material );

				if ( material.isLineDashedMaterial ) {

					refreshUniformsDash( uniforms, material );

				}

			} else if ( material.isPointsMaterial ) {

				refreshUniformsPoints( uniforms, material, pixelRatio, height );

			} else if ( material.isSpriteMaterial ) {

				refreshUniformsSprites( uniforms, material );

			} else if ( material.isShadowMaterial ) {

				uniforms.color.value.copy( material.color );
				uniforms.opacity.value = material.opacity;

			} else if ( material.isShaderMaterial ) {

				material.uniformsNeedUpdate = false; // #15581

			}

		}

		function refreshUniformsCommon( uniforms, material ) {

			uniforms.opacity.value = material.opacity;

			if ( material.color ) {

				uniforms.diffuse.value.copy( material.color );

			}

			if ( material.emissive ) {

				uniforms.emissive.value.copy( material.emissive ).multiplyScalar( material.emissiveIntensity );

			}

			if ( material.map ) {

				uniforms.map.value = material.map;

			}

			if ( material.alphaMap ) {

				uniforms.alphaMap.value = material.alphaMap;

			}

			if ( material.specularMap ) {

				uniforms.specularMap.value = material.specularMap;

			}

			const envMap = properties.get( material ).envMap;

			if ( envMap ) {

				uniforms.envMap.value = envMap;

				uniforms.flipEnvMap.value = ( envMap.isCubeTexture && envMap._needsFlipEnvMap ) ? - 1 : 1;

				uniforms.reflectivity.value = material.reflectivity;
				uniforms.refractionRatio.value = material.refractionRatio;

				const maxMipLevel = properties.get( envMap ).__maxMipLevel;

				if ( maxMipLevel !== undefined ) {

					uniforms.maxMipLevel.value = maxMipLevel;

				}

			}

			if ( material.lightMap ) {

				uniforms.lightMap.value = material.lightMap;
				uniforms.lightMapIntensity.value = material.lightMapIntensity;

			}

			if ( material.aoMap ) {

				uniforms.aoMap.value = material.aoMap;
				uniforms.aoMapIntensity.value = material.aoMapIntensity;

			}

			// uv repeat and offset setting priorities
			// 1. color map
			// 2. specular map
			// 3. displacementMap map
			// 4. normal map
			// 5. bump map
			// 6. roughnessMap map
			// 7. metalnessMap map
			// 8. alphaMap map
			// 9. emissiveMap map
			// 10. clearcoat map
			// 11. clearcoat normal map
			// 12. clearcoat roughnessMap map

			let uvScaleMap;

			if ( material.map ) {

				uvScaleMap = material.map;

			} else if ( material.specularMap ) {

				uvScaleMap = material.specularMap;

			} else if ( material.displacementMap ) {

				uvScaleMap = material.displacementMap;

			} else if ( material.normalMap ) {

				uvScaleMap = material.normalMap;

			} else if ( material.bumpMap ) {

				uvScaleMap = material.bumpMap;

			} else if ( material.roughnessMap ) {

				uvScaleMap = material.roughnessMap;

			} else if ( material.metalnessMap ) {

				uvScaleMap = material.metalnessMap;

			} else if ( material.alphaMap ) {

				uvScaleMap = material.alphaMap;

			} else if ( material.emissiveMap ) {

				uvScaleMap = material.emissiveMap;

			} else if ( material.clearcoatMap ) {

				uvScaleMap = material.clearcoatMap;

			} else if ( material.clearcoatNormalMap ) {

				uvScaleMap = material.clearcoatNormalMap;

			} else if ( material.clearcoatRoughnessMap ) {

				uvScaleMap = material.clearcoatRoughnessMap;

			}

			if ( uvScaleMap !== undefined ) {

				// backwards compatibility
				if ( uvScaleMap.isWebGLRenderTarget ) {

					uvScaleMap = uvScaleMap.texture;

				}

				if ( uvScaleMap.matrixAutoUpdate === true ) {

					uvScaleMap.updateMatrix();

				}

				uniforms.uvTransform.value.copy( uvScaleMap.matrix );

			}

			// uv repeat and offset setting priorities for uv2
			// 1. ao map
			// 2. light map

			let uv2ScaleMap;

			if ( material.aoMap ) {

				uv2ScaleMap = material.aoMap;

			} else if ( material.lightMap ) {

				uv2ScaleMap = material.lightMap;

			}

			if ( uv2ScaleMap !== undefined ) {

				// backwards compatibility
				if ( uv2ScaleMap.isWebGLRenderTarget ) {

					uv2ScaleMap = uv2ScaleMap.texture;

				}

				if ( uv2ScaleMap.matrixAutoUpdate === true ) {

					uv2ScaleMap.updateMatrix();

				}

				uniforms.uv2Transform.value.copy( uv2ScaleMap.matrix );

			}

		}

		function refreshUniformsLine( uniforms, material ) {

			uniforms.diffuse.value.copy( material.color );
			uniforms.opacity.value = material.opacity;

		}

		function refreshUniformsDash( uniforms, material ) {

			uniforms.dashSize.value = material.dashSize;
			uniforms.totalSize.value = material.dashSize + material.gapSize;
			uniforms.scale.value = material.scale;

		}

		function refreshUniformsPoints( uniforms, material, pixelRatio, height ) {

			uniforms.diffuse.value.copy( material.color );
			uniforms.opacity.value = material.opacity;
			uniforms.size.value = material.size * pixelRatio;
			uniforms.scale.value = height * 0.5;

			if ( material.map ) {

				uniforms.map.value = material.map;

			}

			if ( material.alphaMap ) {

				uniforms.alphaMap.value = material.alphaMap;

			}

			// uv repeat and offset setting priorities
			// 1. color map
			// 2. alpha map

			let uvScaleMap;

			if ( material.map ) {

				uvScaleMap = material.map;

			} else if ( material.alphaMap ) {

				uvScaleMap = material.alphaMap;

			}

			if ( uvScaleMap !== undefined ) {

				if ( uvScaleMap.matrixAutoUpdate === true ) {

					uvScaleMap.updateMatrix();

				}

				uniforms.uvTransform.value.copy( uvScaleMap.matrix );

			}

		}

		function refreshUniformsSprites( uniforms, material ) {

			uniforms.diffuse.value.copy( material.color );
			uniforms.opacity.value = material.opacity;
			uniforms.rotation.value = material.rotation;

			if ( material.map ) {

				uniforms.map.value = material.map;

			}

			if ( material.alphaMap ) {

				uniforms.alphaMap.value = material.alphaMap;

			}

			// uv repeat and offset setting priorities
			// 1. color map
			// 2. alpha map

			let uvScaleMap;

			if ( material.map ) {

				uvScaleMap = material.map;

			} else if ( material.alphaMap ) {

				uvScaleMap = material.alphaMap;

			}

			if ( uvScaleMap !== undefined ) {

				if ( uvScaleMap.matrixAutoUpdate === true ) {

					uvScaleMap.updateMatrix();

				}

				uniforms.uvTransform.value.copy( uvScaleMap.matrix );

			}

		}

		function refreshUniformsLambert( uniforms, material ) {

			if ( material.emissiveMap ) {

				uniforms.emissiveMap.value = material.emissiveMap;

			}

		}

		function refreshUniformsPhong( uniforms, material ) {

			uniforms.specular.value.copy( material.specular );
			uniforms.shininess.value = Math.max( material.shininess, 1e-4 ); // to prevent pow( 0.0, 0.0 )

			if ( material.emissiveMap ) {

				uniforms.emissiveMap.value = material.emissiveMap;

			}

			if ( material.bumpMap ) {

				uniforms.bumpMap.value = material.bumpMap;
				uniforms.bumpScale.value = material.bumpScale;
				if ( material.side === BackSide ) uniforms.bumpScale.value *= - 1;

			}

			if ( material.normalMap ) {

				uniforms.normalMap.value = material.normalMap;
				uniforms.normalScale.value.copy( material.normalScale );
				if ( material.side === BackSide ) uniforms.normalScale.value.negate();

			}

			if ( material.displacementMap ) {

				uniforms.displacementMap.value = material.displacementMap;
				uniforms.displacementScale.value = material.displacementScale;
				uniforms.displacementBias.value = material.displacementBias;

			}

		}

		function refreshUniformsToon( uniforms, material ) {

			if ( material.gradientMap ) {

				uniforms.gradientMap.value = material.gradientMap;

			}

			if ( material.emissiveMap ) {

				uniforms.emissiveMap.value = material.emissiveMap;

			}

			if ( material.bumpMap ) {

				uniforms.bumpMap.value = material.bumpMap;
				uniforms.bumpScale.value = material.bumpScale;
				if ( material.side === BackSide ) uniforms.bumpScale.value *= - 1;

			}

			if ( material.normalMap ) {

				uniforms.normalMap.value = material.normalMap;
				uniforms.normalScale.value.copy( material.normalScale );
				if ( material.side === BackSide ) uniforms.normalScale.value.negate();

			}

			if ( material.displacementMap ) {

				uniforms.displacementMap.value = material.displacementMap;
				uniforms.displacementScale.value = material.displacementScale;
				uniforms.displacementBias.value = material.displacementBias;

			}

		}

		function refreshUniformsStandard( uniforms, material ) {

			uniforms.roughness.value = material.roughness;
			uniforms.metalness.value = material.metalness;

			if ( material.roughnessMap ) {

				uniforms.roughnessMap.value = material.roughnessMap;

			}

			if ( material.metalnessMap ) {

				uniforms.metalnessMap.value = material.metalnessMap;

			}

			if ( material.emissiveMap ) {

				uniforms.emissiveMap.value = material.emissiveMap;

			}

			if ( material.bumpMap ) {

				uniforms.bumpMap.value = material.bumpMap;
				uniforms.bumpScale.value = material.bumpScale;
				if ( material.side === BackSide ) uniforms.bumpScale.value *= - 1;

			}

			if ( material.normalMap ) {

				uniforms.normalMap.value = material.normalMap;
				uniforms.normalScale.value.copy( material.normalScale );
				if ( material.side === BackSide ) uniforms.normalScale.value.negate();

			}

			if ( material.displacementMap ) {

				uniforms.displacementMap.value = material.displacementMap;
				uniforms.displacementScale.value = material.displacementScale;
				uniforms.displacementBias.value = material.displacementBias;

			}

			const envMap = properties.get( material ).envMap;

			if ( envMap ) {

				//uniforms.envMap.value = material.envMap; // part of uniforms common
				uniforms.envMapIntensity.value = material.envMapIntensity;

			}

		}

		function refreshUniformsPhysical( uniforms, material ) {

			refreshUniformsStandard( uniforms, material );

			uniforms.reflectivity.value = material.reflectivity; // also part of uniforms common

			uniforms.clearcoat.value = material.clearcoat;
			uniforms.clearcoatRoughness.value = material.clearcoatRoughness;
			if ( material.sheen ) uniforms.sheen.value.copy( material.sheen );

			if ( material.clearcoatMap ) {

				uniforms.clearcoatMap.value = material.clearcoatMap;

			}

			if ( material.clearcoatRoughnessMap ) {

				uniforms.clearcoatRoughnessMap.value = material.clearcoatRoughnessMap;

			}

			if ( material.clearcoatNormalMap ) {

				uniforms.clearcoatNormalScale.value.copy( material.clearcoatNormalScale );
				uniforms.clearcoatNormalMap.value = material.clearcoatNormalMap;

				if ( material.side === BackSide ) {

					uniforms.clearcoatNormalScale.value.negate();

				}

			}

			uniforms.transmission.value = material.transmission;

			if ( material.transmissionMap ) {

				uniforms.transmissionMap.value = material.transmissionMap;

			}

		}

		function refreshUniformsMatcap( uniforms, material ) {

			if ( material.matcap ) {

				uniforms.matcap.value = material.matcap;

			}

			if ( material.bumpMap ) {

				uniforms.bumpMap.value = material.bumpMap;
				uniforms.bumpScale.value = material.bumpScale;
				if ( material.side === BackSide ) uniforms.bumpScale.value *= - 1;

			}

			if ( material.normalMap ) {

				uniforms.normalMap.value = material.normalMap;
				uniforms.normalScale.value.copy( material.normalScale );
				if ( material.side === BackSide ) uniforms.normalScale.value.negate();

			}

			if ( material.displacementMap ) {

				uniforms.displacementMap.value = material.displacementMap;
				uniforms.displacementScale.value = material.displacementScale;
				uniforms.displacementBias.value = material.displacementBias;

			}

		}

		function refreshUniformsDepth( uniforms, material ) {

			if ( material.displacementMap ) {

				uniforms.displacementMap.value = material.displacementMap;
				uniforms.displacementScale.value = material.displacementScale;
				uniforms.displacementBias.value = material.displacementBias;

			}

		}

		function refreshUniformsDistance( uniforms, material ) {

			if ( material.displacementMap ) {

				uniforms.displacementMap.value = material.displacementMap;
				uniforms.displacementScale.value = material.displacementScale;
				uniforms.displacementBias.value = material.displacementBias;

			}

			uniforms.referencePosition.value.copy( material.referencePosition );
			uniforms.nearDistance.value = material.nearDistance;
			uniforms.farDistance.value = material.farDistance;

		}

		function refreshUniformsNormal( uniforms, material ) {

			if ( material.bumpMap ) {

				uniforms.bumpMap.value = material.bumpMap;
				uniforms.bumpScale.value = material.bumpScale;
				if ( material.side === BackSide ) uniforms.bumpScale.value *= - 1;

			}

			if ( material.normalMap ) {

				uniforms.normalMap.value = material.normalMap;
				uniforms.normalScale.value.copy( material.normalScale );
				if ( material.side === BackSide ) uniforms.normalScale.value.negate();

			}

			if ( material.displacementMap ) {

				uniforms.displacementMap.value = material.displacementMap;
				uniforms.displacementScale.value = material.displacementScale;
				uniforms.displacementBias.value = material.displacementBias;

			}

		}

		return {
			refreshFogUniforms: refreshFogUniforms,
			refreshMaterialUniforms: refreshMaterialUniforms
		};

	}

	function createCanvasElement() {

		const canvas = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'canvas' );
		canvas.style.display = 'block';
		return canvas;

	}

	function WebGLRenderer( parameters ) {

		parameters = parameters || {};

		const _canvas = parameters.canvas !== undefined ? parameters.canvas : createCanvasElement(),
			_context = parameters.context !== undefined ? parameters.context : null,

			_alpha = parameters.alpha !== undefined ? parameters.alpha : false,
			_depth = parameters.depth !== undefined ? parameters.depth : true,
			_stencil = parameters.stencil !== undefined ? parameters.stencil : true,
			_antialias = parameters.antialias !== undefined ? parameters.antialias : false,
			_premultipliedAlpha = parameters.premultipliedAlpha !== undefined ? parameters.premultipliedAlpha : true,
			_preserveDrawingBuffer = parameters.preserveDrawingBuffer !== undefined ? parameters.preserveDrawingBuffer : false,
			_powerPreference = parameters.powerPreference !== undefined ? parameters.powerPreference : 'default',
			_failIfMajorPerformanceCaveat = parameters.failIfMajorPerformanceCaveat !== undefined ? parameters.failIfMajorPerformanceCaveat : false;

		let currentRenderList = null;
		let currentRenderState = null;

		// render() can be called from within a callback triggered by another render.
		// We track this so that the nested render call gets its state isolated from the parent render call.

		const renderStateStack = [];

		// public properties

		this.domElement = _canvas;

		// Debug configuration container
		this.debug = {

			/**
			 * Enables error checking and reporting when shader programs are being compiled
			 * @type {boolean}
			 */
			checkShaderErrors: true
		};

		// clearing

		this.autoClear = true;
		this.autoClearColor = true;
		this.autoClearDepth = true;
		this.autoClearStencil = true;

		// scene graph

		this.sortObjects = true;

		// user-defined clipping

		this.clippingPlanes = [];
		this.localClippingEnabled = false;

		// physically based shading

		this.gammaFactor = 2.0;	// for backwards compatibility
		this.outputEncoding = LinearEncoding;

		// physical lights

		this.physicallyCorrectLights = false;

		// tone mapping

		this.toneMapping = NoToneMapping;
		this.toneMappingExposure = 1.0;

		// morphs

		this.maxMorphTargets = 8;
		this.maxMorphNormals = 4;

		// internal properties

		const _this = this;

		let _isContextLost = false;

		// internal state cache

		let _framebuffer = null;

		let _currentActiveCubeFace = 0;
		let _currentActiveMipmapLevel = 0;
		let _currentRenderTarget = null;
		let _currentFramebuffer = null;
		let _currentMaterialId = - 1;

		let _currentCamera = null;

		const _currentViewport = new Vector4();
		const _currentScissor = new Vector4();
		let _currentScissorTest = null;

		//

		let _width = _canvas.width;
		let _height = _canvas.height;

		let _pixelRatio = 1;
		let _opaqueSort = null;
		let _transparentSort = null;

		const _viewport = new Vector4( 0, 0, _width, _height );
		const _scissor = new Vector4( 0, 0, _width, _height );
		let _scissorTest = false;

		// frustum

		const _frustum = new Frustum();

		// clipping

		let _clippingEnabled = false;
		let _localClippingEnabled = false;

		// camera matrices cache

		const _projScreenMatrix = new Matrix4();

		const _vector3 = new Vector3();

		const _emptyScene = { background: null, fog: null, environment: null, overrideMaterial: null, isScene: true };

		function getTargetPixelRatio() {

			return _currentRenderTarget === null ? _pixelRatio : 1;

		}

		// initialize

		let _gl = _context;

		function getContext( contextNames, contextAttributes ) {

			for ( let i = 0; i < contextNames.length; i ++ ) {

				const contextName = contextNames[ i ];
				const context = _canvas.getContext( contextName, contextAttributes );
				if ( context !== null ) return context;

			}

			return null;

		}

		try {

			const contextAttributes = {
				alpha: _alpha,
				depth: _depth,
				stencil: _stencil,
				antialias: _antialias,
				premultipliedAlpha: _premultipliedAlpha,
				preserveDrawingBuffer: _preserveDrawingBuffer,
				powerPreference: _powerPreference,
				failIfMajorPerformanceCaveat: _failIfMajorPerformanceCaveat
			};

			// event listeners must be registered before WebGL context is created, see #12753

			_canvas.addEventListener( 'webglcontextlost', onContextLost, false );
			_canvas.addEventListener( 'webglcontextrestored', onContextRestore, false );

			if ( _gl === null ) {

				const contextNames = [ 'webgl2', 'webgl', 'experimental-webgl' ];

				if ( _this.isWebGL1Renderer === true ) {

					contextNames.shift();

				}

				_gl = getContext( contextNames, contextAttributes );

				if ( _gl === null ) {

					if ( getContext( contextNames ) ) {

						throw new Error( 'Error creating WebGL context with your selected attributes.' );

					} else {

						throw new Error( 'Error creating WebGL context.' );

					}

				}

			}

			// Some experimental-webgl implementations do not have getShaderPrecisionFormat

			if ( _gl.getShaderPrecisionFormat === undefined ) {

				_gl.getShaderPrecisionFormat = function () {

					return { 'rangeMin': 1, 'rangeMax': 1, 'precision': 1 };

				};

			}

		} catch ( error ) {

			console.error( 'THREE.WebGLRenderer: ' + error.message );
			throw error;

		}

		let extensions, capabilities, state, info;
		let properties, textures, cubemaps, attributes, geometries, objects;
		let programCache, materials, renderLists, renderStates, clipping;

		let background, morphtargets, bufferRenderer, indexedBufferRenderer;

		let utils, bindingStates;

		function initGLContext() {

			extensions = new WebGLExtensions( _gl );

			capabilities = new WebGLCapabilities( _gl, extensions, parameters );

			if ( capabilities.isWebGL2 === false ) {

				extensions.get( 'WEBGL_depth_texture' );
				extensions.get( 'OES_texture_float' );
				extensions.get( 'OES_texture_half_float' );
				extensions.get( 'OES_texture_half_float_linear' );
				extensions.get( 'OES_standard_derivatives' );
				extensions.get( 'OES_element_index_uint' );
				extensions.get( 'OES_vertex_array_object' );
				extensions.get( 'ANGLE_instanced_arrays' );

			}

			extensions.get( 'OES_texture_float_linear' );

			utils = new WebGLUtils( _gl, extensions, capabilities );

			state = new WebGLState( _gl, extensions, capabilities );
			state.scissor( _currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ).floor() );
			state.viewport( _currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ).floor() );

			info = new WebGLInfo( _gl );
			properties = new WebGLProperties();
			textures = new WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info );
			cubemaps = new WebGLCubeMaps( _this );
			attributes = new WebGLAttributes( _gl, capabilities );
			bindingStates = new WebGLBindingStates( _gl, extensions, attributes, capabilities );
			geometries = new WebGLGeometries( _gl, attributes, info, bindingStates );
			objects = new WebGLObjects( _gl, geometries, attributes, info );
			morphtargets = new WebGLMorphtargets( _gl );
			clipping = new WebGLClipping( properties );
			programCache = new WebGLPrograms( _this, cubemaps, extensions, capabilities, bindingStates, clipping );
			materials = new WebGLMaterials( properties );
			renderLists = new WebGLRenderLists( properties );
			renderStates = new WebGLRenderStates( extensions, capabilities );
			background = new WebGLBackground( _this, cubemaps, state, objects, _premultipliedAlpha );

			bufferRenderer = new WebGLBufferRenderer( _gl, extensions, info, capabilities );
			indexedBufferRenderer = new WebGLIndexedBufferRenderer( _gl, extensions, info, capabilities );

			info.programs = programCache.programs;

			_this.capabilities = capabilities;
			_this.extensions = extensions;
			_this.properties = properties;
			_this.renderLists = renderLists;
			_this.state = state;
			_this.info = info;

		}

		initGLContext();

		// xr

		const xr = new WebXRManager( _this, _gl );

		this.xr = xr;

		// shadow map

		const shadowMap = new WebGLShadowMap( _this, objects, capabilities.maxTextureSize );

		this.shadowMap = shadowMap;

		// API

		this.getContext = function () {

			return _gl;

		};

		this.getContextAttributes = function () {

			return _gl.getContextAttributes();

		};

		this.forceContextLoss = function () {

			const extension = extensions.get( 'WEBGL_lose_context' );
			if ( extension ) extension.loseContext();

		};

		this.forceContextRestore = function () {

			const extension = extensions.get( 'WEBGL_lose_context' );
			if ( extension ) extension.restoreContext();

		};

		this.getPixelRatio = function () {

			return _pixelRatio;

		};

		this.setPixelRatio = function ( value ) {

			if ( value === undefined ) return;

			_pixelRatio = value;

			this.setSize( _width, _height, false );

		};

		this.getSize = function ( target ) {

			if ( target === undefined ) {

				console.warn( 'WebGLRenderer: .getsize() now requires a Vector2 as an argument' );

				target = new Vector2();

			}

			return target.set( _width, _height );

		};

		this.setSize = function ( width, height, updateStyle ) {

			if ( xr.isPresenting ) {

				console.warn( 'THREE.WebGLRenderer: Can\'t change size while VR device is presenting.' );
				return;

			}

			_width = width;
			_height = height;

			_canvas.width = Math.floor( width * _pixelRatio );
			_canvas.height = Math.floor( height * _pixelRatio );

			if ( updateStyle !== false ) {

				_canvas.style.width = width + 'px';
				_canvas.style.height = height + 'px';

			}

			this.setViewport( 0, 0, width, height );

		};

		this.getDrawingBufferSize = function ( target ) {

			if ( target === undefined ) {

				console.warn( 'WebGLRenderer: .getdrawingBufferSize() now requires a Vector2 as an argument' );

				target = new Vector2();

			}

			return target.set( _width * _pixelRatio, _height * _pixelRatio ).floor();

		};

		this.setDrawingBufferSize = function ( width, height, pixelRatio ) {

			_width = width;
			_height = height;

			_pixelRatio = pixelRatio;

			_canvas.width = Math.floor( width * pixelRatio );
			_canvas.height = Math.floor( height * pixelRatio );

			this.setViewport( 0, 0, width, height );

		};

		this.getCurrentViewport = function ( target ) {

			if ( target === undefined ) {

				console.warn( 'WebGLRenderer: .getCurrentViewport() now requires a Vector4 as an argument' );

				target = new Vector4();

			}

			return target.copy( _currentViewport );

		};

		this.getViewport = function ( target ) {

			return target.copy( _viewport );

		};

		this.setViewport = function ( x, y, width, height ) {

			if ( x.isVector4 ) {

				_viewport.set( x.x, x.y, x.z, x.w );

			} else {

				_viewport.set( x, y, width, height );

			}

			state.viewport( _currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ).floor() );

		};

		this.getScissor = function ( target ) {

			return target.copy( _scissor );

		};

		this.setScissor = function ( x, y, width, height ) {

			if ( x.isVector4 ) {

				_scissor.set( x.x, x.y, x.z, x.w );

			} else {

				_scissor.set( x, y, width, height );

			}

			state.scissor( _currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ).floor() );

		};

		this.getScissorTest = function () {

			return _scissorTest;

		};

		this.setScissorTest = function ( boolean ) {

			state.setScissorTest( _scissorTest = boolean );

		};

		this.setOpaqueSort = function ( method ) {

			_opaqueSort = method;

		};

		this.setTransparentSort = function ( method ) {

			_transparentSort = method;

		};

		// Clearing

		this.getClearColor = function ( target ) {

			if ( target === undefined ) {

				console.warn( 'WebGLRenderer: .getClearColor() now requires a Color as an argument' );

				target = new Color();

			}

			return target.copy( background.getClearColor() );

		};

		this.setClearColor = function () {

			background.setClearColor.apply( background, arguments );

		};

		this.getClearAlpha = function () {

			return background.getClearAlpha();

		};

		this.setClearAlpha = function () {

			background.setClearAlpha.apply( background, arguments );

		};

		this.clear = function ( color, depth, stencil ) {

			let bits = 0;

			if ( color === undefined || color ) bits |= 16384;
			if ( depth === undefined || depth ) bits |= 256;
			if ( stencil === undefined || stencil ) bits |= 1024;

			_gl.clear( bits );

		};

		this.clearColor = function () {

			this.clear( true, false, false );

		};

		this.clearDepth = function () {

			this.clear( false, true, false );

		};

		this.clearStencil = function () {

			this.clear( false, false, true );

		};

		//

		this.dispose = function () {

			_canvas.removeEventListener( 'webglcontextlost', onContextLost, false );
			_canvas.removeEventListener( 'webglcontextrestored', onContextRestore, false );

			renderLists.dispose();
			renderStates.dispose();
			properties.dispose();
			cubemaps.dispose();
			objects.dispose();
			bindingStates.dispose();

			xr.dispose();

			animation.stop();

		};

		// Events

		function onContextLost( event ) {

			event.preventDefault();

			console.log( 'THREE.WebGLRenderer: Context Lost.' );

			_isContextLost = true;

		}

		function onContextRestore( /* event */ ) {

			console.log( 'THREE.WebGLRenderer: Context Restored.' );

			_isContextLost = false;

			initGLContext();

		}

		function onMaterialDispose( event ) {

			const material = event.target;

			material.removeEventListener( 'dispose', onMaterialDispose );

			deallocateMaterial( material );

		}

		// Buffer deallocation

		function deallocateMaterial( material ) {

			releaseMaterialProgramReference( material );

			properties.remove( material );

		}


		function releaseMaterialProgramReference( material ) {

			const programInfo = properties.get( material ).program;

			if ( programInfo !== undefined ) {

				programCache.releaseProgram( programInfo );

			}

		}

		// Buffer rendering

		function renderObjectImmediate( object, program ) {

			object.render( function ( object ) {

				_this.renderBufferImmediate( object, program );

			} );

		}

		this.renderBufferImmediate = function ( object, program ) {

			bindingStates.initAttributes();

			const buffers = properties.get( object );

			if ( object.hasPositions && ! buffers.position ) buffers.position = _gl.createBuffer();
			if ( object.hasNormals && ! buffers.normal ) buffers.normal = _gl.createBuffer();
			if ( object.hasUvs && ! buffers.uv ) buffers.uv = _gl.createBuffer();
			if ( object.hasColors && ! buffers.color ) buffers.color = _gl.createBuffer();

			const programAttributes = program.getAttributes();

			if ( object.hasPositions ) {

				_gl.bindBuffer( 34962, buffers.position );
				_gl.bufferData( 34962, object.positionArray, 35048 );

				bindingStates.enableAttribute( programAttributes.position );
				_gl.vertexAttribPointer( programAttributes.position, 3, 5126, false, 0, 0 );

			}

			if ( object.hasNormals ) {

				_gl.bindBuffer( 34962, buffers.normal );
				_gl.bufferData( 34962, object.normalArray, 35048 );

				bindingStates.enableAttribute( programAttributes.normal );
				_gl.vertexAttribPointer( programAttributes.normal, 3, 5126, false, 0, 0 );

			}

			if ( object.hasUvs ) {

				_gl.bindBuffer( 34962, buffers.uv );
				_gl.bufferData( 34962, object.uvArray, 35048 );

				bindingStates.enableAttribute( programAttributes.uv );
				_gl.vertexAttribPointer( programAttributes.uv, 2, 5126, false, 0, 0 );

			}

			if ( object.hasColors ) {

				_gl.bindBuffer( 34962, buffers.color );
				_gl.bufferData( 34962, object.colorArray, 35048 );

				bindingStates.enableAttribute( programAttributes.color );
				_gl.vertexAttribPointer( programAttributes.color, 3, 5126, false, 0, 0 );

			}

			bindingStates.disableUnusedAttributes();

			_gl.drawArrays( 4, 0, object.count );

			object.count = 0;

		};

		this.renderBufferDirect = function ( camera, scene, geometry, material, object, group ) {

			if ( scene === null ) scene = _emptyScene; // renderBufferDirect second parameter used to be fog (could be null)

			const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant() < 0 );

			const program = setProgram( camera, scene, material, object );

			state.setMaterial( material, frontFaceCW );

			//

			let index = geometry.index;
			const position = geometry.attributes.position;

			//

			if ( index === null ) {

				if ( position === undefined || position.count === 0 ) return;

			} else if ( index.count === 0 ) {

				return;

			}

			//

			let rangeFactor = 1;

			if ( material.wireframe === true ) {

				index = geometries.getWireframeAttribute( geometry );
				rangeFactor = 2;

			}

			if ( material.morphTargets || material.morphNormals ) {

				morphtargets.update( object, geometry, material, program );

			}

			bindingStates.setup( object, material, program, geometry, index );

			let attribute;
			let renderer = bufferRenderer;

			if ( index !== null ) {

				attribute = attributes.get( index );

				renderer = indexedBufferRenderer;
				renderer.setIndex( attribute );

			}

			//

			const dataCount = ( index !== null ) ? index.count : position.count;

			const rangeStart = geometry.drawRange.start * rangeFactor;
			const rangeCount = geometry.drawRange.count * rangeFactor;

			const groupStart = group !== null ? group.start * rangeFactor : 0;
			const groupCount = group !== null ? group.count * rangeFactor : Infinity;

			const drawStart = Math.max( rangeStart, groupStart );
			const drawEnd = Math.min( dataCount, rangeStart + rangeCount, groupStart + groupCount ) - 1;

			const drawCount = Math.max( 0, drawEnd - drawStart + 1 );

			if ( drawCount === 0 ) return;

			//

			if ( object.isMesh ) {

				if ( material.wireframe === true ) {

					state.setLineWidth( material.wireframeLinewidth * getTargetPixelRatio() );
					renderer.setMode( 1 );

				} else {

					renderer.setMode( 4 );

				}

			} else if ( object.isLine ) {

				let lineWidth = material.linewidth;

				if ( lineWidth === undefined ) lineWidth = 1; // Not using Line*Material

				state.setLineWidth( lineWidth * getTargetPixelRatio() );

				if ( object.isLineSegments ) {

					renderer.setMode( 1 );

				} else if ( object.isLineLoop ) {

					renderer.setMode( 2 );

				} else {

					renderer.setMode( 3 );

				}

			} else if ( object.isPoints ) {

				renderer.setMode( 0 );

			} else if ( object.isSprite ) {

				renderer.setMode( 4 );

			}

			if ( object.isInstancedMesh ) {

				renderer.renderInstances( drawStart, drawCount, object.count );

			} else if ( geometry.isInstancedBufferGeometry ) {

				const instanceCount = Math.min( geometry.instanceCount, geometry._maxInstanceCount );

				renderer.renderInstances( drawStart, drawCount, instanceCount );

			} else {

				renderer.render( drawStart, drawCount );

			}

		};

		// Compile

		this.compile = function ( scene, camera ) {

			currentRenderState = renderStates.get( scene );
			currentRenderState.init();

			scene.traverseVisible( function ( object ) {

				if ( object.isLight && object.layers.test( camera.layers ) ) {

					currentRenderState.pushLight( object );

					if ( object.castShadow ) {

						currentRenderState.pushShadow( object );

					}

				}

			} );

			currentRenderState.setupLights();

			const compiled = new WeakMap();

			scene.traverse( function ( object ) {

				const material = object.material;

				if ( material ) {

					if ( Array.isArray( material ) ) {

						for ( let i = 0; i < material.length; i ++ ) {

							const material2 = material[ i ];

							if ( compiled.has( material2 ) === false ) {

								initMaterial( material2, scene, object );
								compiled.set( material2 );

							}

						}

					} else if ( compiled.has( material ) === false ) {

						initMaterial( material, scene, object );
						compiled.set( material );

					}

				}

			} );

		};

		// Animation Loop

		let onAnimationFrameCallback = null;

		function onAnimationFrame( time ) {

			if ( xr.isPresenting ) return;
			if ( onAnimationFrameCallback ) onAnimationFrameCallback( time );

		}

		const animation = new WebGLAnimation();
		animation.setAnimationLoop( onAnimationFrame );

		if ( typeof window !== 'undefined' ) animation.setContext( window );

		this.setAnimationLoop = function ( callback ) {

			onAnimationFrameCallback = callback;
			xr.setAnimationLoop( callback );

			( callback === null ) ? animation.stop() : animation.start();

		};

		// Rendering

		this.render = function ( scene, camera ) {

			let renderTarget, forceClear;

			if ( arguments[ 2 ] !== undefined ) {

				console.warn( 'THREE.WebGLRenderer.render(): the renderTarget argument has been removed. Use .setRenderTarget() instead.' );
				renderTarget = arguments[ 2 ];

			}

			if ( arguments[ 3 ] !== undefined ) {

				console.warn( 'THREE.WebGLRenderer.render(): the forceClear argument has been removed. Use .clear() instead.' );
				forceClear = arguments[ 3 ];

			}

			if ( camera !== undefined && camera.isCamera !== true ) {

				console.error( 'THREE.WebGLRenderer.render: camera is not an instance of THREE.Camera.' );
				return;

			}

			if ( _isContextLost === true ) return;

			// reset caching for this frame

			bindingStates.resetDefaultState();
			_currentMaterialId = - 1;
			_currentCamera = null;

			// update scene graph

			if ( scene.autoUpdate === true ) scene.updateMatrixWorld();

			// update camera matrices and frustum

			if ( camera.parent === null ) camera.updateMatrixWorld();

			if ( xr.enabled === true && xr.isPresenting === true ) {

				camera = xr.getCamera( camera );

			}

			//
			if ( scene.isScene === true ) scene.onBeforeRender( _this, scene, camera, renderTarget || _currentRenderTarget );

			currentRenderState = renderStates.get( scene, renderStateStack.length );
			currentRenderState.init();

			renderStateStack.push( currentRenderState );

			_projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
			_frustum.setFromProjectionMatrix( _projScreenMatrix );

			_localClippingEnabled = this.localClippingEnabled;
			_clippingEnabled = clipping.init( this.clippingPlanes, _localClippingEnabled, camera );

			currentRenderList = renderLists.get( scene, camera );
			currentRenderList.init();

			projectObject( scene, camera, 0, _this.sortObjects );

			currentRenderList.finish();

			if ( _this.sortObjects === true ) {

				currentRenderList.sort( _opaqueSort, _transparentSort );

			}

			//

			if ( _clippingEnabled === true ) clipping.beginShadows();

			const shadowsArray = currentRenderState.state.shadowsArray;

			shadowMap.render( shadowsArray, scene, camera );

			currentRenderState.setupLights();
			currentRenderState.setupLightsView( camera );

			if ( _clippingEnabled === true ) clipping.endShadows();

			//

			if ( this.info.autoReset === true ) this.info.reset();

			if ( renderTarget !== undefined ) {

				this.setRenderTarget( renderTarget );

			}

			//

			background.render( currentRenderList, scene, camera, forceClear );

			// render scene

			const opaqueObjects = currentRenderList.opaque;
			const transparentObjects = currentRenderList.transparent;

			if ( opaqueObjects.length > 0 ) renderObjects( opaqueObjects, scene, camera );
			if ( transparentObjects.length > 0 ) renderObjects( transparentObjects, scene, camera );

			//

			if ( scene.isScene === true ) scene.onAfterRender( _this, scene, camera );

			//

			if ( _currentRenderTarget !== null ) {

				// Generate mipmap if we're using any kind of mipmap filtering

				textures.updateRenderTargetMipmap( _currentRenderTarget );

				// resolve multisample renderbuffers to a single-sample texture if necessary

				textures.updateMultisampleRenderTarget( _currentRenderTarget );

			}

			// Ensure depth buffer writing is enabled so it can be cleared on next render

			state.buffers.depth.setTest( true );
			state.buffers.depth.setMask( true );
			state.buffers.color.setMask( true );

			state.setPolygonOffset( false );

			// _gl.finish();

			renderStateStack.pop();
			if ( renderStateStack.length > 0 ) {

				currentRenderState = renderStateStack[ renderStateStack.length - 1 ];

			} else {

				currentRenderState = null;

			}

			currentRenderList = null;

		};

		function projectObject( object, camera, groupOrder, sortObjects ) {

			if ( object.visible === false ) return;

			const visible = object.layers.test( camera.layers );

			if ( visible ) {

				if ( object.isGroup ) {

					groupOrder = object.renderOrder;

				} else if ( object.isLOD ) {

					if ( object.autoUpdate === true ) object.update( camera );

				} else if ( object.isLight ) {

					currentRenderState.pushLight( object );

					if ( object.castShadow ) {

						currentRenderState.pushShadow( object );

					}

				} else if ( object.isSprite ) {

					if ( ! object.frustumCulled || _frustum.intersectsSprite( object ) ) {

						if ( sortObjects ) {

							_vector3.setFromMatrixPosition( object.matrixWorld )
								.applyMatrix4( _projScreenMatrix );

						}

						const geometry = objects.update( object );
						const material = object.material;

						if ( material.visible ) {

							currentRenderList.push( object, geometry, material, groupOrder, _vector3.z, null );

						}

					}

				} else if ( object.isImmediateRenderObject ) {

					if ( sortObjects ) {

						_vector3.setFromMatrixPosition( object.matrixWorld )
							.applyMatrix4( _projScreenMatrix );

					}

					currentRenderList.push( object, null, object.material, groupOrder, _vector3.z, null );

				} else if ( object.isMesh || object.isLine || object.isPoints ) {

					if ( object.isSkinnedMesh ) {

						// update skeleton only once in a frame

						if ( object.skeleton.frame !== info.render.frame ) {

							object.skeleton.update();
							object.skeleton.frame = info.render.frame;

						}

					}

					if ( ! object.frustumCulled || _frustum.intersectsObject( object ) ) {

						if ( sortObjects ) {

							_vector3.setFromMatrixPosition( object.matrixWorld )
								.applyMatrix4( _projScreenMatrix );

						}

						const geometry = objects.update( object );
						const material = object.material;

						if ( Array.isArray( material ) ) {

							const groups = geometry.groups;

							for ( let i = 0, l = groups.length; i < l; i ++ ) {

								const group = groups[ i ];
								const groupMaterial = material[ group.materialIndex ];

								if ( groupMaterial && groupMaterial.visible ) {

									currentRenderList.push( object, geometry, groupMaterial, groupOrder, _vector3.z, group );

								}

							}

						} else if ( material.visible ) {

							currentRenderList.push( object, geometry, material, groupOrder, _vector3.z, null );

						}

					}

				}

			}

			const children = object.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				projectObject( children[ i ], camera, groupOrder, sortObjects );

			}

		}

		function renderObjects( renderList, scene, camera ) {

			const overrideMaterial = scene.isScene === true ? scene.overrideMaterial : null;

			for ( let i = 0, l = renderList.length; i < l; i ++ ) {

				const renderItem = renderList[ i ];

				const object = renderItem.object;
				const geometry = renderItem.geometry;
				const material = overrideMaterial === null ? renderItem.material : overrideMaterial;
				const group = renderItem.group;

				if ( camera.isArrayCamera ) {

					const cameras = camera.cameras;

					for ( let j = 0, jl = cameras.length; j < jl; j ++ ) {

						const camera2 = cameras[ j ];

						if ( object.layers.test( camera2.layers ) ) {

							state.viewport( _currentViewport.copy( camera2.viewport ) );

							currentRenderState.setupLightsView( camera2 );

							renderObject( object, scene, camera2, geometry, material, group );

						}

					}

				} else {

					renderObject( object, scene, camera, geometry, material, group );

				}

			}

		}

		function renderObject( object, scene, camera, geometry, material, group ) {

			object.onBeforeRender( _this, scene, camera, geometry, material, group );

			object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
			object.normalMatrix.getNormalMatrix( object.modelViewMatrix );

			if ( object.isImmediateRenderObject ) {

				const program = setProgram( camera, scene, material, object );

				state.setMaterial( material );

				bindingStates.reset();

				renderObjectImmediate( object, program );

			} else {

				_this.renderBufferDirect( camera, scene, geometry, material, object, group );

			}

			object.onAfterRender( _this, scene, camera, geometry, material, group );

		}

		function initMaterial( material, scene, object ) {

			if ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ...

			const materialProperties = properties.get( material );

			const lights = currentRenderState.state.lights;
			const shadowsArray = currentRenderState.state.shadowsArray;

			const lightsStateVersion = lights.state.version;

			const parameters = programCache.getParameters( material, lights.state, shadowsArray, scene, object );
			const programCacheKey = programCache.getProgramCacheKey( parameters );

			let program = materialProperties.program;
			let programChange = true;

			if ( program === undefined ) {

				// new material
				material.addEventListener( 'dispose', onMaterialDispose );

			} else if ( program.cacheKey !== programCacheKey ) {

				// changed glsl or parameters
				releaseMaterialProgramReference( material );

			} else if ( materialProperties.lightsStateVersion !== lightsStateVersion ) {

				programChange = false;

			} else if ( parameters.shaderID !== undefined ) {

				// same glsl and uniform list, envMap still needs the update here to avoid a frame-late effect

				const environment = material.isMeshStandardMaterial ? scene.environment : null;
				materialProperties.envMap = cubemaps.get( material.envMap || environment );

				return;

			} else {

				// only rebuild uniform list
				programChange = false;

			}

			if ( programChange ) {

				parameters.uniforms = programCache.getUniforms( material );

				material.onBeforeCompile( parameters, _this );

				program = programCache.acquireProgram( parameters, programCacheKey );

				materialProperties.program = program;
				materialProperties.uniforms = parameters.uniforms;
				materialProperties.outputEncoding = parameters.outputEncoding;

			}

			const uniforms = materialProperties.uniforms;

			if ( ! material.isShaderMaterial &&
				! material.isRawShaderMaterial ||
				material.clipping === true ) {

				materialProperties.numClippingPlanes = clipping.numPlanes;
				materialProperties.numIntersection = clipping.numIntersection;
				uniforms.clippingPlanes = clipping.uniform;

			}

			materialProperties.environment = material.isMeshStandardMaterial ? scene.environment : null;
			materialProperties.fog = scene.fog;
			materialProperties.envMap = cubemaps.get( material.envMap || materialProperties.environment );

			// store the light setup it was created for

			materialProperties.needsLights = materialNeedsLights( material );
			materialProperties.lightsStateVersion = lightsStateVersion;

			if ( materialProperties.needsLights ) {

				// wire up the material to this renderer's lighting state

				uniforms.ambientLightColor.value = lights.state.ambient;
				uniforms.lightProbe.value = lights.state.probe;
				uniforms.directionalLights.value = lights.state.directional;
				uniforms.directionalLightShadows.value = lights.state.directionalShadow;
				uniforms.spotLights.value = lights.state.spot;
				uniforms.spotLightShadows.value = lights.state.spotShadow;
				uniforms.rectAreaLights.value = lights.state.rectArea;
				uniforms.ltc_1.value = lights.state.rectAreaLTC1;
				uniforms.ltc_2.value = lights.state.rectAreaLTC2;
				uniforms.pointLights.value = lights.state.point;
				uniforms.pointLightShadows.value = lights.state.pointShadow;
				uniforms.hemisphereLights.value = lights.state.hemi;

				uniforms.directionalShadowMap.value = lights.state.directionalShadowMap;
				uniforms.directionalShadowMatrix.value = lights.state.directionalShadowMatrix;
				uniforms.spotShadowMap.value = lights.state.spotShadowMap;
				uniforms.spotShadowMatrix.value = lights.state.spotShadowMatrix;
				uniforms.pointShadowMap.value = lights.state.pointShadowMap;
				uniforms.pointShadowMatrix.value = lights.state.pointShadowMatrix;
				// TODO (abelnation): add area lights shadow info to uniforms

			}

			const progUniforms = materialProperties.program.getUniforms();
			const uniformsList = WebGLUniforms.seqWithValue( progUniforms.seq, uniforms );

			materialProperties.uniformsList = uniformsList;

		}

		function setProgram( camera, scene, material, object ) {

			if ( scene.isScene !== true ) scene = _emptyScene; // scene could be a Mesh, Line, Points, ...

			textures.resetTextureUnits();

			const fog = scene.fog;
			const environment = material.isMeshStandardMaterial ? scene.environment : null;
			const encoding = ( _currentRenderTarget === null ) ? _this.outputEncoding : _currentRenderTarget.texture.encoding;
			const envMap = cubemaps.get( material.envMap || environment );

			const materialProperties = properties.get( material );
			const lights = currentRenderState.state.lights;

			if ( _clippingEnabled === true ) {

				if ( _localClippingEnabled === true || camera !== _currentCamera ) {

					const useCache =
						camera === _currentCamera &&
						material.id === _currentMaterialId;

					// we might want to call this function with some ClippingGroup
					// object instead of the material, once it becomes feasible
					// (#8465, #8379)
					clipping.setState( material, camera, useCache );

				}

			}

			if ( material.version === materialProperties.__version ) {

				if ( material.fog && materialProperties.fog !== fog ) {

					initMaterial( material, scene, object );

				} else if ( materialProperties.environment !== environment ) {

					initMaterial( material, scene, object );

				} else if ( materialProperties.needsLights && ( materialProperties.lightsStateVersion !== lights.state.version ) ) {

					initMaterial( material, scene, object );

				} else if ( materialProperties.numClippingPlanes !== undefined &&
					( materialProperties.numClippingPlanes !== clipping.numPlanes ||
					materialProperties.numIntersection !== clipping.numIntersection ) ) {

					initMaterial( material, scene, object );

				} else if ( materialProperties.outputEncoding !== encoding ) {

					initMaterial( material, scene, object );

				} else if ( materialProperties.envMap !== envMap ) {

					initMaterial( material, scene, object );

				}

			} else {

				initMaterial( material, scene, object );
				materialProperties.__version = material.version;

			}

			let refreshProgram = false;
			let refreshMaterial = false;
			let refreshLights = false;

			const program = materialProperties.program,
				p_uniforms = program.getUniforms(),
				m_uniforms = materialProperties.uniforms;

			if ( state.useProgram( program.program ) ) {

				refreshProgram = true;
				refreshMaterial = true;
				refreshLights = true;

			}

			if ( material.id !== _currentMaterialId ) {

				_currentMaterialId = material.id;

				refreshMaterial = true;

			}

			if ( refreshProgram || _currentCamera !== camera ) {

				p_uniforms.setValue( _gl, 'projectionMatrix', camera.projectionMatrix );

				if ( capabilities.logarithmicDepthBuffer ) {

					p_uniforms.setValue( _gl, 'logDepthBufFC',
						2.0 / ( Math.log( camera.far + 1.0 ) / Math.LN2 ) );

				}

				if ( _currentCamera !== camera ) {

					_currentCamera = camera;

					// lighting uniforms depend on the camera so enforce an update
					// now, in case this material supports lights - or later, when
					// the next material that does gets activated:

					refreshMaterial = true;		// set to true on material change
					refreshLights = true;		// remains set until update done

				}

				// load material specific uniforms
				// (shader material also gets them for the sake of genericity)

				if ( material.isShaderMaterial ||
					material.isMeshPhongMaterial ||
					material.isMeshToonMaterial ||
					material.isMeshStandardMaterial ||
					material.envMap ) {

					const uCamPos = p_uniforms.map.cameraPosition;

					if ( uCamPos !== undefined ) {

						uCamPos.setValue( _gl,
							_vector3.setFromMatrixPosition( camera.matrixWorld ) );

					}

				}

				if ( material.isMeshPhongMaterial ||
					material.isMeshToonMaterial ||
					material.isMeshLambertMaterial ||
					material.isMeshBasicMaterial ||
					material.isMeshStandardMaterial ||
					material.isShaderMaterial ) {

					p_uniforms.setValue( _gl, 'isOrthographic', camera.isOrthographicCamera === true );

				}

				if ( material.isMeshPhongMaterial ||
					material.isMeshToonMaterial ||
					material.isMeshLambertMaterial ||
					material.isMeshBasicMaterial ||
					material.isMeshStandardMaterial ||
					material.isShaderMaterial ||
					material.isShadowMaterial ||
					material.skinning ) {

					p_uniforms.setValue( _gl, 'viewMatrix', camera.matrixWorldInverse );

				}

			}

			// skinning uniforms must be set even if material didn't change
			// auto-setting of texture unit for bone texture must go before other textures
			// otherwise textures used for skinning can take over texture units reserved for other material textures

			if ( material.skinning ) {

				p_uniforms.setOptional( _gl, object, 'bindMatrix' );
				p_uniforms.setOptional( _gl, object, 'bindMatrixInverse' );

				const skeleton = object.skeleton;

				if ( skeleton ) {

					const bones = skeleton.bones;

					if ( capabilities.floatVertexTextures ) {

						if ( skeleton.boneTexture === null ) {

							// layout (1 matrix = 4 pixels)
							//      RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
							//  with  8x8  pixel texture max   16 bones * 4 pixels =  (8 * 8)
							//       16x16 pixel texture max   64 bones * 4 pixels = (16 * 16)
							//       32x32 pixel texture max  256 bones * 4 pixels = (32 * 32)
							//       64x64 pixel texture max 1024 bones * 4 pixels = (64 * 64)


							let size = Math.sqrt( bones.length * 4 ); // 4 pixels needed for 1 matrix
							size = MathUtils.ceilPowerOfTwo( size );
							size = Math.max( size, 4 );

							const boneMatrices = new Float32Array( size * size * 4 ); // 4 floats per RGBA pixel
							boneMatrices.set( skeleton.boneMatrices ); // copy current values

							const boneTexture = new DataTexture( boneMatrices, size, size, RGBAFormat, FloatType );

							skeleton.boneMatrices = boneMatrices;
							skeleton.boneTexture = boneTexture;
							skeleton.boneTextureSize = size;

						}

						p_uniforms.setValue( _gl, 'boneTexture', skeleton.boneTexture, textures );
						p_uniforms.setValue( _gl, 'boneTextureSize', skeleton.boneTextureSize );

					} else {

						p_uniforms.setOptional( _gl, skeleton, 'boneMatrices' );

					}

				}

			}

			if ( refreshMaterial || materialProperties.receiveShadow !== object.receiveShadow ) {

				materialProperties.receiveShadow = object.receiveShadow;
				p_uniforms.setValue( _gl, 'receiveShadow', object.receiveShadow );

			}

			if ( refreshMaterial ) {

				p_uniforms.setValue( _gl, 'toneMappingExposure', _this.toneMappingExposure );

				if ( materialProperties.needsLights ) {

					// the current material requires lighting info

					// note: all lighting uniforms are always set correctly
					// they simply reference the renderer's state for their
					// values
					//
					// use the current material's .needsUpdate flags to set
					// the GL state when required

					markUniformsLightsNeedsUpdate( m_uniforms, refreshLights );

				}

				// refresh uniforms common to several materials

				if ( fog && material.fog ) {

					materials.refreshFogUniforms( m_uniforms, fog );

				}

				materials.refreshMaterialUniforms( m_uniforms, material, _pixelRatio, _height );

				WebGLUniforms.upload( _gl, materialProperties.uniformsList, m_uniforms, textures );

			}

			if ( material.isShaderMaterial && material.uniformsNeedUpdate === true ) {

				WebGLUniforms.upload( _gl, materialProperties.uniformsList, m_uniforms, textures );
				material.uniformsNeedUpdate = false;

			}

			if ( material.isSpriteMaterial ) {

				p_uniforms.setValue( _gl, 'center', object.center );

			}

			// common matrices

			p_uniforms.setValue( _gl, 'modelViewMatrix', object.modelViewMatrix );
			p_uniforms.setValue( _gl, 'normalMatrix', object.normalMatrix );
			p_uniforms.setValue( _gl, 'modelMatrix', object.matrixWorld );

			return program;

		}

		// If uniforms are marked as clean, they don't need to be loaded to the GPU.

		function markUniformsLightsNeedsUpdate( uniforms, value ) {

			uniforms.ambientLightColor.needsUpdate = value;
			uniforms.lightProbe.needsUpdate = value;

			uniforms.directionalLights.needsUpdate = value;
			uniforms.directionalLightShadows.needsUpdate = value;
			uniforms.pointLights.needsUpdate = value;
			uniforms.pointLightShadows.needsUpdate = value;
			uniforms.spotLights.needsUpdate = value;
			uniforms.spotLightShadows.needsUpdate = value;
			uniforms.rectAreaLights.needsUpdate = value;
			uniforms.hemisphereLights.needsUpdate = value;

		}

		function materialNeedsLights( material ) {

			return material.isMeshLambertMaterial || material.isMeshToonMaterial || material.isMeshPhongMaterial ||
				material.isMeshStandardMaterial || material.isShadowMaterial ||
				( material.isShaderMaterial && material.lights === true );

		}

		//
		this.setFramebuffer = function ( value ) {

			if ( _framebuffer !== value && _currentRenderTarget === null ) _gl.bindFramebuffer( 36160, value );

			_framebuffer = value;

		};

		this.getActiveCubeFace = function () {

			return _currentActiveCubeFace;

		};

		this.getActiveMipmapLevel = function () {

			return _currentActiveMipmapLevel;

		};

		this.getRenderList = function () {

			return currentRenderList;

		};

		this.setRenderList = function ( renderList ) {

			currentRenderList = renderList;

		};

		this.getRenderTarget = function () {

			return _currentRenderTarget;

		};

		this.setRenderTarget = function ( renderTarget, activeCubeFace = 0, activeMipmapLevel = 0 ) {

			_currentRenderTarget = renderTarget;
			_currentActiveCubeFace = activeCubeFace;
			_currentActiveMipmapLevel = activeMipmapLevel;

			if ( renderTarget && properties.get( renderTarget ).__webglFramebuffer === undefined ) {

				textures.setupRenderTarget( renderTarget );

			}

			let framebuffer = _framebuffer;
			let isCube = false;

			if ( renderTarget ) {

				const __webglFramebuffer = properties.get( renderTarget ).__webglFramebuffer;

				if ( renderTarget.isWebGLCubeRenderTarget ) {

					framebuffer = __webglFramebuffer[ activeCubeFace ];
					isCube = true;

				} else if ( renderTarget.isWebGLMultisampleRenderTarget ) {

					framebuffer = properties.get( renderTarget ).__webglMultisampledFramebuffer;

				} else {

					framebuffer = __webglFramebuffer;

				}

				_currentViewport.copy( renderTarget.viewport );
				_currentScissor.copy( renderTarget.scissor );
				_currentScissorTest = renderTarget.scissorTest;

			} else {

				_currentViewport.copy( _viewport ).multiplyScalar( _pixelRatio ).floor();
				_currentScissor.copy( _scissor ).multiplyScalar( _pixelRatio ).floor();
				_currentScissorTest = _scissorTest;

			}

			if ( _currentFramebuffer !== framebuffer ) {

				_gl.bindFramebuffer( 36160, framebuffer );
				_currentFramebuffer = framebuffer;

			}

			state.viewport( _currentViewport );
			state.scissor( _currentScissor );
			state.setScissorTest( _currentScissorTest );

			if ( isCube ) {

				const textureProperties = properties.get( renderTarget.texture );
				_gl.framebufferTexture2D( 36160, 36064, 34069 + activeCubeFace, textureProperties.__webglTexture, activeMipmapLevel );

			}

		};

		this.readRenderTargetPixels = function ( renderTarget, x, y, width, height, buffer, activeCubeFaceIndex ) {

			if ( ! ( renderTarget && renderTarget.isWebGLRenderTarget ) ) {

				console.error( 'THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.' );
				return;

			}

			let framebuffer = properties.get( renderTarget ).__webglFramebuffer;

			if ( renderTarget.isWebGLCubeRenderTarget && activeCubeFaceIndex !== undefined ) {

				framebuffer = framebuffer[ activeCubeFaceIndex ];

			}

			if ( framebuffer ) {

				let restore = false;

				if ( framebuffer !== _currentFramebuffer ) {

					_gl.bindFramebuffer( 36160, framebuffer );

					restore = true;

				}

				try {

					const texture = renderTarget.texture;
					const textureFormat = texture.format;
					const textureType = texture.type;

					if ( textureFormat !== RGBAFormat && utils.convert( textureFormat ) !== _gl.getParameter( 35739 ) ) {

						console.error( 'THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.' );
						return;

					}

					if ( textureType !== UnsignedByteType && utils.convert( textureType ) !== _gl.getParameter( 35738 ) && // IE11, Edge and Chrome Mac < 52 (#9513)
						! ( textureType === FloatType && ( capabilities.isWebGL2 || extensions.get( 'OES_texture_float' ) || extensions.get( 'WEBGL_color_buffer_float' ) ) ) && // Chrome Mac >= 52 and Firefox
						! ( textureType === HalfFloatType && ( capabilities.isWebGL2 ? extensions.get( 'EXT_color_buffer_float' ) : extensions.get( 'EXT_color_buffer_half_float' ) ) ) ) {

						console.error( 'THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.' );
						return;

					}

					if ( _gl.checkFramebufferStatus( 36160 ) === 36053 ) {

						// the following if statement ensures valid read requests (no out-of-bounds pixels, see #8604)

						if ( ( x >= 0 && x <= ( renderTarget.width - width ) ) && ( y >= 0 && y <= ( renderTarget.height - height ) ) ) {

							_gl.readPixels( x, y, width, height, utils.convert( textureFormat ), utils.convert( textureType ), buffer );

						}

					} else {

						console.error( 'THREE.WebGLRenderer.readRenderTargetPixels: readPixels from renderTarget failed. Framebuffer not complete.' );

					}

				} finally {

					if ( restore ) {

						_gl.bindFramebuffer( 36160, _currentFramebuffer );

					}

				}

			}

		};

		this.copyFramebufferToTexture = function ( position, texture, level = 0 ) {

			const levelScale = Math.pow( 2, - level );
			const width = Math.floor( texture.image.width * levelScale );
			const height = Math.floor( texture.image.height * levelScale );
			const glFormat = utils.convert( texture.format );

			textures.setTexture2D( texture, 0 );

			_gl.copyTexImage2D( 3553, level, glFormat, position.x, position.y, width, height, 0 );

			state.unbindTexture();

		};

		this.copyTextureToTexture = function ( position, srcTexture, dstTexture, level = 0 ) {

			const width = srcTexture.image.width;
			const height = srcTexture.image.height;
			const glFormat = utils.convert( dstTexture.format );
			const glType = utils.convert( dstTexture.type );

			textures.setTexture2D( dstTexture, 0 );

			// As another texture upload may have changed pixelStorei
			// parameters, make sure they are correct for the dstTexture
			_gl.pixelStorei( 37440, dstTexture.flipY );
			_gl.pixelStorei( 37441, dstTexture.premultiplyAlpha );
			_gl.pixelStorei( 3317, dstTexture.unpackAlignment );

			if ( srcTexture.isDataTexture ) {

				_gl.texSubImage2D( 3553, level, position.x, position.y, width, height, glFormat, glType, srcTexture.image.data );

			} else {

				if ( srcTexture.isCompressedTexture ) {

					_gl.compressedTexSubImage2D( 3553, level, position.x, position.y, srcTexture.mipmaps[ 0 ].width, srcTexture.mipmaps[ 0 ].height, glFormat, srcTexture.mipmaps[ 0 ].data );

				} else {

					_gl.texSubImage2D( 3553, level, position.x, position.y, glFormat, glType, srcTexture.image );

				}

			}

			// Generate mipmaps only when copying level 0
			if ( level === 0 && dstTexture.generateMipmaps ) _gl.generateMipmap( 3553 );

			state.unbindTexture();

		};

		this.initTexture = function ( texture ) {

			textures.setTexture2D( texture, 0 );

			state.unbindTexture();

		};

		this.resetState = function () {

			state.reset();
			bindingStates.reset();

		};

		if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {

			__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) ); // eslint-disable-line no-undef

		}

	}

	function WebGL1Renderer( parameters ) {

		WebGLRenderer.call( this, parameters );

	}

	WebGL1Renderer.prototype = Object.assign( Object.create( WebGLRenderer.prototype ), {

		constructor: WebGL1Renderer,

		isWebGL1Renderer: true

	} );

	class FogExp2 {

		constructor( color, density ) {

			Object.defineProperty( this, 'isFogExp2', { value: true } );

			this.name = '';

			this.color = new Color( color );
			this.density = ( density !== undefined ) ? density : 0.00025;

		}

		clone() {

			return new FogExp2( this.color, this.density );

		}

		toJSON( /* meta */ ) {

			return {
				type: 'FogExp2',
				color: this.color.getHex(),
				density: this.density
			};

		}

	}

	class Fog {

		constructor( color, near, far ) {

			Object.defineProperty( this, 'isFog', { value: true } );

			this.name = '';

			this.color = new Color( color );

			this.near = ( near !== undefined ) ? near : 1;
			this.far = ( far !== undefined ) ? far : 1000;

		}

		clone() {

			return new Fog( this.color, this.near, this.far );

		}

		toJSON( /* meta */ ) {

			return {
				type: 'Fog',
				color: this.color.getHex(),
				near: this.near,
				far: this.far
			};

		}

	}

	class Scene extends Object3D {

		constructor() {

			super();

			Object.defineProperty( this, 'isScene', { value: true } );

			this.type = 'Scene';

			this.background = null;
			this.environment = null;
			this.fog = null;

			this.overrideMaterial = null;

			this.autoUpdate = true; // checked by the renderer

			if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {

				__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) ); // eslint-disable-line no-undef

			}

		}

		copy( source, recursive ) {

			super.copy( source, recursive );

			if ( source.background !== null ) this.background = source.background.clone();
			if ( source.environment !== null ) this.environment = source.environment.clone();
			if ( source.fog !== null ) this.fog = source.fog.clone();

			if ( source.overrideMaterial !== null ) this.overrideMaterial = source.overrideMaterial.clone();

			this.autoUpdate = source.autoUpdate;
			this.matrixAutoUpdate = source.matrixAutoUpdate;

			return this;

		}

		toJSON( meta ) {

			const data = super.toJSON( meta );

			if ( this.background !== null ) data.object.background = this.background.toJSON( meta );
			if ( this.environment !== null ) data.object.environment = this.environment.toJSON( meta );
			if ( this.fog !== null ) data.object.fog = this.fog.toJSON();

			return data;

		}

	}

	function InterleavedBuffer( array, stride ) {

		this.array = array;
		this.stride = stride;
		this.count = array !== undefined ? array.length / stride : 0;

		this.usage = StaticDrawUsage;
		this.updateRange = { offset: 0, count: - 1 };

		this.version = 0;

		this.uuid = MathUtils.generateUUID();

	}

	Object.defineProperty( InterleavedBuffer.prototype, 'needsUpdate', {

		set: function ( value ) {

			if ( value === true ) this.version ++;

		}

	} );

	Object.assign( InterleavedBuffer.prototype, {

		isInterleavedBuffer: true,

		onUploadCallback: function () {},

		setUsage: function ( value ) {

			this.usage = value;

			return this;

		},

		copy: function ( source ) {

			this.array = new source.array.constructor( source.array );
			this.count = source.count;
			this.stride = source.stride;
			this.usage = source.usage;

			return this;

		},

		copyAt: function ( index1, attribute, index2 ) {

			index1 *= this.stride;
			index2 *= attribute.stride;

			for ( let i = 0, l = this.stride; i < l; i ++ ) {

				this.array[ index1 + i ] = attribute.array[ index2 + i ];

			}

			return this;

		},

		set: function ( value, offset = 0 ) {

			this.array.set( value, offset );

			return this;

		},

		clone: function ( data ) {

			if ( data.arrayBuffers === undefined ) {

				data.arrayBuffers = {};

			}

			if ( this.array.buffer._uuid === undefined ) {

				this.array.buffer._uuid = MathUtils.generateUUID();

			}

			if ( data.arrayBuffers[ this.array.buffer._uuid ] === undefined ) {

				data.arrayBuffers[ this.array.buffer._uuid ] = this.array.slice( 0 ).buffer;

			}

			const array = new this.array.constructor( data.arrayBuffers[ this.array.buffer._uuid ] );

			const ib = new InterleavedBuffer( array, this.stride );
			ib.setUsage( this.usage );

			return ib;

		},

		onUpload: function ( callback ) {

			this.onUploadCallback = callback;

			return this;

		},

		toJSON: function ( data ) {

			if ( data.arrayBuffers === undefined ) {

				data.arrayBuffers = {};

			}

			// generate UUID for array buffer if necessary

			if ( this.array.buffer._uuid === undefined ) {

				this.array.buffer._uuid = MathUtils.generateUUID();

			}

			if ( data.arrayBuffers[ this.array.buffer._uuid ] === undefined ) {

				data.arrayBuffers[ this.array.buffer._uuid ] = Array.prototype.slice.call( new Uint32Array( this.array.buffer ) );

			}

			//

			return {
				uuid: this.uuid,
				buffer: this.array.buffer._uuid,
				type: this.array.constructor.name,
				stride: this.stride
			};

		}

	} );

	const _vector$6 = new Vector3();

	function InterleavedBufferAttribute( interleavedBuffer, itemSize, offset, normalized ) {

		this.name = '';

		this.data = interleavedBuffer;
		this.itemSize = itemSize;
		this.offset = offset;

		this.normalized = normalized === true;

	}

	Object.defineProperties( InterleavedBufferAttribute.prototype, {

		count: {

			get: function () {

				return this.data.count;

			}

		},

		array: {

			get: function () {

				return this.data.array;

			}

		},

		needsUpdate: {

			set: function ( value ) {

				this.data.needsUpdate = value;

			}

		}

	} );

	Object.assign( InterleavedBufferAttribute.prototype, {

		isInterleavedBufferAttribute: true,

		applyMatrix4: function ( m ) {

			for ( let i = 0, l = this.data.count; i < l; i ++ ) {

				_vector$6.x = this.getX( i );
				_vector$6.y = this.getY( i );
				_vector$6.z = this.getZ( i );

				_vector$6.applyMatrix4( m );

				this.setXYZ( i, _vector$6.x, _vector$6.y, _vector$6.z );

			}

			return this;

		},

		setX: function ( index, x ) {

			this.data.array[ index * this.data.stride + this.offset ] = x;

			return this;

		},

		setY: function ( index, y ) {

			this.data.array[ index * this.data.stride + this.offset + 1 ] = y;

			return this;

		},

		setZ: function ( index, z ) {

			this.data.array[ index * this.data.stride + this.offset + 2 ] = z;

			return this;

		},

		setW: function ( index, w ) {

			this.data.array[ index * this.data.stride + this.offset + 3 ] = w;

			return this;

		},

		getX: function ( index ) {

			return this.data.array[ index * this.data.stride + this.offset ];

		},

		getY: function ( index ) {

			return this.data.array[ index * this.data.stride + this.offset + 1 ];

		},

		getZ: function ( index ) {

			return this.data.array[ index * this.data.stride + this.offset + 2 ];

		},

		getW: function ( index ) {

			return this.data.array[ index * this.data.stride + this.offset + 3 ];

		},

		setXY: function ( index, x, y ) {

			index = index * this.data.stride + this.offset;

			this.data.array[ index + 0 ] = x;
			this.data.array[ index + 1 ] = y;

			return this;

		},

		setXYZ: function ( index, x, y, z ) {

			index = index * this.data.stride + this.offset;

			this.data.array[ index + 0 ] = x;
			this.data.array[ index + 1 ] = y;
			this.data.array[ index + 2 ] = z;

			return this;

		},

		setXYZW: function ( index, x, y, z, w ) {

			index = index * this.data.stride + this.offset;

			this.data.array[ index + 0 ] = x;
			this.data.array[ index + 1 ] = y;
			this.data.array[ index + 2 ] = z;
			this.data.array[ index + 3 ] = w;

			return this;

		},

		clone: function ( data ) {

			if ( data === undefined ) {

				console.log( 'THREE.InterleavedBufferAttribute.clone(): Cloning an interlaved buffer attribute will deinterleave buffer data.' );

				const array = [];

				for ( let i = 0; i < this.count; i ++ ) {

					const index = i * this.data.stride + this.offset;

					for ( let j = 0; j < this.itemSize; j ++ ) {

						array.push( this.data.array[ index + j ] );

					}

				}

				return new BufferAttribute( new this.array.constructor( array ), this.itemSize, this.normalized );

			} else {

				if ( data.interleavedBuffers === undefined ) {

					data.interleavedBuffers = {};

				}

				if ( data.interleavedBuffers[ this.data.uuid ] === undefined ) {

					data.interleavedBuffers[ this.data.uuid ] = this.data.clone( data );

				}

				return new InterleavedBufferAttribute( data.interleavedBuffers[ this.data.uuid ], this.itemSize, this.offset, this.normalized );

			}

		},

		toJSON: function ( data ) {

			if ( data === undefined ) {

				console.log( 'THREE.InterleavedBufferAttribute.toJSON(): Serializing an interlaved buffer attribute will deinterleave buffer data.' );

				const array = [];

				for ( let i = 0; i < this.count; i ++ ) {

					const index = i * this.data.stride + this.offset;

					for ( let j = 0; j < this.itemSize; j ++ ) {

						array.push( this.data.array[ index + j ] );

					}

				}

				// deinterleave data and save it as an ordinary buffer attribute for now

				return {
					itemSize: this.itemSize,
					type: this.array.constructor.name,
					array: array,
					normalized: this.normalized
				};

			} else {

				// save as true interlaved attribtue

				if ( data.interleavedBuffers === undefined ) {

					data.interleavedBuffers = {};

				}

				if ( data.interleavedBuffers[ this.data.uuid ] === undefined ) {

					data.interleavedBuffers[ this.data.uuid ] = this.data.toJSON( data );

				}

				return {
					isInterleavedBufferAttribute: true,
					itemSize: this.itemSize,
					data: this.data.uuid,
					offset: this.offset,
					normalized: this.normalized
				};

			}

		}

	} );

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  map: new THREE.Texture( <Image> ),
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *  rotation: <float>,
	 *  sizeAttenuation: <bool>
	 * }
	 */

	function SpriteMaterial( parameters ) {

		Material.call( this );

		this.type = 'SpriteMaterial';

		this.color = new Color( 0xffffff );

		this.map = null;

		this.alphaMap = null;

		this.rotation = 0;

		this.sizeAttenuation = true;

		this.transparent = true;

		this.setValues( parameters );

	}

	SpriteMaterial.prototype = Object.create( Material.prototype );
	SpriteMaterial.prototype.constructor = SpriteMaterial;
	SpriteMaterial.prototype.isSpriteMaterial = true;

	SpriteMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.color.copy( source.color );

		this.map = source.map;

		this.alphaMap = source.alphaMap;

		this.rotation = source.rotation;

		this.sizeAttenuation = source.sizeAttenuation;

		return this;

	};

	let _geometry;

	const _intersectPoint = new Vector3();
	const _worldScale = new Vector3();
	const _mvPosition = new Vector3();

	const _alignedPosition = new Vector2();
	const _rotatedPosition = new Vector2();
	const _viewWorldMatrix = new Matrix4();

	const _vA$1 = new Vector3();
	const _vB$1 = new Vector3();
	const _vC$1 = new Vector3();

	const _uvA$1 = new Vector2();
	const _uvB$1 = new Vector2();
	const _uvC$1 = new Vector2();

	function Sprite( material ) {

		Object3D.call( this );

		this.type = 'Sprite';

		if ( _geometry === undefined ) {

			_geometry = new BufferGeometry();

			const float32Array = new Float32Array( [
				- 0.5, - 0.5, 0, 0, 0,
				0.5, - 0.5, 0, 1, 0,
				0.5, 0.5, 0, 1, 1,
				- 0.5, 0.5, 0, 0, 1
			] );

			const interleavedBuffer = new InterleavedBuffer( float32Array, 5 );

			_geometry.setIndex( [ 0, 1, 2,	0, 2, 3 ] );
			_geometry.setAttribute( 'position', new InterleavedBufferAttribute( interleavedBuffer, 3, 0, false ) );
			_geometry.setAttribute( 'uv', new InterleavedBufferAttribute( interleavedBuffer, 2, 3, false ) );

		}

		this.geometry = _geometry;
		this.material = ( material !== undefined ) ? material : new SpriteMaterial();

		this.center = new Vector2( 0.5, 0.5 );

	}

	Sprite.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: Sprite,

		isSprite: true,

		raycast: function ( raycaster, intersects ) {

			if ( raycaster.camera === null ) {

				console.error( 'THREE.Sprite: "Raycaster.camera" needs to be set in order to raycast against sprites.' );

			}

			_worldScale.setFromMatrixScale( this.matrixWorld );

			_viewWorldMatrix.copy( raycaster.camera.matrixWorld );
			this.modelViewMatrix.multiplyMatrices( raycaster.camera.matrixWorldInverse, this.matrixWorld );

			_mvPosition.setFromMatrixPosition( this.modelViewMatrix );

			if ( raycaster.camera.isPerspectiveCamera && this.material.sizeAttenuation === false ) {

				_worldScale.multiplyScalar( - _mvPosition.z );

			}

			const rotation = this.material.rotation;
			let sin, cos;

			if ( rotation !== 0 ) {

				cos = Math.cos( rotation );
				sin = Math.sin( rotation );

			}

			const center = this.center;

			transformVertex( _vA$1.set( - 0.5, - 0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );
			transformVertex( _vB$1.set( 0.5, - 0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );
			transformVertex( _vC$1.set( 0.5, 0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );

			_uvA$1.set( 0, 0 );
			_uvB$1.set( 1, 0 );
			_uvC$1.set( 1, 1 );

			// check first triangle
			let intersect = raycaster.ray.intersectTriangle( _vA$1, _vB$1, _vC$1, false, _intersectPoint );

			if ( intersect === null ) {

				// check second triangle
				transformVertex( _vB$1.set( - 0.5, 0.5, 0 ), _mvPosition, center, _worldScale, sin, cos );
				_uvB$1.set( 0, 1 );

				intersect = raycaster.ray.intersectTriangle( _vA$1, _vC$1, _vB$1, false, _intersectPoint );
				if ( intersect === null ) {

					return;

				}

			}

			const distance = raycaster.ray.origin.distanceTo( _intersectPoint );

			if ( distance < raycaster.near || distance > raycaster.far ) return;

			intersects.push( {

				distance: distance,
				point: _intersectPoint.clone(),
				uv: Triangle.getUV( _intersectPoint, _vA$1, _vB$1, _vC$1, _uvA$1, _uvB$1, _uvC$1, new Vector2() ),
				face: null,
				object: this

			} );

		},

		copy: function ( source ) {

			Object3D.prototype.copy.call( this, source );

			if ( source.center !== undefined ) this.center.copy( source.center );

			this.material = source.material;

			return this;

		}

	} );

	function transformVertex( vertexPosition, mvPosition, center, scale, sin, cos ) {

		// compute position in camera space
		_alignedPosition.subVectors( vertexPosition, center ).addScalar( 0.5 ).multiply( scale );

		// to check if rotation is not zero
		if ( sin !== undefined ) {

			_rotatedPosition.x = ( cos * _alignedPosition.x ) - ( sin * _alignedPosition.y );
			_rotatedPosition.y = ( sin * _alignedPosition.x ) + ( cos * _alignedPosition.y );

		} else {

			_rotatedPosition.copy( _alignedPosition );

		}


		vertexPosition.copy( mvPosition );
		vertexPosition.x += _rotatedPosition.x;
		vertexPosition.y += _rotatedPosition.y;

		// transform to world space
		vertexPosition.applyMatrix4( _viewWorldMatrix );

	}

	const _v1$4 = new Vector3();
	const _v2$2 = new Vector3();

	function LOD() {

		Object3D.call( this );

		this._currentLevel = 0;

		this.type = 'LOD';

		Object.defineProperties( this, {
			levels: {
				enumerable: true,
				value: []
			}
		} );

		this.autoUpdate = true;

	}

	LOD.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: LOD,

		isLOD: true,

		copy: function ( source ) {

			Object3D.prototype.copy.call( this, source, false );

			const levels = source.levels;

			for ( let i = 0, l = levels.length; i < l; i ++ ) {

				const level = levels[ i ];

				this.addLevel( level.object.clone(), level.distance );

			}

			this.autoUpdate = source.autoUpdate;

			return this;

		},

		addLevel: function ( object, distance = 0 ) {

			distance = Math.abs( distance );

			const levels = this.levels;

			let l;

			for ( l = 0; l < levels.length; l ++ ) {

				if ( distance < levels[ l ].distance ) {

					break;

				}

			}

			levels.splice( l, 0, { distance: distance, object: object } );

			this.add( object );

			return this;

		},

		getCurrentLevel: function () {

			return this._currentLevel;

		},

		getObjectForDistance: function ( distance ) {

			const levels = this.levels;

			if ( levels.length > 0 ) {

				let i, l;

				for ( i = 1, l = levels.length; i < l; i ++ ) {

					if ( distance < levels[ i ].distance ) {

						break;

					}

				}

				return levels[ i - 1 ].object;

			}

			return null;

		},

		raycast: function ( raycaster, intersects ) {

			const levels = this.levels;

			if ( levels.length > 0 ) {

				_v1$4.setFromMatrixPosition( this.matrixWorld );

				const distance = raycaster.ray.origin.distanceTo( _v1$4 );

				this.getObjectForDistance( distance ).raycast( raycaster, intersects );

			}

		},

		update: function ( camera ) {

			const levels = this.levels;

			if ( levels.length > 1 ) {

				_v1$4.setFromMatrixPosition( camera.matrixWorld );
				_v2$2.setFromMatrixPosition( this.matrixWorld );

				const distance = _v1$4.distanceTo( _v2$2 ) / camera.zoom;

				levels[ 0 ].object.visible = true;

				let i, l;

				for ( i = 1, l = levels.length; i < l; i ++ ) {

					if ( distance >= levels[ i ].distance ) {

						levels[ i - 1 ].object.visible = false;
						levels[ i ].object.visible = true;

					} else {

						break;

					}

				}

				this._currentLevel = i - 1;

				for ( ; i < l; i ++ ) {

					levels[ i ].object.visible = false;

				}

			}

		},

		toJSON: function ( meta ) {

			const data = Object3D.prototype.toJSON.call( this, meta );

			if ( this.autoUpdate === false ) data.object.autoUpdate = false;

			data.object.levels = [];

			const levels = this.levels;

			for ( let i = 0, l = levels.length; i < l; i ++ ) {

				const level = levels[ i ];

				data.object.levels.push( {
					object: level.object.uuid,
					distance: level.distance
				} );

			}

			return data;

		}

	} );

	const _basePosition = new Vector3();

	const _skinIndex = new Vector4();
	const _skinWeight = new Vector4();

	const _vector$7 = new Vector3();
	const _matrix$1 = new Matrix4();

	function SkinnedMesh( geometry, material ) {

		if ( geometry && geometry.isGeometry ) {

			console.error( 'THREE.SkinnedMesh no longer supports THREE.Geometry. Use THREE.BufferGeometry instead.' );

		}

		Mesh.call( this, geometry, material );

		this.type = 'SkinnedMesh';

		this.bindMode = 'attached';
		this.bindMatrix = new Matrix4();
		this.bindMatrixInverse = new Matrix4();

	}

	SkinnedMesh.prototype = Object.assign( Object.create( Mesh.prototype ), {

		constructor: SkinnedMesh,

		isSkinnedMesh: true,

		copy: function ( source ) {

			Mesh.prototype.copy.call( this, source );

			this.bindMode = source.bindMode;
			this.bindMatrix.copy( source.bindMatrix );
			this.bindMatrixInverse.copy( source.bindMatrixInverse );

			this.skeleton = source.skeleton;

			return this;

		},

		bind: function ( skeleton, bindMatrix ) {

			this.skeleton = skeleton;

			if ( bindMatrix === undefined ) {

				this.updateMatrixWorld( true );

				this.skeleton.calculateInverses();

				bindMatrix = this.matrixWorld;

			}

			this.bindMatrix.copy( bindMatrix );
			this.bindMatrixInverse.copy( bindMatrix ).invert();

		},

		pose: function () {

			this.skeleton.pose();

		},

		normalizeSkinWeights: function () {

			const vector = new Vector4();

			const skinWeight = this.geometry.attributes.skinWeight;

			for ( let i = 0, l = skinWeight.count; i < l; i ++ ) {

				vector.x = skinWeight.getX( i );
				vector.y = skinWeight.getY( i );
				vector.z = skinWeight.getZ( i );
				vector.w = skinWeight.getW( i );

				const scale = 1.0 / vector.manhattanLength();

				if ( scale !== Infinity ) {

					vector.multiplyScalar( scale );

				} else {

					vector.set( 1, 0, 0, 0 ); // do something reasonable

				}

				skinWeight.setXYZW( i, vector.x, vector.y, vector.z, vector.w );

			}

		},

		updateMatrixWorld: function ( force ) {

			Mesh.prototype.updateMatrixWorld.call( this, force );

			if ( this.bindMode === 'attached' ) {

				this.bindMatrixInverse.copy( this.matrixWorld ).invert();

			} else if ( this.bindMode === 'detached' ) {

				this.bindMatrixInverse.copy( this.bindMatrix ).invert();

			} else {

				console.warn( 'THREE.SkinnedMesh: Unrecognized bindMode: ' + this.bindMode );

			}

		},

		boneTransform: function ( index, target ) {

			const skeleton = this.skeleton;
			const geometry = this.geometry;

			_skinIndex.fromBufferAttribute( geometry.attributes.skinIndex, index );
			_skinWeight.fromBufferAttribute( geometry.attributes.skinWeight, index );

			_basePosition.fromBufferAttribute( geometry.attributes.position, index ).applyMatrix4( this.bindMatrix );

			target.set( 0, 0, 0 );

			for ( let i = 0; i < 4; i ++ ) {

				const weight = _skinWeight.getComponent( i );

				if ( weight !== 0 ) {

					const boneIndex = _skinIndex.getComponent( i );

					_matrix$1.multiplyMatrices( skeleton.bones[ boneIndex ].matrixWorld, skeleton.boneInverses[ boneIndex ] );

					target.addScaledVector( _vector$7.copy( _basePosition ).applyMatrix4( _matrix$1 ), weight );

				}

			}

			return target.applyMatrix4( this.bindMatrixInverse );

		}

	} );

	function Bone() {

		Object3D.call( this );

		this.type = 'Bone';

	}

	Bone.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: Bone,

		isBone: true

	} );

	const _offsetMatrix = new Matrix4();
	const _identityMatrix = new Matrix4();

	function Skeleton( bones = [], boneInverses = [] ) {

		this.uuid = MathUtils.generateUUID();

		this.bones = bones.slice( 0 );
		this.boneInverses = boneInverses;
		this.boneMatrices = null;

		this.boneTexture = null;
		this.boneTextureSize = 0;

		this.frame = - 1;

		this.init();

	}

	Object.assign( Skeleton.prototype, {

		init: function () {

			const bones = this.bones;
			const boneInverses = this.boneInverses;

			this.boneMatrices = new Float32Array( bones.length * 16 );

			// calculate inverse bone matrices if necessary

			if ( boneInverses.length === 0 ) {

				this.calculateInverses();

			} else {

				// handle special case

				if ( bones.length !== boneInverses.length ) {

					console.warn( 'THREE.Skeleton: Number of inverse bone matrices does not match amount of bones.' );

					this.boneInverses = [];

					for ( let i = 0, il = this.bones.length; i < il; i ++ ) {

						this.boneInverses.push( new Matrix4() );

					}

				}

			}

		},

		calculateInverses: function () {

			this.boneInverses.length = 0;

			for ( let i = 0, il = this.bones.length; i < il; i ++ ) {

				const inverse = new Matrix4();

				if ( this.bones[ i ] ) {

					inverse.copy( this.bones[ i ].matrixWorld ).invert();

				}

				this.boneInverses.push( inverse );

			}

		},

		pose: function () {

			// recover the bind-time world matrices

			for ( let i = 0, il = this.bones.length; i < il; i ++ ) {

				const bone = this.bones[ i ];

				if ( bone ) {

					bone.matrixWorld.copy( this.boneInverses[ i ] ).invert();

				}

			}

			// compute the local matrices, positions, rotations and scales

			for ( let i = 0, il = this.bones.length; i < il; i ++ ) {

				const bone = this.bones[ i ];

				if ( bone ) {

					if ( bone.parent && bone.parent.isBone ) {

						bone.matrix.copy( bone.parent.matrixWorld ).invert();
						bone.matrix.multiply( bone.matrixWorld );

					} else {

						bone.matrix.copy( bone.matrixWorld );

					}

					bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );

				}

			}

		},

		update: function () {

			const bones = this.bones;
			const boneInverses = this.boneInverses;
			const boneMatrices = this.boneMatrices;
			const boneTexture = this.boneTexture;

			// flatten bone matrices to array

			for ( let i = 0, il = bones.length; i < il; i ++ ) {

				// compute the offset between the current and the original transform

				const matrix = bones[ i ] ? bones[ i ].matrixWorld : _identityMatrix;

				_offsetMatrix.multiplyMatrices( matrix, boneInverses[ i ] );
				_offsetMatrix.toArray( boneMatrices, i * 16 );

			}

			if ( boneTexture !== null ) {

				boneTexture.needsUpdate = true;

			}

		},

		clone: function () {

			return new Skeleton( this.bones, this.boneInverses );

		},

		getBoneByName: function ( name ) {

			for ( let i = 0, il = this.bones.length; i < il; i ++ ) {

				const bone = this.bones[ i ];

				if ( bone.name === name ) {

					return bone;

				}

			}

			return undefined;

		},

		dispose: function ( ) {

			if ( this.boneTexture !== null ) {

				this.boneTexture.dispose();

				this.boneTexture = null;

			}

		},

		fromJSON: function ( json, bones ) {

			this.uuid = json.uuid;

			for ( let i = 0, l = json.bones.length; i < l; i ++ ) {

				const uuid = json.bones[ i ];
				let bone = bones[ uuid ];

				if ( bone === undefined ) {

					console.warn( 'THREE.Skeleton: No bone found with UUID:', uuid );
					bone = new Bone();

				}

				this.bones.push( bone );
				this.boneInverses.push( new Matrix4().fromArray( json.boneInverses[ i ] ) );

			}

			this.init();

			return this;

		},

		toJSON: function () {

			const data = {
				metadata: {
					version: 4.5,
					type: 'Skeleton',
					generator: 'Skeleton.toJSON'
				},
				bones: [],
				boneInverses: []
			};

			data.uuid = this.uuid;

			const bones = this.bones;
			const boneInverses = this.boneInverses;

			for ( let i = 0, l = bones.length; i < l; i ++ ) {

				const bone = bones[ i ];
				data.bones.push( bone.uuid );

				const boneInverse = boneInverses[ i ];
				data.boneInverses.push( boneInverse.toArray() );

			}

			return data;

		}

	} );

	const _instanceLocalMatrix = new Matrix4();
	const _instanceWorldMatrix = new Matrix4();

	const _instanceIntersects = [];

	const _mesh = new Mesh();

	function InstancedMesh( geometry, material, count ) {

		Mesh.call( this, geometry, material );

		this.instanceMatrix = new BufferAttribute( new Float32Array( count * 16 ), 16 );
		this.instanceColor = null;

		this.count = count;

		this.frustumCulled = false;

	}

	InstancedMesh.prototype = Object.assign( Object.create( Mesh.prototype ), {

		constructor: InstancedMesh,

		isInstancedMesh: true,

		copy: function ( source ) {

			Mesh.prototype.copy.call( this, source );

			this.instanceMatrix.copy( source.instanceMatrix );
			this.count = source.count;

			return this;

		},

		getColorAt: function ( index, color ) {

			color.fromArray( this.instanceColor.array, index * 3 );

		},

		getMatrixAt: function ( index, matrix ) {

			matrix.fromArray( this.instanceMatrix.array, index * 16 );

		},

		raycast: function ( raycaster, intersects ) {

			const matrixWorld = this.matrixWorld;
			const raycastTimes = this.count;

			_mesh.geometry = this.geometry;
			_mesh.material = this.material;

			if ( _mesh.material === undefined ) return;

			for ( let instanceId = 0; instanceId < raycastTimes; instanceId ++ ) {

				// calculate the world matrix for each instance

				this.getMatrixAt( instanceId, _instanceLocalMatrix );

				_instanceWorldMatrix.multiplyMatrices( matrixWorld, _instanceLocalMatrix );

				// the mesh represents this single instance

				_mesh.matrixWorld = _instanceWorldMatrix;

				_mesh.raycast( raycaster, _instanceIntersects );

				// process the result of raycast

				for ( let i = 0, l = _instanceIntersects.length; i < l; i ++ ) {

					const intersect = _instanceIntersects[ i ];
					intersect.instanceId = instanceId;
					intersect.object = this;
					intersects.push( intersect );

				}

				_instanceIntersects.length = 0;

			}

		},

		setColorAt: function ( index, color ) {

			if ( this.instanceColor === null ) {

				this.instanceColor = new BufferAttribute( new Float32Array( this.count * 3 ), 3 );

			}

			color.toArray( this.instanceColor.array, index * 3 );

		},

		setMatrixAt: function ( index, matrix ) {

			matrix.toArray( this.instanceMatrix.array, index * 16 );

		},

		updateMorphTargets: function () {

		},

		dispose: function () {

			this.dispatchEvent( { type: 'dispose' } );

		}

	} );

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  opacity: <float>,
	 *
	 *  linewidth: <float>,
	 *  linecap: "round",
	 *  linejoin: "round"
	 * }
	 */

	function LineBasicMaterial( parameters ) {

		Material.call( this );

		this.type = 'LineBasicMaterial';

		this.color = new Color( 0xffffff );

		this.linewidth = 1;
		this.linecap = 'round';
		this.linejoin = 'round';

		this.morphTargets = false;

		this.setValues( parameters );

	}

	LineBasicMaterial.prototype = Object.create( Material.prototype );
	LineBasicMaterial.prototype.constructor = LineBasicMaterial;

	LineBasicMaterial.prototype.isLineBasicMaterial = true;

	LineBasicMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.color.copy( source.color );

		this.linewidth = source.linewidth;
		this.linecap = source.linecap;
		this.linejoin = source.linejoin;

		this.morphTargets = source.morphTargets;

		return this;

	};

	const _start = new Vector3();
	const _end = new Vector3();
	const _inverseMatrix$1 = new Matrix4();
	const _ray$1 = new Ray();
	const _sphere$2 = new Sphere();

	function Line( geometry = new BufferGeometry(), material = new LineBasicMaterial() ) {

		Object3D.call( this );

		this.type = 'Line';

		this.geometry = geometry;
		this.material = material;

		this.updateMorphTargets();

	}

	Line.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: Line,

		isLine: true,

		copy: function ( source ) {

			Object3D.prototype.copy.call( this, source );

			this.material = source.material;
			this.geometry = source.geometry;

			return this;

		},

		computeLineDistances: function () {

			const geometry = this.geometry;

			if ( geometry.isBufferGeometry ) {

				// we assume non-indexed geometry

				if ( geometry.index === null ) {

					const positionAttribute = geometry.attributes.position;
					const lineDistances = [ 0 ];

					for ( let i = 1, l = positionAttribute.count; i < l; i ++ ) {

						_start.fromBufferAttribute( positionAttribute, i - 1 );
						_end.fromBufferAttribute( positionAttribute, i );

						lineDistances[ i ] = lineDistances[ i - 1 ];
						lineDistances[ i ] += _start.distanceTo( _end );

					}

					geometry.setAttribute( 'lineDistance', new Float32BufferAttribute( lineDistances, 1 ) );

				} else {

					console.warn( 'THREE.Line.computeLineDistances(): Computation only possible with non-indexed BufferGeometry.' );

				}

			} else if ( geometry.isGeometry ) {

				const vertices = geometry.vertices;
				const lineDistances = geometry.lineDistances;

				lineDistances[ 0 ] = 0;

				for ( let i = 1, l = vertices.length; i < l; i ++ ) {

					lineDistances[ i ] = lineDistances[ i - 1 ];
					lineDistances[ i ] += vertices[ i - 1 ].distanceTo( vertices[ i ] );

				}

			}

			return this;

		},

		raycast: function ( raycaster, intersects ) {

			const geometry = this.geometry;
			const matrixWorld = this.matrixWorld;
			const threshold = raycaster.params.Line.threshold;

			// Checking boundingSphere distance to ray

			if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();

			_sphere$2.copy( geometry.boundingSphere );
			_sphere$2.applyMatrix4( matrixWorld );
			_sphere$2.radius += threshold;

			if ( raycaster.ray.intersectsSphere( _sphere$2 ) === false ) return;

			//

			_inverseMatrix$1.copy( matrixWorld ).invert();
			_ray$1.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$1 );

			const localThreshold = threshold / ( ( this.scale.x + this.scale.y + this.scale.z ) / 3 );
			const localThresholdSq = localThreshold * localThreshold;

			const vStart = new Vector3();
			const vEnd = new Vector3();
			const interSegment = new Vector3();
			const interRay = new Vector3();
			const step = this.isLineSegments ? 2 : 1;

			if ( geometry.isBufferGeometry ) {

				const index = geometry.index;
				const attributes = geometry.attributes;
				const positionAttribute = attributes.position;

				if ( index !== null ) {

					const indices = index.array;

					for ( let i = 0, l = indices.length - 1; i < l; i += step ) {

						const a = indices[ i ];
						const b = indices[ i + 1 ];

						vStart.fromBufferAttribute( positionAttribute, a );
						vEnd.fromBufferAttribute( positionAttribute, b );

						const distSq = _ray$1.distanceSqToSegment( vStart, vEnd, interRay, interSegment );

						if ( distSq > localThresholdSq ) continue;

						interRay.applyMatrix4( this.matrixWorld ); //Move back to world space for distance calculation

						const distance = raycaster.ray.origin.distanceTo( interRay );

						if ( distance < raycaster.near || distance > raycaster.far ) continue;

						intersects.push( {

							distance: distance,
							// What do we want? intersection point on the ray or on the segment??
							// point: raycaster.ray.at( distance ),
							point: interSegment.clone().applyMatrix4( this.matrixWorld ),
							index: i,
							face: null,
							faceIndex: null,
							object: this

						} );

					}

				} else {

					for ( let i = 0, l = positionAttribute.count - 1; i < l; i += step ) {

						vStart.fromBufferAttribute( positionAttribute, i );
						vEnd.fromBufferAttribute( positionAttribute, i + 1 );

						const distSq = _ray$1.distanceSqToSegment( vStart, vEnd, interRay, interSegment );

						if ( distSq > localThresholdSq ) continue;

						interRay.applyMatrix4( this.matrixWorld ); //Move back to world space for distance calculation

						const distance = raycaster.ray.origin.distanceTo( interRay );

						if ( distance < raycaster.near || distance > raycaster.far ) continue;

						intersects.push( {

							distance: distance,
							// What do we want? intersection point on the ray or on the segment??
							// point: raycaster.ray.at( distance ),
							point: interSegment.clone().applyMatrix4( this.matrixWorld ),
							index: i,
							face: null,
							faceIndex: null,
							object: this

						} );

					}

				}

			} else if ( geometry.isGeometry ) {

				const vertices = geometry.vertices;
				const nbVertices = vertices.length;

				for ( let i = 0; i < nbVertices - 1; i += step ) {

					const distSq = _ray$1.distanceSqToSegment( vertices[ i ], vertices[ i + 1 ], interRay, interSegment );

					if ( distSq > localThresholdSq ) continue;

					interRay.applyMatrix4( this.matrixWorld ); //Move back to world space for distance calculation

					const distance = raycaster.ray.origin.distanceTo( interRay );

					if ( distance < raycaster.near || distance > raycaster.far ) continue;

					intersects.push( {

						distance: distance,
						// What do we want? intersection point on the ray or on the segment??
						// point: raycaster.ray.at( distance ),
						point: interSegment.clone().applyMatrix4( this.matrixWorld ),
						index: i,
						face: null,
						faceIndex: null,
						object: this

					} );

				}

			}

		},

		updateMorphTargets: function () {

			const geometry = this.geometry;

			if ( geometry.isBufferGeometry ) {

				const morphAttributes = geometry.morphAttributes;
				const keys = Object.keys( morphAttributes );

				if ( keys.length > 0 ) {

					const morphAttribute = morphAttributes[ keys[ 0 ] ];

					if ( morphAttribute !== undefined ) {

						this.morphTargetInfluences = [];
						this.morphTargetDictionary = {};

						for ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {

							const name = morphAttribute[ m ].name || String( m );

							this.morphTargetInfluences.push( 0 );
							this.morphTargetDictionary[ name ] = m;

						}

					}

				}

			} else {

				const morphTargets = geometry.morphTargets;

				if ( morphTargets !== undefined && morphTargets.length > 0 ) {

					console.error( 'THREE.Line.updateMorphTargets() does not support THREE.Geometry. Use THREE.BufferGeometry instead.' );

				}

			}

		}

	} );

	const _start$1 = new Vector3();
	const _end$1 = new Vector3();

	function LineSegments( geometry, material ) {

		Line.call( this, geometry, material );

		this.type = 'LineSegments';

	}

	LineSegments.prototype = Object.assign( Object.create( Line.prototype ), {

		constructor: LineSegments,

		isLineSegments: true,

		computeLineDistances: function () {

			const geometry = this.geometry;

			if ( geometry.isBufferGeometry ) {

				// we assume non-indexed geometry

				if ( geometry.index === null ) {

					const positionAttribute = geometry.attributes.position;
					const lineDistances = [];

					for ( let i = 0, l = positionAttribute.count; i < l; i += 2 ) {

						_start$1.fromBufferAttribute( positionAttribute, i );
						_end$1.fromBufferAttribute( positionAttribute, i + 1 );

						lineDistances[ i ] = ( i === 0 ) ? 0 : lineDistances[ i - 1 ];
						lineDistances[ i + 1 ] = lineDistances[ i ] + _start$1.distanceTo( _end$1 );

					}

					geometry.setAttribute( 'lineDistance', new Float32BufferAttribute( lineDistances, 1 ) );

				} else {

					console.warn( 'THREE.LineSegments.computeLineDistances(): Computation only possible with non-indexed BufferGeometry.' );

				}

			} else if ( geometry.isGeometry ) {

				const vertices = geometry.vertices;
				const lineDistances = geometry.lineDistances;

				for ( let i = 0, l = vertices.length; i < l; i += 2 ) {

					_start$1.copy( vertices[ i ] );
					_end$1.copy( vertices[ i + 1 ] );

					lineDistances[ i ] = ( i === 0 ) ? 0 : lineDistances[ i - 1 ];
					lineDistances[ i + 1 ] = lineDistances[ i ] + _start$1.distanceTo( _end$1 );

				}

			}

			return this;

		}

	} );

	function LineLoop( geometry, material ) {

		Line.call( this, geometry, material );

		this.type = 'LineLoop';

	}

	LineLoop.prototype = Object.assign( Object.create( Line.prototype ), {

		constructor: LineLoop,

		isLineLoop: true,

	} );

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  opacity: <float>,
	 *  map: new THREE.Texture( <Image> ),
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *
	 *  size: <float>,
	 *  sizeAttenuation: <bool>
	 *
	 *  morphTargets: <bool>
	 * }
	 */

	function PointsMaterial( parameters ) {

		Material.call( this );

		this.type = 'PointsMaterial';

		this.color = new Color( 0xffffff );

		this.map = null;

		this.alphaMap = null;

		this.size = 1;
		this.sizeAttenuation = true;

		this.morphTargets = false;

		this.setValues( parameters );

	}

	PointsMaterial.prototype = Object.create( Material.prototype );
	PointsMaterial.prototype.constructor = PointsMaterial;

	PointsMaterial.prototype.isPointsMaterial = true;

	PointsMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.color.copy( source.color );

		this.map = source.map;

		this.alphaMap = source.alphaMap;

		this.size = source.size;
		this.sizeAttenuation = source.sizeAttenuation;

		this.morphTargets = source.morphTargets;

		return this;

	};

	const _inverseMatrix$2 = new Matrix4();
	const _ray$2 = new Ray();
	const _sphere$3 = new Sphere();
	const _position$1 = new Vector3();

	function Points( geometry = new BufferGeometry(), material = new PointsMaterial() ) {

		Object3D.call( this );

		this.type = 'Points';

		this.geometry = geometry;
		this.material = material;

		this.updateMorphTargets();

	}

	Points.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: Points,

		isPoints: true,

		copy: function ( source ) {

			Object3D.prototype.copy.call( this, source );

			this.material = source.material;
			this.geometry = source.geometry;

			return this;

		},

		raycast: function ( raycaster, intersects ) {

			const geometry = this.geometry;
			const matrixWorld = this.matrixWorld;
			const threshold = raycaster.params.Points.threshold;

			// Checking boundingSphere distance to ray

			if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();

			_sphere$3.copy( geometry.boundingSphere );
			_sphere$3.applyMatrix4( matrixWorld );
			_sphere$3.radius += threshold;

			if ( raycaster.ray.intersectsSphere( _sphere$3 ) === false ) return;

			//

			_inverseMatrix$2.copy( matrixWorld ).invert();
			_ray$2.copy( raycaster.ray ).applyMatrix4( _inverseMatrix$2 );

			const localThreshold = threshold / ( ( this.scale.x + this.scale.y + this.scale.z ) / 3 );
			const localThresholdSq = localThreshold * localThreshold;

			if ( geometry.isBufferGeometry ) {

				const index = geometry.index;
				const attributes = geometry.attributes;
				const positionAttribute = attributes.position;

				if ( index !== null ) {

					const indices = index.array;

					for ( let i = 0, il = indices.length; i < il; i ++ ) {

						const a = indices[ i ];

						_position$1.fromBufferAttribute( positionAttribute, a );

						testPoint( _position$1, a, localThresholdSq, matrixWorld, raycaster, intersects, this );

					}

				} else {

					for ( let i = 0, l = positionAttribute.count; i < l; i ++ ) {

						_position$1.fromBufferAttribute( positionAttribute, i );

						testPoint( _position$1, i, localThresholdSq, matrixWorld, raycaster, intersects, this );

					}

				}

			} else {

				const vertices = geometry.vertices;

				for ( let i = 0, l = vertices.length; i < l; i ++ ) {

					testPoint( vertices[ i ], i, localThresholdSq, matrixWorld, raycaster, intersects, this );

				}

			}

		},

		updateMorphTargets: function () {

			const geometry = this.geometry;

			if ( geometry.isBufferGeometry ) {

				const morphAttributes = geometry.morphAttributes;
				const keys = Object.keys( morphAttributes );

				if ( keys.length > 0 ) {

					const morphAttribute = morphAttributes[ keys[ 0 ] ];

					if ( morphAttribute !== undefined ) {

						this.morphTargetInfluences = [];
						this.morphTargetDictionary = {};

						for ( let m = 0, ml = morphAttribute.length; m < ml; m ++ ) {

							const name = morphAttribute[ m ].name || String( m );

							this.morphTargetInfluences.push( 0 );
							this.morphTargetDictionary[ name ] = m;

						}

					}

				}

			} else {

				const morphTargets = geometry.morphTargets;

				if ( morphTargets !== undefined && morphTargets.length > 0 ) {

					console.error( 'THREE.Points.updateMorphTargets() does not support THREE.Geometry. Use THREE.BufferGeometry instead.' );

				}

			}

		}

	} );

	function testPoint( point, index, localThresholdSq, matrixWorld, raycaster, intersects, object ) {

		const rayPointDistanceSq = _ray$2.distanceSqToPoint( point );

		if ( rayPointDistanceSq < localThresholdSq ) {

			const intersectPoint = new Vector3();

			_ray$2.closestPointToPoint( point, intersectPoint );
			intersectPoint.applyMatrix4( matrixWorld );

			const distance = raycaster.ray.origin.distanceTo( intersectPoint );

			if ( distance < raycaster.near || distance > raycaster.far ) return;

			intersects.push( {

				distance: distance,
				distanceToRay: Math.sqrt( rayPointDistanceSq ),
				point: intersectPoint,
				index: index,
				face: null,
				object: object

			} );

		}

	}

	function VideoTexture( video, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {

		Texture.call( this, video, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );

		this.format = format !== undefined ? format : RGBFormat;

		this.minFilter = minFilter !== undefined ? minFilter : LinearFilter;
		this.magFilter = magFilter !== undefined ? magFilter : LinearFilter;

		this.generateMipmaps = false;

		const scope = this;

		function updateVideo() {

			scope.needsUpdate = true;
			video.requestVideoFrameCallback( updateVideo );

		}

		if ( 'requestVideoFrameCallback' in video ) {

			video.requestVideoFrameCallback( updateVideo );

		}

	}

	VideoTexture.prototype = Object.assign( Object.create( Texture.prototype ), {

		constructor: VideoTexture,

		clone: function () {

			return new this.constructor( this.image ).copy( this );

		},

		isVideoTexture: true,

		update: function () {

			const video = this.image;
			const hasVideoFrameCallback = 'requestVideoFrameCallback' in video;

			if ( hasVideoFrameCallback === false && video.readyState >= video.HAVE_CURRENT_DATA ) {

				this.needsUpdate = true;

			}

		}

	} );

	function CompressedTexture( mipmaps, width, height, format, type, mapping, wrapS, wrapT, magFilter, minFilter, anisotropy, encoding ) {

		Texture.call( this, null, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy, encoding );

		this.image = { width: width, height: height };
		this.mipmaps = mipmaps;

		// no flipping for cube textures
		// (also flipping doesn't work for compressed textures )

		this.flipY = false;

		// can't generate mipmaps for compressed textures
		// mips must be embedded in DDS files

		this.generateMipmaps = false;

	}

	CompressedTexture.prototype = Object.create( Texture.prototype );
	CompressedTexture.prototype.constructor = CompressedTexture;

	CompressedTexture.prototype.isCompressedTexture = true;

	function CanvasTexture( canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy ) {

		Texture.call( this, canvas, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );

		this.needsUpdate = true;

	}

	CanvasTexture.prototype = Object.create( Texture.prototype );
	CanvasTexture.prototype.constructor = CanvasTexture;
	CanvasTexture.prototype.isCanvasTexture = true;

	function DepthTexture( width, height, type, mapping, wrapS, wrapT, magFilter, minFilter, anisotropy, format ) {

		format = format !== undefined ? format : DepthFormat;

		if ( format !== DepthFormat && format !== DepthStencilFormat ) {

			throw new Error( 'DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat' );

		}

		if ( type === undefined && format === DepthFormat ) type = UnsignedShortType;
		if ( type === undefined && format === DepthStencilFormat ) type = UnsignedInt248Type$1;

		Texture.call( this, null, mapping, wrapS, wrapT, magFilter, minFilter, format, type, anisotropy );

		this.image = { width: width, height: height };

		this.magFilter = magFilter !== undefined ? magFilter : NearestFilter;
		this.minFilter = minFilter !== undefined ? minFilter : NearestFilter;

		this.flipY = false;
		this.generateMipmaps = false;

	}

	DepthTexture.prototype = Object.create( Texture.prototype );
	DepthTexture.prototype.constructor = DepthTexture;
	DepthTexture.prototype.isDepthTexture = true;

	let _geometryId = 0; // Geometry uses even numbers as Id
	const _m1$3 = new Matrix4();
	const _obj$1 = new Object3D();
	const _offset$1 = new Vector3();

	function Geometry() {

		Object.defineProperty( this, 'id', { value: _geometryId += 2 } );

		this.uuid = MathUtils.generateUUID();

		this.name = '';
		this.type = 'Geometry';

		this.vertices = [];
		this.colors = [];
		this.faces = [];
		this.faceVertexUvs = [[]];

		this.morphTargets = [];
		this.morphNormals = [];

		this.skinWeights = [];
		this.skinIndices = [];

		this.lineDistances = [];

		this.boundingBox = null;
		this.boundingSphere = null;

		// update flags

		this.elementsNeedUpdate = false;
		this.verticesNeedUpdate = false;
		this.uvsNeedUpdate = false;
		this.normalsNeedUpdate = false;
		this.colorsNeedUpdate = false;
		this.lineDistancesNeedUpdate = false;
		this.groupsNeedUpdate = false;

	}

	Geometry.prototype = Object.assign( Object.create( EventDispatcher$1.prototype ), {

		constructor: Geometry,

		isGeometry: true,

		applyMatrix4: function ( matrix ) {

			const normalMatrix = new Matrix3().getNormalMatrix( matrix );

			for ( let i = 0, il = this.vertices.length; i < il; i ++ ) {

				const vertex = this.vertices[ i ];
				vertex.applyMatrix4( matrix );

			}

			for ( let i = 0, il = this.faces.length; i < il; i ++ ) {

				const face = this.faces[ i ];
				face.normal.applyMatrix3( normalMatrix ).normalize();

				for ( let j = 0, jl = face.vertexNormals.length; j < jl; j ++ ) {

					face.vertexNormals[ j ].applyMatrix3( normalMatrix ).normalize();

				}

			}

			if ( this.boundingBox !== null ) {

				this.computeBoundingBox();

			}

			if ( this.boundingSphere !== null ) {

				this.computeBoundingSphere();

			}

			this.verticesNeedUpdate = true;
			this.normalsNeedUpdate = true;

			return this;

		},

		rotateX: function ( angle ) {

			// rotate geometry around world x-axis

			_m1$3.makeRotationX( angle );

			this.applyMatrix4( _m1$3 );

			return this;

		},

		rotateY: function ( angle ) {

			// rotate geometry around world y-axis

			_m1$3.makeRotationY( angle );

			this.applyMatrix4( _m1$3 );

			return this;

		},

		rotateZ: function ( angle ) {

			// rotate geometry around world z-axis

			_m1$3.makeRotationZ( angle );

			this.applyMatrix4( _m1$3 );

			return this;

		},

		translate: function ( x, y, z ) {

			// translate geometry

			_m1$3.makeTranslation( x, y, z );

			this.applyMatrix4( _m1$3 );

			return this;

		},

		scale: function ( x, y, z ) {

			// scale geometry

			_m1$3.makeScale( x, y, z );

			this.applyMatrix4( _m1$3 );

			return this;

		},

		lookAt: function ( vector ) {

			_obj$1.lookAt( vector );

			_obj$1.updateMatrix();

			this.applyMatrix4( _obj$1.matrix );

			return this;

		},

		fromBufferGeometry: function ( geometry ) {

			const scope = this;

			const index = geometry.index !== null ? geometry.index : undefined;
			const attributes = geometry.attributes;

			if ( attributes.position === undefined ) {

				console.error( 'THREE.Geometry.fromBufferGeometry(): Position attribute required for conversion.' );
				return this;

			}

			const position = attributes.position;
			const normal = attributes.normal;
			const color = attributes.color;
			const uv = attributes.uv;
			const uv2 = attributes.uv2;

			if ( uv2 !== undefined ) this.faceVertexUvs[ 1 ] = [];

			for ( let i = 0; i < position.count; i ++ ) {

				scope.vertices.push( new Vector3().fromBufferAttribute( position, i ) );

				if ( color !== undefined ) {

					scope.colors.push( new Color().fromBufferAttribute( color, i ) );

				}

			}

			function addFace( a, b, c, materialIndex ) {

				const vertexColors = ( color === undefined ) ? [] : [
					scope.colors[ a ].clone(),
					scope.colors[ b ].clone(),
					scope.colors[ c ].clone()
				];

				const vertexNormals = ( normal === undefined ) ? [] : [
					new Vector3().fromBufferAttribute( normal, a ),
					new Vector3().fromBufferAttribute( normal, b ),
					new Vector3().fromBufferAttribute( normal, c )
				];

				const face = new Face3( a, b, c, vertexNormals, vertexColors, materialIndex );

				scope.faces.push( face );

				if ( uv !== undefined ) {

					scope.faceVertexUvs[ 0 ].push( [
						new Vector2().fromBufferAttribute( uv, a ),
						new Vector2().fromBufferAttribute( uv, b ),
						new Vector2().fromBufferAttribute( uv, c )
					] );

				}

				if ( uv2 !== undefined ) {

					scope.faceVertexUvs[ 1 ].push( [
						new Vector2().fromBufferAttribute( uv2, a ),
						new Vector2().fromBufferAttribute( uv2, b ),
						new Vector2().fromBufferAttribute( uv2, c )
					] );

				}

			}

			const groups = geometry.groups;

			if ( groups.length > 0 ) {

				for ( let i = 0; i < groups.length; i ++ ) {

					const group = groups[ i ];

					const start = group.start;
					const count = group.count;

					for ( let j = start, jl = start + count; j < jl; j += 3 ) {

						if ( index !== undefined ) {

							addFace( index.getX( j ), index.getX( j + 1 ), index.getX( j + 2 ), group.materialIndex );

						} else {

							addFace( j, j + 1, j + 2, group.materialIndex );

						}

					}

				}

			} else {

				if ( index !== undefined ) {

					for ( let i = 0; i < index.count; i += 3 ) {

						addFace( index.getX( i ), index.getX( i + 1 ), index.getX( i + 2 ) );

					}

				} else {

					for ( let i = 0; i < position.count; i += 3 ) {

						addFace( i, i + 1, i + 2 );

					}

				}

			}

			this.computeFaceNormals();

			if ( geometry.boundingBox !== null ) {

				this.boundingBox = geometry.boundingBox.clone();

			}

			if ( geometry.boundingSphere !== null ) {

				this.boundingSphere = geometry.boundingSphere.clone();

			}

			return this;

		},

		center: function () {

			this.computeBoundingBox();

			this.boundingBox.getCenter( _offset$1 ).negate();

			this.translate( _offset$1.x, _offset$1.y, _offset$1.z );

			return this;

		},

		normalize: function () {

			this.computeBoundingSphere();

			const center = this.boundingSphere.center;
			const radius = this.boundingSphere.radius;

			const s = radius === 0 ? 1 : 1.0 / radius;

			const matrix = new Matrix4();
			matrix.set(
				s, 0, 0, - s * center.x,
				0, s, 0, - s * center.y,
				0, 0, s, - s * center.z,
				0, 0, 0, 1
			);

			this.applyMatrix4( matrix );

			return this;

		},

		computeFaceNormals: function () {

			const cb = new Vector3(), ab = new Vector3();

			for ( let f = 0, fl = this.faces.length; f < fl; f ++ ) {

				const face = this.faces[ f ];

				const vA = this.vertices[ face.a ];
				const vB = this.vertices[ face.b ];
				const vC = this.vertices[ face.c ];

				cb.subVectors( vC, vB );
				ab.subVectors( vA, vB );
				cb.cross( ab );

				cb.normalize();

				face.normal.copy( cb );

			}

		},

		computeVertexNormals: function ( areaWeighted = true ) {

			const vertices = new Array( this.vertices.length );

			for ( let v = 0, vl = this.vertices.length; v < vl; v ++ ) {

				vertices[ v ] = new Vector3();

			}

			if ( areaWeighted ) {

				// vertex normals weighted by triangle areas
				// http://www.iquilezles.org/www/articles/normals/normals.htm

				const cb = new Vector3(), ab = new Vector3();

				for ( let f = 0, fl = this.faces.length; f < fl; f ++ ) {

					const face = this.faces[ f ];

					const vA = this.vertices[ face.a ];
					const vB = this.vertices[ face.b ];
					const vC = this.vertices[ face.c ];

					cb.subVectors( vC, vB );
					ab.subVectors( vA, vB );
					cb.cross( ab );

					vertices[ face.a ].add( cb );
					vertices[ face.b ].add( cb );
					vertices[ face.c ].add( cb );

				}

			} else {

				this.computeFaceNormals();

				for ( let f = 0, fl = this.faces.length; f < fl; f ++ ) {

					const face = this.faces[ f ];

					vertices[ face.a ].add( face.normal );
					vertices[ face.b ].add( face.normal );
					vertices[ face.c ].add( face.normal );

				}

			}

			for ( let v = 0, vl = this.vertices.length; v < vl; v ++ ) {

				vertices[ v ].normalize();

			}

			for ( let f = 0, fl = this.faces.length; f < fl; f ++ ) {

				const face = this.faces[ f ];

				const vertexNormals = face.vertexNormals;

				if ( vertexNormals.length === 3 ) {

					vertexNormals[ 0 ].copy( vertices[ face.a ] );
					vertexNormals[ 1 ].copy( vertices[ face.b ] );
					vertexNormals[ 2 ].copy( vertices[ face.c ] );

				} else {

					vertexNormals[ 0 ] = vertices[ face.a ].clone();
					vertexNormals[ 1 ] = vertices[ face.b ].clone();
					vertexNormals[ 2 ] = vertices[ face.c ].clone();

				}

			}

			if ( this.faces.length > 0 ) {

				this.normalsNeedUpdate = true;

			}

		},

		computeFlatVertexNormals: function () {

			this.computeFaceNormals();

			for ( let f = 0, fl = this.faces.length; f < fl; f ++ ) {

				const face = this.faces[ f ];

				const vertexNormals = face.vertexNormals;

				if ( vertexNormals.length === 3 ) {

					vertexNormals[ 0 ].copy( face.normal );
					vertexNormals[ 1 ].copy( face.normal );
					vertexNormals[ 2 ].copy( face.normal );

				} else {

					vertexNormals[ 0 ] = face.normal.clone();
					vertexNormals[ 1 ] = face.normal.clone();
					vertexNormals[ 2 ] = face.normal.clone();

				}

			}

			if ( this.faces.length > 0 ) {

				this.normalsNeedUpdate = true;

			}

		},

		computeMorphNormals: function () {

			// save original normals
			// - create temp variables on first access
			//   otherwise just copy (for faster repeated calls)

			for ( let f = 0, fl = this.faces.length; f < fl; f ++ ) {

				const face = this.faces[ f ];

				if ( ! face.__originalFaceNormal ) {

					face.__originalFaceNormal = face.normal.clone();

				} else {

					face.__originalFaceNormal.copy( face.normal );

				}

				if ( ! face.__originalVertexNormals ) face.__originalVertexNormals = [];

				for ( let i = 0, il = face.vertexNormals.length; i < il; i ++ ) {

					if ( ! face.__originalVertexNormals[ i ] ) {

						face.__originalVertexNormals[ i ] = face.vertexNormals[ i ].clone();

					} else {

						face.__originalVertexNormals[ i ].copy( face.vertexNormals[ i ] );

					}

				}

			}

			// use temp geometry to compute face and vertex normals for each morph

			const tmpGeo = new Geometry();
			tmpGeo.faces = this.faces;

			for ( let i = 0, il = this.morphTargets.length; i < il; i ++ ) {

				// create on first access

				if ( ! this.morphNormals[ i ] ) {

					this.morphNormals[ i ] = {};
					this.morphNormals[ i ].faceNormals = [];
					this.morphNormals[ i ].vertexNormals = [];

					const dstNormalsFace = this.morphNormals[ i ].faceNormals;
					const dstNormalsVertex = this.morphNormals[ i ].vertexNormals;

					for ( let f = 0, fl = this.faces.length; f < fl; f ++ ) {

						const faceNormal = new Vector3();
						const vertexNormals = { a: new Vector3(), b: new Vector3(), c: new Vector3() };

						dstNormalsFace.push( faceNormal );
						dstNormalsVertex.push( vertexNormals );

					}

				}

				const morphNormals = this.morphNormals[ i ];

				// set vertices to morph target

				tmpGeo.vertices = this.morphTargets[ i ].vertices;

				// compute morph normals

				tmpGeo.computeFaceNormals();
				tmpGeo.computeVertexNormals();

				// store morph normals

				for ( let f = 0, fl = this.faces.length; f < fl; f ++ ) {

					const face = this.faces[ f ];

					const faceNormal = morphNormals.faceNormals[ f ];
					const vertexNormals = morphNormals.vertexNormals[ f ];

					faceNormal.copy( face.normal );

					vertexNormals.a.copy( face.vertexNormals[ 0 ] );
					vertexNormals.b.copy( face.vertexNormals[ 1 ] );
					vertexNormals.c.copy( face.vertexNormals[ 2 ] );

				}

			}

			// restore original normals

			for ( let f = 0, fl = this.faces.length; f < fl; f ++ ) {

				const face = this.faces[ f ];

				face.normal = face.__originalFaceNormal;
				face.vertexNormals = face.__originalVertexNormals;

			}

		},

		computeBoundingBox: function () {

			if ( this.boundingBox === null ) {

				this.boundingBox = new Box3();

			}

			this.boundingBox.setFromPoints( this.vertices );

		},

		computeBoundingSphere: function () {

			if ( this.boundingSphere === null ) {

				this.boundingSphere = new Sphere();

			}

			this.boundingSphere.setFromPoints( this.vertices );

		},

		merge: function ( geometry, matrix, materialIndexOffset = 0 ) {

			if ( ! ( geometry && geometry.isGeometry ) ) {

				console.error( 'THREE.Geometry.merge(): geometry not an instance of THREE.Geometry.', geometry );
				return;

			}

			let normalMatrix;
			const vertexOffset = this.vertices.length,
				vertices1 = this.vertices,
				vertices2 = geometry.vertices,
				faces1 = this.faces,
				faces2 = geometry.faces,
				colors1 = this.colors,
				colors2 = geometry.colors;

			if ( matrix !== undefined ) {

				normalMatrix = new Matrix3().getNormalMatrix( matrix );

			}

			// vertices

			for ( let i = 0, il = vertices2.length; i < il; i ++ ) {

				const vertex = vertices2[ i ];

				const vertexCopy = vertex.clone();

				if ( matrix !== undefined ) vertexCopy.applyMatrix4( matrix );

				vertices1.push( vertexCopy );

			}

			// colors

			for ( let i = 0, il = colors2.length; i < il; i ++ ) {

				colors1.push( colors2[ i ].clone() );

			}

			// faces

			for ( let i = 0, il = faces2.length; i < il; i ++ ) {

				const face = faces2[ i ];
				let normal, color;
				const faceVertexNormals = face.vertexNormals,
					faceVertexColors = face.vertexColors;

				const faceCopy = new Face3( face.a + vertexOffset, face.b + vertexOffset, face.c + vertexOffset );
				faceCopy.normal.copy( face.normal );

				if ( normalMatrix !== undefined ) {

					faceCopy.normal.applyMatrix3( normalMatrix ).normalize();

				}

				for ( let j = 0, jl = faceVertexNormals.length; j < jl; j ++ ) {

					normal = faceVertexNormals[ j ].clone();

					if ( normalMatrix !== undefined ) {

						normal.applyMatrix3( normalMatrix ).normalize();

					}

					faceCopy.vertexNormals.push( normal );

				}

				faceCopy.color.copy( face.color );

				for ( let j = 0, jl = faceVertexColors.length; j < jl; j ++ ) {

					color = faceVertexColors[ j ];
					faceCopy.vertexColors.push( color.clone() );

				}

				faceCopy.materialIndex = face.materialIndex + materialIndexOffset;

				faces1.push( faceCopy );

			}

			// uvs

			for ( let i = 0, il = geometry.faceVertexUvs.length; i < il; i ++ ) {

				const faceVertexUvs2 = geometry.faceVertexUvs[ i ];

				if ( this.faceVertexUvs[ i ] === undefined ) this.faceVertexUvs[ i ] = [];

				for ( let j = 0, jl = faceVertexUvs2.length; j < jl; j ++ ) {

					const uvs2 = faceVertexUvs2[ j ], uvsCopy = [];

					for ( let k = 0, kl = uvs2.length; k < kl; k ++ ) {

						uvsCopy.push( uvs2[ k ].clone() );

					}

					this.faceVertexUvs[ i ].push( uvsCopy );

				}

			}

		},

		mergeMesh: function ( mesh ) {

			if ( ! ( mesh && mesh.isMesh ) ) {

				console.error( 'THREE.Geometry.mergeMesh(): mesh not an instance of THREE.Mesh.', mesh );
				return;

			}

			if ( mesh.matrixAutoUpdate ) mesh.updateMatrix();

			this.merge( mesh.geometry, mesh.matrix );

		},

		/*
		 * Checks for duplicate vertices with hashmap.
		 * Duplicated vertices are removed
		 * and faces' vertices are updated.
		 */

		mergeVertices: function ( precisionPoints = 4 ) {

			const verticesMap = {}; // Hashmap for looking up vertices by position coordinates (and making sure they are unique)
			const unique = [], changes = [];

			const precision = Math.pow( 10, precisionPoints );

			for ( let i = 0, il = this.vertices.length; i < il; i ++ ) {

				const v = this.vertices[ i ];
				const key = Math.round( v.x * precision ) + '_' + Math.round( v.y * precision ) + '_' + Math.round( v.z * precision );

				if ( verticesMap[ key ] === undefined ) {

					verticesMap[ key ] = i;
					unique.push( this.vertices[ i ] );
					changes[ i ] = unique.length - 1;

				} else {

					//console.log('Duplicate vertex found. ', i, ' could be using ', verticesMap[key]);
					changes[ i ] = changes[ verticesMap[ key ] ];

				}

			}


			// if faces are completely degenerate after merging vertices, we
			// have to remove them from the geometry.
			const faceIndicesToRemove = [];

			for ( let i = 0, il = this.faces.length; i < il; i ++ ) {

				const face = this.faces[ i ];

				face.a = changes[ face.a ];
				face.b = changes[ face.b ];
				face.c = changes[ face.c ];

				const indices = [ face.a, face.b, face.c ];

				// if any duplicate vertices are found in a Face3
				// we have to remove the face as nothing can be saved
				for ( let n = 0; n < 3; n ++ ) {

					if ( indices[ n ] === indices[ ( n + 1 ) % 3 ] ) {

						faceIndicesToRemove.push( i );
						break;

					}

				}

			}

			for ( let i = faceIndicesToRemove.length - 1; i >= 0; i -- ) {

				const idx = faceIndicesToRemove[ i ];

				this.faces.splice( idx, 1 );

				for ( let j = 0, jl = this.faceVertexUvs.length; j < jl; j ++ ) {

					this.faceVertexUvs[ j ].splice( idx, 1 );

				}

			}

			// Use unique set of vertices

			const diff = this.vertices.length - unique.length;
			this.vertices = unique;
			return diff;

		},

		setFromPoints: function ( points ) {

			this.vertices = [];

			for ( let i = 0, l = points.length; i < l; i ++ ) {

				const point = points[ i ];
				this.vertices.push( new Vector3( point.x, point.y, point.z || 0 ) );

			}

			return this;

		},

		sortFacesByMaterialIndex: function () {

			const faces = this.faces;
			const length = faces.length;

			// tag faces

			for ( let i = 0; i < length; i ++ ) {

				faces[ i ]._id = i;

			}

			// sort faces

			function materialIndexSort( a, b ) {

				return a.materialIndex - b.materialIndex;

			}

			faces.sort( materialIndexSort );

			// sort uvs

			const uvs1 = this.faceVertexUvs[ 0 ];
			const uvs2 = this.faceVertexUvs[ 1 ];

			let newUvs1, newUvs2;

			if ( uvs1 && uvs1.length === length ) newUvs1 = [];
			if ( uvs2 && uvs2.length === length ) newUvs2 = [];

			for ( let i = 0; i < length; i ++ ) {

				const id = faces[ i ]._id;

				if ( newUvs1 ) newUvs1.push( uvs1[ id ] );
				if ( newUvs2 ) newUvs2.push( uvs2[ id ] );

			}

			if ( newUvs1 ) this.faceVertexUvs[ 0 ] = newUvs1;
			if ( newUvs2 ) this.faceVertexUvs[ 1 ] = newUvs2;

		},

		toJSON: function () {

			const data = {
				metadata: {
					version: 4.5,
					type: 'Geometry',
					generator: 'Geometry.toJSON'
				}
			};

			// standard Geometry serialization

			data.uuid = this.uuid;
			data.type = this.type;
			if ( this.name !== '' ) data.name = this.name;

			if ( this.parameters !== undefined ) {

				const parameters = this.parameters;

				for ( const key in parameters ) {

					if ( parameters[ key ] !== undefined ) data[ key ] = parameters[ key ];

				}

				return data;

			}

			const vertices = [];

			for ( let i = 0; i < this.vertices.length; i ++ ) {

				const vertex = this.vertices[ i ];
				vertices.push( vertex.x, vertex.y, vertex.z );

			}

			const faces = [];
			const normals = [];
			const normalsHash = {};
			const colors = [];
			const colorsHash = {};
			const uvs = [];
			const uvsHash = {};

			for ( let i = 0; i < this.faces.length; i ++ ) {

				const face = this.faces[ i ];

				const hasMaterial = true;
				const hasFaceUv = false; // deprecated
				const hasFaceVertexUv = this.faceVertexUvs[ 0 ][ i ] !== undefined;
				const hasFaceNormal = face.normal.length() > 0;
				const hasFaceVertexNormal = face.vertexNormals.length > 0;
				const hasFaceColor = face.color.r !== 1 || face.color.g !== 1 || face.color.b !== 1;
				const hasFaceVertexColor = face.vertexColors.length > 0;

				let faceType = 0;

				faceType = setBit( faceType, 0, 0 ); // isQuad
				faceType = setBit( faceType, 1, hasMaterial );
				faceType = setBit( faceType, 2, hasFaceUv );
				faceType = setBit( faceType, 3, hasFaceVertexUv );
				faceType = setBit( faceType, 4, hasFaceNormal );
				faceType = setBit( faceType, 5, hasFaceVertexNormal );
				faceType = setBit( faceType, 6, hasFaceColor );
				faceType = setBit( faceType, 7, hasFaceVertexColor );

				faces.push( faceType );
				faces.push( face.a, face.b, face.c );
				faces.push( face.materialIndex );

				if ( hasFaceVertexUv ) {

					const faceVertexUvs = this.faceVertexUvs[ 0 ][ i ];

					faces.push(
						getUvIndex( faceVertexUvs[ 0 ] ),
						getUvIndex( faceVertexUvs[ 1 ] ),
						getUvIndex( faceVertexUvs[ 2 ] )
					);

				}

				if ( hasFaceNormal ) {

					faces.push( getNormalIndex( face.normal ) );

				}

				if ( hasFaceVertexNormal ) {

					const vertexNormals = face.vertexNormals;

					faces.push(
						getNormalIndex( vertexNormals[ 0 ] ),
						getNormalIndex( vertexNormals[ 1 ] ),
						getNormalIndex( vertexNormals[ 2 ] )
					);

				}

				if ( hasFaceColor ) {

					faces.push( getColorIndex( face.color ) );

				}

				if ( hasFaceVertexColor ) {

					const vertexColors = face.vertexColors;

					faces.push(
						getColorIndex( vertexColors[ 0 ] ),
						getColorIndex( vertexColors[ 1 ] ),
						getColorIndex( vertexColors[ 2 ] )
					);

				}

			}

			function setBit( value, position, enabled ) {

				return enabled ? value | ( 1 << position ) : value & ( ~ ( 1 << position ) );

			}

			function getNormalIndex( normal ) {

				const hash = normal.x.toString() + normal.y.toString() + normal.z.toString();

				if ( normalsHash[ hash ] !== undefined ) {

					return normalsHash[ hash ];

				}

				normalsHash[ hash ] = normals.length / 3;
				normals.push( normal.x, normal.y, normal.z );

				return normalsHash[ hash ];

			}

			function getColorIndex( color ) {

				const hash = color.r.toString() + color.g.toString() + color.b.toString();

				if ( colorsHash[ hash ] !== undefined ) {

					return colorsHash[ hash ];

				}

				colorsHash[ hash ] = colors.length;
				colors.push( color.getHex() );

				return colorsHash[ hash ];

			}

			function getUvIndex( uv ) {

				const hash = uv.x.toString() + uv.y.toString();

				if ( uvsHash[ hash ] !== undefined ) {

					return uvsHash[ hash ];

				}

				uvsHash[ hash ] = uvs.length / 2;
				uvs.push( uv.x, uv.y );

				return uvsHash[ hash ];

			}

			data.data = {};

			data.data.vertices = vertices;
			data.data.normals = normals;
			if ( colors.length > 0 ) data.data.colors = colors;
			if ( uvs.length > 0 ) data.data.uvs = [ uvs ]; // temporal backward compatibility
			data.data.faces = faces;

			return data;

		},

		clone: function () {

			/*
			 // Handle primitives

			 const parameters = this.parameters;

			 if ( parameters !== undefined ) {

			 const values = [];

			 for ( const key in parameters ) {

			 values.push( parameters[ key ] );

			 }

			 const geometry = Object.create( this.constructor.prototype );
			 this.constructor.apply( geometry, values );
			 return geometry;

			 }

			 return new this.constructor().copy( this );
			 */

			return new Geometry().copy( this );

		},

		copy: function ( source ) {

			// reset

			this.vertices = [];
			this.colors = [];
			this.faces = [];
			this.faceVertexUvs = [[]];
			this.morphTargets = [];
			this.morphNormals = [];
			this.skinWeights = [];
			this.skinIndices = [];
			this.lineDistances = [];
			this.boundingBox = null;
			this.boundingSphere = null;

			// name

			this.name = source.name;

			// vertices

			const vertices = source.vertices;

			for ( let i = 0, il = vertices.length; i < il; i ++ ) {

				this.vertices.push( vertices[ i ].clone() );

			}

			// colors

			const colors = source.colors;

			for ( let i = 0, il = colors.length; i < il; i ++ ) {

				this.colors.push( colors[ i ].clone() );

			}

			// faces

			const faces = source.faces;

			for ( let i = 0, il = faces.length; i < il; i ++ ) {

				this.faces.push( faces[ i ].clone() );

			}

			// face vertex uvs

			for ( let i = 0, il = source.faceVertexUvs.length; i < il; i ++ ) {

				const faceVertexUvs = source.faceVertexUvs[ i ];

				if ( this.faceVertexUvs[ i ] === undefined ) {

					this.faceVertexUvs[ i ] = [];

				}

				for ( let j = 0, jl = faceVertexUvs.length; j < jl; j ++ ) {

					const uvs = faceVertexUvs[ j ], uvsCopy = [];

					for ( let k = 0, kl = uvs.length; k < kl; k ++ ) {

						const uv = uvs[ k ];

						uvsCopy.push( uv.clone() );

					}

					this.faceVertexUvs[ i ].push( uvsCopy );

				}

			}

			// morph targets

			const morphTargets = source.morphTargets;

			for ( let i = 0, il = morphTargets.length; i < il; i ++ ) {

				const morphTarget = {};
				morphTarget.name = morphTargets[ i ].name;

				// vertices

				if ( morphTargets[ i ].vertices !== undefined ) {

					morphTarget.vertices = [];

					for ( let j = 0, jl = morphTargets[ i ].vertices.length; j < jl; j ++ ) {

						morphTarget.vertices.push( morphTargets[ i ].vertices[ j ].clone() );

					}

				}

				// normals

				if ( morphTargets[ i ].normals !== undefined ) {

					morphTarget.normals = [];

					for ( let j = 0, jl = morphTargets[ i ].normals.length; j < jl; j ++ ) {

						morphTarget.normals.push( morphTargets[ i ].normals[ j ].clone() );

					}

				}

				this.morphTargets.push( morphTarget );

			}

			// morph normals

			const morphNormals = source.morphNormals;

			for ( let i = 0, il = morphNormals.length; i < il; i ++ ) {

				const morphNormal = {};

				// vertex normals

				if ( morphNormals[ i ].vertexNormals !== undefined ) {

					morphNormal.vertexNormals = [];

					for ( let j = 0, jl = morphNormals[ i ].vertexNormals.length; j < jl; j ++ ) {

						const srcVertexNormal = morphNormals[ i ].vertexNormals[ j ];
						const destVertexNormal = {};

						destVertexNormal.a = srcVertexNormal.a.clone();
						destVertexNormal.b = srcVertexNormal.b.clone();
						destVertexNormal.c = srcVertexNormal.c.clone();

						morphNormal.vertexNormals.push( destVertexNormal );

					}

				}

				// face normals

				if ( morphNormals[ i ].faceNormals !== undefined ) {

					morphNormal.faceNormals = [];

					for ( let j = 0, jl = morphNormals[ i ].faceNormals.length; j < jl; j ++ ) {

						morphNormal.faceNormals.push( morphNormals[ i ].faceNormals[ j ].clone() );

					}

				}

				this.morphNormals.push( morphNormal );

			}

			// skin weights

			const skinWeights = source.skinWeights;

			for ( let i = 0, il = skinWeights.length; i < il; i ++ ) {

				this.skinWeights.push( skinWeights[ i ].clone() );

			}

			// skin indices

			const skinIndices = source.skinIndices;

			for ( let i = 0, il = skinIndices.length; i < il; i ++ ) {

				this.skinIndices.push( skinIndices[ i ].clone() );

			}

			// line distances

			const lineDistances = source.lineDistances;

			for ( let i = 0, il = lineDistances.length; i < il; i ++ ) {

				this.lineDistances.push( lineDistances[ i ] );

			}

			// bounding box

			const boundingBox = source.boundingBox;

			if ( boundingBox !== null ) {

				this.boundingBox = boundingBox.clone();

			}

			// bounding sphere

			const boundingSphere = source.boundingSphere;

			if ( boundingSphere !== null ) {

				this.boundingSphere = boundingSphere.clone();

			}

			// update flags

			this.elementsNeedUpdate = source.elementsNeedUpdate;
			this.verticesNeedUpdate = source.verticesNeedUpdate;
			this.uvsNeedUpdate = source.uvsNeedUpdate;
			this.normalsNeedUpdate = source.normalsNeedUpdate;
			this.colorsNeedUpdate = source.colorsNeedUpdate;
			this.lineDistancesNeedUpdate = source.lineDistancesNeedUpdate;
			this.groupsNeedUpdate = source.groupsNeedUpdate;

			return this;

		},

		dispose: function () {

			this.dispatchEvent( { type: 'dispose' } );

		}

	} );

	class BoxGeometry extends Geometry {

		constructor( width, height, depth, widthSegments, heightSegments, depthSegments ) {

			super();

			this.type = 'BoxGeometry';

			this.parameters = {
				width: width,
				height: height,
				depth: depth,
				widthSegments: widthSegments,
				heightSegments: heightSegments,
				depthSegments: depthSegments
			};

			this.fromBufferGeometry( new BoxBufferGeometry( width, height, depth, widthSegments, heightSegments, depthSegments ) );
			this.mergeVertices();

		}

	}

	class CircleBufferGeometry extends BufferGeometry {

		constructor( radius = 1, segments = 8, thetaStart = 0, thetaLength = Math.PI * 2 ) {

			super();

			this.type = 'CircleBufferGeometry';

			this.parameters = {
				radius: radius,
				segments: segments,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

			segments = Math.max( 3, segments );

			// buffers

			const indices = [];
			const vertices = [];
			const normals = [];
			const uvs = [];

			// helper variables

			const vertex = new Vector3();
			const uv = new Vector2();

			// center point

			vertices.push( 0, 0, 0 );
			normals.push( 0, 0, 1 );
			uvs.push( 0.5, 0.5 );

			for ( let s = 0, i = 3; s <= segments; s ++, i += 3 ) {

				const segment = thetaStart + s / segments * thetaLength;

				// vertex

				vertex.x = radius * Math.cos( segment );
				vertex.y = radius * Math.sin( segment );

				vertices.push( vertex.x, vertex.y, vertex.z );

				// normal

				normals.push( 0, 0, 1 );

				// uvs

				uv.x = ( vertices[ i ] / radius + 1 ) / 2;
				uv.y = ( vertices[ i + 1 ] / radius + 1 ) / 2;

				uvs.push( uv.x, uv.y );

			}

			// indices

			for ( let i = 1; i <= segments; i ++ ) {

				indices.push( i, i + 1, 0 );

			}

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

		}

	}

	class CircleGeometry extends Geometry {

		constructor( radius, segments, thetaStart, thetaLength ) {

			super();
			this.type = 'CircleGeometry';

			this.parameters = {
				radius: radius,
				segments: segments,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

			this.fromBufferGeometry( new CircleBufferGeometry( radius, segments, thetaStart, thetaLength ) );
			this.mergeVertices();

		}

	}

	class CylinderBufferGeometry extends BufferGeometry {

		constructor( radiusTop = 1, radiusBottom = 1, height = 1, radialSegments = 8, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2 ) {

			super();
			this.type = 'CylinderBufferGeometry';

			this.parameters = {
				radiusTop: radiusTop,
				radiusBottom: radiusBottom,
				height: height,
				radialSegments: radialSegments,
				heightSegments: heightSegments,
				openEnded: openEnded,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

			const scope = this;

			radialSegments = Math.floor( radialSegments );
			heightSegments = Math.floor( heightSegments );

			// buffers

			const indices = [];
			const vertices = [];
			const normals = [];
			const uvs = [];

			// helper variables

			let index = 0;
			const indexArray = [];
			const halfHeight = height / 2;
			let groupStart = 0;

			// generate geometry

			generateTorso();

			if ( openEnded === false ) {

				if ( radiusTop > 0 ) generateCap( true );
				if ( radiusBottom > 0 ) generateCap( false );

			}

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

			function generateTorso() {

				const normal = new Vector3();
				const vertex = new Vector3();

				let groupCount = 0;

				// this will be used to calculate the normal
				const slope = ( radiusBottom - radiusTop ) / height;

				// generate vertices, normals and uvs

				for ( let y = 0; y <= heightSegments; y ++ ) {

					const indexRow = [];

					const v = y / heightSegments;

					// calculate the radius of the current row

					const radius = v * ( radiusBottom - radiusTop ) + radiusTop;

					for ( let x = 0; x <= radialSegments; x ++ ) {

						const u = x / radialSegments;

						const theta = u * thetaLength + thetaStart;

						const sinTheta = Math.sin( theta );
						const cosTheta = Math.cos( theta );

						// vertex

						vertex.x = radius * sinTheta;
						vertex.y = - v * height + halfHeight;
						vertex.z = radius * cosTheta;
						vertices.push( vertex.x, vertex.y, vertex.z );

						// normal

						normal.set( sinTheta, slope, cosTheta ).normalize();
						normals.push( normal.x, normal.y, normal.z );

						// uv

						uvs.push( u, 1 - v );

						// save index of vertex in respective row

						indexRow.push( index ++ );

					}

					// now save vertices of the row in our index array

					indexArray.push( indexRow );

				}

				// generate indices

				for ( let x = 0; x < radialSegments; x ++ ) {

					for ( let y = 0; y < heightSegments; y ++ ) {

						// we use the index array to access the correct indices

						const a = indexArray[ y ][ x ];
						const b = indexArray[ y + 1 ][ x ];
						const c = indexArray[ y + 1 ][ x + 1 ];
						const d = indexArray[ y ][ x + 1 ];

						// faces

						indices.push( a, b, d );
						indices.push( b, c, d );

						// update group counter

						groupCount += 6;

					}

				}

				// add a group to the geometry. this will ensure multi material support

				scope.addGroup( groupStart, groupCount, 0 );

				// calculate new start value for groups

				groupStart += groupCount;

			}

			function generateCap( top ) {

				// save the index of the first center vertex
				const centerIndexStart = index;

				const uv = new Vector2();
				const vertex = new Vector3();

				let groupCount = 0;

				const radius = ( top === true ) ? radiusTop : radiusBottom;
				const sign = ( top === true ) ? 1 : - 1;

				// first we generate the center vertex data of the cap.
				// because the geometry needs one set of uvs per face,
				// we must generate a center vertex per face/segment

				for ( let x = 1; x <= radialSegments; x ++ ) {

					// vertex

					vertices.push( 0, halfHeight * sign, 0 );

					// normal

					normals.push( 0, sign, 0 );

					// uv

					uvs.push( 0.5, 0.5 );

					// increase index

					index ++;

				}

				// save the index of the last center vertex
				const centerIndexEnd = index;

				// now we generate the surrounding vertices, normals and uvs

				for ( let x = 0; x <= radialSegments; x ++ ) {

					const u = x / radialSegments;
					const theta = u * thetaLength + thetaStart;

					const cosTheta = Math.cos( theta );
					const sinTheta = Math.sin( theta );

					// vertex

					vertex.x = radius * sinTheta;
					vertex.y = halfHeight * sign;
					vertex.z = radius * cosTheta;
					vertices.push( vertex.x, vertex.y, vertex.z );

					// normal

					normals.push( 0, sign, 0 );

					// uv

					uv.x = ( cosTheta * 0.5 ) + 0.5;
					uv.y = ( sinTheta * 0.5 * sign ) + 0.5;
					uvs.push( uv.x, uv.y );

					// increase index

					index ++;

				}

				// generate indices

				for ( let x = 0; x < radialSegments; x ++ ) {

					const c = centerIndexStart + x;
					const i = centerIndexEnd + x;

					if ( top === true ) {

						// face top

						indices.push( i, i + 1, c );

					} else {

						// face bottom

						indices.push( i + 1, i, c );

					}

					groupCount += 3;

				}

				// add a group to the geometry. this will ensure multi material support

				scope.addGroup( groupStart, groupCount, top === true ? 1 : 2 );

				// calculate new start value for groups

				groupStart += groupCount;

			}

		}

	}

	class CylinderGeometry extends Geometry {

		constructor( radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength ) {

			super();
			this.type = 'CylinderGeometry';

			this.parameters = {
				radiusTop: radiusTop,
				radiusBottom: radiusBottom,
				height: height,
				radialSegments: radialSegments,
				heightSegments: heightSegments,
				openEnded: openEnded,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

			this.fromBufferGeometry( new CylinderBufferGeometry( radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength ) );
			this.mergeVertices();

		}

	}

	class ConeGeometry extends CylinderGeometry {

		constructor( radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength ) {

			super( 0, radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength );
			this.type = 'ConeGeometry';

			this.parameters = {
				radius: radius,
				height: height,
				radialSegments: radialSegments,
				heightSegments: heightSegments,
				openEnded: openEnded,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

		}

	}

	class ConeBufferGeometry extends CylinderBufferGeometry {

		constructor( radius = 1, height = 1, radialSegments = 8, heightSegments = 1, openEnded = false, thetaStart = 0, thetaLength = Math.PI * 2 ) {

			super( 0, radius, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength );

			this.type = 'ConeBufferGeometry';

			this.parameters = {
				radius: radius,
				height: height,
				radialSegments: radialSegments,
				heightSegments: heightSegments,
				openEnded: openEnded,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

		}

	}

	class PolyhedronBufferGeometry extends BufferGeometry {

		constructor( vertices, indices, radius = 1, detail = 0 ) {

			super();

			this.type = 'PolyhedronBufferGeometry';

			this.parameters = {
				vertices: vertices,
				indices: indices,
				radius: radius,
				detail: detail
			};

			// default buffer data

			const vertexBuffer = [];
			const uvBuffer = [];

			// the subdivision creates the vertex buffer data

			subdivide( detail );

			// all vertices should lie on a conceptual sphere with a given radius

			applyRadius( radius );

			// finally, create the uv data

			generateUVs();

			// build non-indexed geometry

			this.setAttribute( 'position', new Float32BufferAttribute( vertexBuffer, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( vertexBuffer.slice(), 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvBuffer, 2 ) );

			if ( detail === 0 ) {

				this.computeVertexNormals(); // flat normals

			} else {

				this.normalizeNormals(); // smooth normals

			}

			// helper functions

			function subdivide( detail ) {

				const a = new Vector3();
				const b = new Vector3();
				const c = new Vector3();

				// iterate over all faces and apply a subdivison with the given detail value

				for ( let i = 0; i < indices.length; i += 3 ) {

					// get the vertices of the face

					getVertexByIndex( indices[ i + 0 ], a );
					getVertexByIndex( indices[ i + 1 ], b );
					getVertexByIndex( indices[ i + 2 ], c );

					// perform subdivision

					subdivideFace( a, b, c, detail );

				}

			}

			function subdivideFace( a, b, c, detail ) {

				const cols = detail + 1;

				// we use this multidimensional array as a data structure for creating the subdivision

				const v = [];

				// construct all of the vertices for this subdivision

				for ( let i = 0; i <= cols; i ++ ) {

					v[ i ] = [];

					const aj = a.clone().lerp( c, i / cols );
					const bj = b.clone().lerp( c, i / cols );

					const rows = cols - i;

					for ( let j = 0; j <= rows; j ++ ) {

						if ( j === 0 && i === cols ) {

							v[ i ][ j ] = aj;

						} else {

							v[ i ][ j ] = aj.clone().lerp( bj, j / rows );

						}

					}

				}

				// construct all of the faces

				for ( let i = 0; i < cols; i ++ ) {

					for ( let j = 0; j < 2 * ( cols - i ) - 1; j ++ ) {

						const k = Math.floor( j / 2 );

						if ( j % 2 === 0 ) {

							pushVertex( v[ i ][ k + 1 ] );
							pushVertex( v[ i + 1 ][ k ] );
							pushVertex( v[ i ][ k ] );

						} else {

							pushVertex( v[ i ][ k + 1 ] );
							pushVertex( v[ i + 1 ][ k + 1 ] );
							pushVertex( v[ i + 1 ][ k ] );

						}

					}

				}

			}

			function applyRadius( radius ) {

				const vertex = new Vector3();

				// iterate over the entire buffer and apply the radius to each vertex

				for ( let i = 0; i < vertexBuffer.length; i += 3 ) {

					vertex.x = vertexBuffer[ i + 0 ];
					vertex.y = vertexBuffer[ i + 1 ];
					vertex.z = vertexBuffer[ i + 2 ];

					vertex.normalize().multiplyScalar( radius );

					vertexBuffer[ i + 0 ] = vertex.x;
					vertexBuffer[ i + 1 ] = vertex.y;
					vertexBuffer[ i + 2 ] = vertex.z;

				}

			}

			function generateUVs() {

				const vertex = new Vector3();

				for ( let i = 0; i < vertexBuffer.length; i += 3 ) {

					vertex.x = vertexBuffer[ i + 0 ];
					vertex.y = vertexBuffer[ i + 1 ];
					vertex.z = vertexBuffer[ i + 2 ];

					const u = azimuth( vertex ) / 2 / Math.PI + 0.5;
					const v = inclination( vertex ) / Math.PI + 0.5;
					uvBuffer.push( u, 1 - v );

				}

				correctUVs();

				correctSeam();

			}

			function correctSeam() {

				// handle case when face straddles the seam, see #3269

				for ( let i = 0; i < uvBuffer.length; i += 6 ) {

					// uv data of a single face

					const x0 = uvBuffer[ i + 0 ];
					const x1 = uvBuffer[ i + 2 ];
					const x2 = uvBuffer[ i + 4 ];

					const max = Math.max( x0, x1, x2 );
					const min = Math.min( x0, x1, x2 );

					// 0.9 is somewhat arbitrary

					if ( max > 0.9 && min < 0.1 ) {

						if ( x0 < 0.2 ) uvBuffer[ i + 0 ] += 1;
						if ( x1 < 0.2 ) uvBuffer[ i + 2 ] += 1;
						if ( x2 < 0.2 ) uvBuffer[ i + 4 ] += 1;

					}

				}

			}

			function pushVertex( vertex ) {

				vertexBuffer.push( vertex.x, vertex.y, vertex.z );

			}

			function getVertexByIndex( index, vertex ) {

				const stride = index * 3;

				vertex.x = vertices[ stride + 0 ];
				vertex.y = vertices[ stride + 1 ];
				vertex.z = vertices[ stride + 2 ];

			}

			function correctUVs() {

				const a = new Vector3();
				const b = new Vector3();
				const c = new Vector3();

				const centroid = new Vector3();

				const uvA = new Vector2();
				const uvB = new Vector2();
				const uvC = new Vector2();

				for ( let i = 0, j = 0; i < vertexBuffer.length; i += 9, j += 6 ) {

					a.set( vertexBuffer[ i + 0 ], vertexBuffer[ i + 1 ], vertexBuffer[ i + 2 ] );
					b.set( vertexBuffer[ i + 3 ], vertexBuffer[ i + 4 ], vertexBuffer[ i + 5 ] );
					c.set( vertexBuffer[ i + 6 ], vertexBuffer[ i + 7 ], vertexBuffer[ i + 8 ] );

					uvA.set( uvBuffer[ j + 0 ], uvBuffer[ j + 1 ] );
					uvB.set( uvBuffer[ j + 2 ], uvBuffer[ j + 3 ] );
					uvC.set( uvBuffer[ j + 4 ], uvBuffer[ j + 5 ] );

					centroid.copy( a ).add( b ).add( c ).divideScalar( 3 );

					const azi = azimuth( centroid );

					correctUV( uvA, j + 0, a, azi );
					correctUV( uvB, j + 2, b, azi );
					correctUV( uvC, j + 4, c, azi );

				}

			}

			function correctUV( uv, stride, vector, azimuth ) {

				if ( ( azimuth < 0 ) && ( uv.x === 1 ) ) {

					uvBuffer[ stride ] = uv.x - 1;

				}

				if ( ( vector.x === 0 ) && ( vector.z === 0 ) ) {

					uvBuffer[ stride ] = azimuth / 2 / Math.PI + 0.5;

				}

			}

			// Angle around the Y axis, counter-clockwise when looking from above.

			function azimuth( vector ) {

				return Math.atan2( vector.z, - vector.x );

			}


			// Angle above the XZ plane.

			function inclination( vector ) {

				return Math.atan2( - vector.y, Math.sqrt( ( vector.x * vector.x ) + ( vector.z * vector.z ) ) );

			}

		}

	}

	class DodecahedronBufferGeometry extends PolyhedronBufferGeometry {

		constructor( radius = 1, detail = 0 ) {

			const t = ( 1 + Math.sqrt( 5 ) ) / 2;
			const r = 1 / t;

			const vertices = [

				// (±1, ±1, ±1)
				- 1, - 1, - 1,	- 1, - 1, 1,
				- 1, 1, - 1, - 1, 1, 1,
				1, - 1, - 1, 1, - 1, 1,
				1, 1, - 1, 1, 1, 1,

				// (0, ±1/φ, ±φ)
				0, - r, - t, 0, - r, t,
				0, r, - t, 0, r, t,

				// (±1/φ, ±φ, 0)
				- r, - t, 0, - r, t, 0,
				r, - t, 0, r, t, 0,

				// (±φ, 0, ±1/φ)
				- t, 0, - r, t, 0, - r,
				- t, 0, r, t, 0, r
			];

			const indices = [
				3, 11, 7, 	3, 7, 15, 	3, 15, 13,
				7, 19, 17, 	7, 17, 6, 	7, 6, 15,
				17, 4, 8, 	17, 8, 10, 	17, 10, 6,
				8, 0, 16, 	8, 16, 2, 	8, 2, 10,
				0, 12, 1, 	0, 1, 18, 	0, 18, 16,
				6, 10, 2, 	6, 2, 13, 	6, 13, 15,
				2, 16, 18, 	2, 18, 3, 	2, 3, 13,
				18, 1, 9, 	18, 9, 11, 	18, 11, 3,
				4, 14, 12, 	4, 12, 0, 	4, 0, 8,
				11, 9, 5, 	11, 5, 19, 	11, 19, 7,
				19, 5, 14, 	19, 14, 4, 	19, 4, 17,
				1, 12, 14, 	1, 14, 5, 	1, 5, 9
			];

			super( vertices, indices, radius, detail );

			this.type = 'DodecahedronBufferGeometry';

			this.parameters = {
				radius: radius,
				detail: detail
			};

		}

	}

	class DodecahedronGeometry extends Geometry {

		constructor( radius, detail ) {

			super();
			this.type = 'DodecahedronGeometry';

			this.parameters = {
				radius: radius,
				detail: detail
			};

			this.fromBufferGeometry( new DodecahedronBufferGeometry( radius, detail ) );
			this.mergeVertices();

		}

	}

	const _v0$2 = new Vector3();
	const _v1$5 = new Vector3();
	const _normal$1 = new Vector3();
	const _triangle = new Triangle();

	class EdgesGeometry extends BufferGeometry {

		constructor( geometry, thresholdAngle ) {

			super();

			this.type = 'EdgesGeometry';

			this.parameters = {
				thresholdAngle: thresholdAngle
			};

			thresholdAngle = ( thresholdAngle !== undefined ) ? thresholdAngle : 1;

			if ( geometry.isGeometry ) {

				geometry = new BufferGeometry().fromGeometry( geometry );

			}

			const precisionPoints = 4;
			const precision = Math.pow( 10, precisionPoints );
			const thresholdDot = Math.cos( MathUtils.DEG2RAD * thresholdAngle );

			const indexAttr = geometry.getIndex();
			const positionAttr = geometry.getAttribute( 'position' );
			const indexCount = indexAttr ? indexAttr.count : positionAttr.count;

			const indexArr = [ 0, 0, 0 ];
			const vertKeys = [ 'a', 'b', 'c' ];
			const hashes = new Array( 3 );

			const edgeData = {};
			const vertices = [];
			for ( let i = 0; i < indexCount; i += 3 ) {

				if ( indexAttr ) {

					indexArr[ 0 ] = indexAttr.getX( i );
					indexArr[ 1 ] = indexAttr.getX( i + 1 );
					indexArr[ 2 ] = indexAttr.getX( i + 2 );

				} else {

					indexArr[ 0 ] = i;
					indexArr[ 1 ] = i + 1;
					indexArr[ 2 ] = i + 2;

				}

				const { a, b, c } = _triangle;
				a.fromBufferAttribute( positionAttr, indexArr[ 0 ] );
				b.fromBufferAttribute( positionAttr, indexArr[ 1 ] );
				c.fromBufferAttribute( positionAttr, indexArr[ 2 ] );
				_triangle.getNormal( _normal$1 );

				// create hashes for the edge from the vertices
				hashes[ 0 ] = `${ Math.round( a.x * precision ) },${ Math.round( a.y * precision ) },${ Math.round( a.z * precision ) }`;
				hashes[ 1 ] = `${ Math.round( b.x * precision ) },${ Math.round( b.y * precision ) },${ Math.round( b.z * precision ) }`;
				hashes[ 2 ] = `${ Math.round( c.x * precision ) },${ Math.round( c.y * precision ) },${ Math.round( c.z * precision ) }`;

				// skip degenerate triangles
				if ( hashes[ 0 ] === hashes[ 1 ] || hashes[ 1 ] === hashes[ 2 ] || hashes[ 2 ] === hashes[ 0 ] ) {

					continue;

				}

				// iterate over every edge
				for ( let j = 0; j < 3; j ++ ) {

					// get the first and next vertex making up the edge
					const jNext = ( j + 1 ) % 3;
					const vecHash0 = hashes[ j ];
					const vecHash1 = hashes[ jNext ];
					const v0 = _triangle[ vertKeys[ j ] ];
					const v1 = _triangle[ vertKeys[ jNext ] ];

					const hash = `${ vecHash0 }_${ vecHash1 }`;
					const reverseHash = `${ vecHash1 }_${ vecHash0 }`;

					if ( reverseHash in edgeData && edgeData[ reverseHash ] ) {

						// if we found a sibling edge add it into the vertex array if
						// it meets the angle threshold and delete the edge from the map.
						if ( _normal$1.dot( edgeData[ reverseHash ].normal ) <= thresholdDot ) {

							vertices.push( v0.x, v0.y, v0.z );
							vertices.push( v1.x, v1.y, v1.z );

						}

						edgeData[ reverseHash ] = null;

					} else if ( ! ( hash in edgeData ) ) {

						// if we've already got an edge here then skip adding a new one
						edgeData[ hash ] = {

							index0: indexArr[ j ],
							index1: indexArr[ jNext ],
							normal: _normal$1.clone(),

						};

					}

				}

			}

			// iterate over all remaining, unmatched edges and add them to the vertex array
			for ( const key in edgeData ) {

				if ( edgeData[ key ] ) {

					const { index0, index1 } = edgeData[ key ];
					_v0$2.fromBufferAttribute( positionAttr, index0 );
					_v1$5.fromBufferAttribute( positionAttr, index1 );

					vertices.push( _v0$2.x, _v0$2.y, _v0$2.z );
					vertices.push( _v1$5.x, _v1$5.y, _v1$5.z );

				}

			}

			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );

		}

	}

	/**
	 * Port from https://github.com/mapbox/earcut (v2.2.2)
	 */

	const Earcut = {

		triangulate: function ( data, holeIndices, dim ) {

			dim = dim || 2;

			const hasHoles = holeIndices && holeIndices.length;
			const outerLen = hasHoles ? holeIndices[ 0 ] * dim : data.length;
			let outerNode = linkedList( data, 0, outerLen, dim, true );
			const triangles = [];

			if ( ! outerNode || outerNode.next === outerNode.prev ) return triangles;

			let minX, minY, maxX, maxY, x, y, invSize;

			if ( hasHoles ) outerNode = eliminateHoles( data, holeIndices, outerNode, dim );

			// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
			if ( data.length > 80 * dim ) {

				minX = maxX = data[ 0 ];
				minY = maxY = data[ 1 ];

				for ( let i = dim; i < outerLen; i += dim ) {

					x = data[ i ];
					y = data[ i + 1 ];
					if ( x < minX ) minX = x;
					if ( y < minY ) minY = y;
					if ( x > maxX ) maxX = x;
					if ( y > maxY ) maxY = y;

				}

				// minX, minY and invSize are later used to transform coords into integers for z-order calculation
				invSize = Math.max( maxX - minX, maxY - minY );
				invSize = invSize !== 0 ? 1 / invSize : 0;

			}

			earcutLinked( outerNode, triangles, dim, minX, minY, invSize );

			return triangles;

		}

	};

	// create a circular doubly linked list from polygon points in the specified winding order
	function linkedList( data, start, end, dim, clockwise ) {

		let i, last;

		if ( clockwise === ( signedArea( data, start, end, dim ) > 0 ) ) {

			for ( i = start; i < end; i += dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );

		} else {

			for ( i = end - dim; i >= start; i -= dim ) last = insertNode( i, data[ i ], data[ i + 1 ], last );

		}

		if ( last && equals( last, last.next ) ) {

			removeNode( last );
			last = last.next;

		}

		return last;

	}

	// eliminate colinear or duplicate points
	function filterPoints( start, end ) {

		if ( ! start ) return start;
		if ( ! end ) end = start;

		let p = start,
			again;
		do {

			again = false;

			if ( ! p.steiner && ( equals( p, p.next ) || area( p.prev, p, p.next ) === 0 ) ) {

				removeNode( p );
				p = end = p.prev;
				if ( p === p.next ) break;
				again = true;

			} else {

				p = p.next;

			}

		} while ( again || p !== end );

		return end;

	}

	// main ear slicing loop which triangulates a polygon (given as a linked list)
	function earcutLinked( ear, triangles, dim, minX, minY, invSize, pass ) {

		if ( ! ear ) return;

		// interlink polygon nodes in z-order
		if ( ! pass && invSize ) indexCurve( ear, minX, minY, invSize );

		let stop = ear,
			prev, next;

		// iterate through ears, slicing them one by one
		while ( ear.prev !== ear.next ) {

			prev = ear.prev;
			next = ear.next;

			if ( invSize ? isEarHashed( ear, minX, minY, invSize ) : isEar( ear ) ) {

				// cut off the triangle
				triangles.push( prev.i / dim );
				triangles.push( ear.i / dim );
				triangles.push( next.i / dim );

				removeNode( ear );

				// skipping the next vertex leads to less sliver triangles
				ear = next.next;
				stop = next.next;

				continue;

			}

			ear = next;

			// if we looped through the whole remaining polygon and can't find any more ears
			if ( ear === stop ) {

				// try filtering points and slicing again
				if ( ! pass ) {

					earcutLinked( filterPoints( ear ), triangles, dim, minX, minY, invSize, 1 );

					// if this didn't work, try curing all small self-intersections locally

				} else if ( pass === 1 ) {

					ear = cureLocalIntersections( filterPoints( ear ), triangles, dim );
					earcutLinked( ear, triangles, dim, minX, minY, invSize, 2 );

					// as a last resort, try splitting the remaining polygon into two

				} else if ( pass === 2 ) {

					splitEarcut( ear, triangles, dim, minX, minY, invSize );

				}

				break;

			}

		}

	}

	// check whether a polygon node forms a valid ear with adjacent nodes
	function isEar( ear ) {

		const a = ear.prev,
			b = ear,
			c = ear.next;

		if ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear

		// now make sure we don't have other points inside the potential ear
		let p = ear.next.next;

		while ( p !== ear.prev ) {

			if ( pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y ) &&
				area( p.prev, p, p.next ) >= 0 ) return false;
			p = p.next;

		}

		return true;

	}

	function isEarHashed( ear, minX, minY, invSize ) {

		const a = ear.prev,
			b = ear,
			c = ear.next;

		if ( area( a, b, c ) >= 0 ) return false; // reflex, can't be an ear

		// triangle bbox; min & max are calculated like this for speed
		const minTX = a.x < b.x ? ( a.x < c.x ? a.x : c.x ) : ( b.x < c.x ? b.x : c.x ),
			minTY = a.y < b.y ? ( a.y < c.y ? a.y : c.y ) : ( b.y < c.y ? b.y : c.y ),
			maxTX = a.x > b.x ? ( a.x > c.x ? a.x : c.x ) : ( b.x > c.x ? b.x : c.x ),
			maxTY = a.y > b.y ? ( a.y > c.y ? a.y : c.y ) : ( b.y > c.y ? b.y : c.y );

		// z-order range for the current triangle bbox;
		const minZ = zOrder( minTX, minTY, minX, minY, invSize ),
			maxZ = zOrder( maxTX, maxTY, minX, minY, invSize );

		let p = ear.prevZ,
			n = ear.nextZ;

		// look for points inside the triangle in both directions
		while ( p && p.z >= minZ && n && n.z <= maxZ ) {

			if ( p !== ear.prev && p !== ear.next &&
				pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y ) &&
				area( p.prev, p, p.next ) >= 0 ) return false;
			p = p.prevZ;

			if ( n !== ear.prev && n !== ear.next &&
				pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y ) &&
				area( n.prev, n, n.next ) >= 0 ) return false;
			n = n.nextZ;

		}

		// look for remaining points in decreasing z-order
		while ( p && p.z >= minZ ) {

			if ( p !== ear.prev && p !== ear.next &&
				pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y ) &&
				area( p.prev, p, p.next ) >= 0 ) return false;
			p = p.prevZ;

		}

		// look for remaining points in increasing z-order
		while ( n && n.z <= maxZ ) {

			if ( n !== ear.prev && n !== ear.next &&
				pointInTriangle( a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y ) &&
				area( n.prev, n, n.next ) >= 0 ) return false;
			n = n.nextZ;

		}

		return true;

	}

	// go through all polygon nodes and cure small local self-intersections
	function cureLocalIntersections( start, triangles, dim ) {

		let p = start;
		do {

			const a = p.prev,
				b = p.next.next;

			if ( ! equals( a, b ) && intersects( a, p, p.next, b ) && locallyInside( a, b ) && locallyInside( b, a ) ) {

				triangles.push( a.i / dim );
				triangles.push( p.i / dim );
				triangles.push( b.i / dim );

				// remove two nodes involved
				removeNode( p );
				removeNode( p.next );

				p = start = b;

			}

			p = p.next;

		} while ( p !== start );

		return filterPoints( p );

	}

	// try splitting polygon into two and triangulate them independently
	function splitEarcut( start, triangles, dim, minX, minY, invSize ) {

		// look for a valid diagonal that divides the polygon into two
		let a = start;
		do {

			let b = a.next.next;
			while ( b !== a.prev ) {

				if ( a.i !== b.i && isValidDiagonal( a, b ) ) {

					// split the polygon in two by the diagonal
					let c = splitPolygon( a, b );

					// filter colinear points around the cuts
					a = filterPoints( a, a.next );
					c = filterPoints( c, c.next );

					// run earcut on each half
					earcutLinked( a, triangles, dim, minX, minY, invSize );
					earcutLinked( c, triangles, dim, minX, minY, invSize );
					return;

				}

				b = b.next;

			}

			a = a.next;

		} while ( a !== start );

	}

	// link every hole into the outer loop, producing a single-ring polygon without holes
	function eliminateHoles( data, holeIndices, outerNode, dim ) {

		const queue = [];
		let i, len, start, end, list;

		for ( i = 0, len = holeIndices.length; i < len; i ++ ) {

			start = holeIndices[ i ] * dim;
			end = i < len - 1 ? holeIndices[ i + 1 ] * dim : data.length;
			list = linkedList( data, start, end, dim, false );
			if ( list === list.next ) list.steiner = true;
			queue.push( getLeftmost( list ) );

		}

		queue.sort( compareX );

		// process holes from left to right
		for ( i = 0; i < queue.length; i ++ ) {

			eliminateHole( queue[ i ], outerNode );
			outerNode = filterPoints( outerNode, outerNode.next );

		}

		return outerNode;

	}

	function compareX( a, b ) {

		return a.x - b.x;

	}

	// find a bridge between vertices that connects hole with an outer ring and and link it
	function eliminateHole( hole, outerNode ) {

		outerNode = findHoleBridge( hole, outerNode );
		if ( outerNode ) {

			const b = splitPolygon( outerNode, hole );

			// filter collinear points around the cuts
			filterPoints( outerNode, outerNode.next );
			filterPoints( b, b.next );

		}

	}

	// David Eberly's algorithm for finding a bridge between hole and outer polygon
	function findHoleBridge( hole, outerNode ) {

		let p = outerNode;
		const hx = hole.x;
		const hy = hole.y;
		let qx = - Infinity, m;

		// find a segment intersected by a ray from the hole's leftmost point to the left;
		// segment's endpoint with lesser x will be potential connection point
		do {

			if ( hy <= p.y && hy >= p.next.y && p.next.y !== p.y ) {

				const x = p.x + ( hy - p.y ) * ( p.next.x - p.x ) / ( p.next.y - p.y );
				if ( x <= hx && x > qx ) {

					qx = x;
					if ( x === hx ) {

						if ( hy === p.y ) return p;
						if ( hy === p.next.y ) return p.next;

					}

					m = p.x < p.next.x ? p : p.next;

				}

			}

			p = p.next;

		} while ( p !== outerNode );

		if ( ! m ) return null;

		if ( hx === qx ) return m; // hole touches outer segment; pick leftmost endpoint

		// look for points inside the triangle of hole point, segment intersection and endpoint;
		// if there are no points found, we have a valid connection;
		// otherwise choose the point of the minimum angle with the ray as connection point

		const stop = m,
			mx = m.x,
			my = m.y;
		let tanMin = Infinity, tan;

		p = m;

		do {

			if ( hx >= p.x && p.x >= mx && hx !== p.x &&
					pointInTriangle( hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y ) ) {

				tan = Math.abs( hy - p.y ) / ( hx - p.x ); // tangential

				if ( locallyInside( p, hole ) && ( tan < tanMin || ( tan === tanMin && ( p.x > m.x || ( p.x === m.x && sectorContainsSector( m, p ) ) ) ) ) ) {

					m = p;
					tanMin = tan;

				}

			}

			p = p.next;

		} while ( p !== stop );

		return m;

	}

	// whether sector in vertex m contains sector in vertex p in the same coordinates
	function sectorContainsSector( m, p ) {

		return area( m.prev, m, p.prev ) < 0 && area( p.next, m, m.next ) < 0;

	}

	// interlink polygon nodes in z-order
	function indexCurve( start, minX, minY, invSize ) {

		let p = start;
		do {

			if ( p.z === null ) p.z = zOrder( p.x, p.y, minX, minY, invSize );
			p.prevZ = p.prev;
			p.nextZ = p.next;
			p = p.next;

		} while ( p !== start );

		p.prevZ.nextZ = null;
		p.prevZ = null;

		sortLinked( p );

	}

	// Simon Tatham's linked list merge sort algorithm
	// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
	function sortLinked( list ) {

		let i, p, q, e, tail, numMerges, pSize, qSize,
			inSize = 1;

		do {

			p = list;
			list = null;
			tail = null;
			numMerges = 0;

			while ( p ) {

				numMerges ++;
				q = p;
				pSize = 0;
				for ( i = 0; i < inSize; i ++ ) {

					pSize ++;
					q = q.nextZ;
					if ( ! q ) break;

				}

				qSize = inSize;

				while ( pSize > 0 || ( qSize > 0 && q ) ) {

					if ( pSize !== 0 && ( qSize === 0 || ! q || p.z <= q.z ) ) {

						e = p;
						p = p.nextZ;
						pSize --;

					} else {

						e = q;
						q = q.nextZ;
						qSize --;

					}

					if ( tail ) tail.nextZ = e;
					else list = e;

					e.prevZ = tail;
					tail = e;

				}

				p = q;

			}

			tail.nextZ = null;
			inSize *= 2;

		} while ( numMerges > 1 );

		return list;

	}

	// z-order of a point given coords and inverse of the longer side of data bbox
	function zOrder( x, y, minX, minY, invSize ) {

		// coords are transformed into non-negative 15-bit integer range
		x = 32767 * ( x - minX ) * invSize;
		y = 32767 * ( y - minY ) * invSize;

		x = ( x | ( x << 8 ) ) & 0x00FF00FF;
		x = ( x | ( x << 4 ) ) & 0x0F0F0F0F;
		x = ( x | ( x << 2 ) ) & 0x33333333;
		x = ( x | ( x << 1 ) ) & 0x55555555;

		y = ( y | ( y << 8 ) ) & 0x00FF00FF;
		y = ( y | ( y << 4 ) ) & 0x0F0F0F0F;
		y = ( y | ( y << 2 ) ) & 0x33333333;
		y = ( y | ( y << 1 ) ) & 0x55555555;

		return x | ( y << 1 );

	}

	// find the leftmost node of a polygon ring
	function getLeftmost( start ) {

		let p = start,
			leftmost = start;
		do {

			if ( p.x < leftmost.x || ( p.x === leftmost.x && p.y < leftmost.y ) ) leftmost = p;
			p = p.next;

		} while ( p !== start );

		return leftmost;

	}

	// check if a point lies within a convex triangle
	function pointInTriangle( ax, ay, bx, by, cx, cy, px, py ) {

		return ( cx - px ) * ( ay - py ) - ( ax - px ) * ( cy - py ) >= 0 &&
				( ax - px ) * ( by - py ) - ( bx - px ) * ( ay - py ) >= 0 &&
				( bx - px ) * ( cy - py ) - ( cx - px ) * ( by - py ) >= 0;

	}

	// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
	function isValidDiagonal( a, b ) {

		return a.next.i !== b.i && a.prev.i !== b.i && ! intersectsPolygon( a, b ) && // dones't intersect other edges
			( locallyInside( a, b ) && locallyInside( b, a ) && middleInside( a, b ) && // locally visible
			( area( a.prev, a, b.prev ) || area( a, b.prev, b ) ) || // does not create opposite-facing sectors
			equals( a, b ) && area( a.prev, a, a.next ) > 0 && area( b.prev, b, b.next ) > 0 ); // special zero-length case

	}

	// signed area of a triangle
	function area( p, q, r ) {

		return ( q.y - p.y ) * ( r.x - q.x ) - ( q.x - p.x ) * ( r.y - q.y );

	}

	// check if two points are equal
	function equals( p1, p2 ) {

		return p1.x === p2.x && p1.y === p2.y;

	}

	// check if two segments intersect
	function intersects( p1, q1, p2, q2 ) {

		const o1 = sign( area( p1, q1, p2 ) );
		const o2 = sign( area( p1, q1, q2 ) );
		const o3 = sign( area( p2, q2, p1 ) );
		const o4 = sign( area( p2, q2, q1 ) );

		if ( o1 !== o2 && o3 !== o4 ) return true; // general case

		if ( o1 === 0 && onSegment( p1, p2, q1 ) ) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
		if ( o2 === 0 && onSegment( p1, q2, q1 ) ) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
		if ( o3 === 0 && onSegment( p2, p1, q2 ) ) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
		if ( o4 === 0 && onSegment( p2, q1, q2 ) ) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2

		return false;

	}

	// for collinear points p, q, r, check if point q lies on segment pr
	function onSegment( p, q, r ) {

		return q.x <= Math.max( p.x, r.x ) && q.x >= Math.min( p.x, r.x ) && q.y <= Math.max( p.y, r.y ) && q.y >= Math.min( p.y, r.y );

	}

	function sign( num ) {

		return num > 0 ? 1 : num < 0 ? - 1 : 0;

	}

	// check if a polygon diagonal intersects any polygon segments
	function intersectsPolygon( a, b ) {

		let p = a;
		do {

			if ( p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i &&
					intersects( p, p.next, a, b ) ) return true;
			p = p.next;

		} while ( p !== a );

		return false;

	}

	// check if a polygon diagonal is locally inside the polygon
	function locallyInside( a, b ) {

		return area( a.prev, a, a.next ) < 0 ?
			area( a, b, a.next ) >= 0 && area( a, a.prev, b ) >= 0 :
			area( a, b, a.prev ) < 0 || area( a, a.next, b ) < 0;

	}

	// check if the middle point of a polygon diagonal is inside the polygon
	function middleInside( a, b ) {

		let p = a,
			inside = false;
		const px = ( a.x + b.x ) / 2,
			py = ( a.y + b.y ) / 2;
		do {

			if ( ( ( p.y > py ) !== ( p.next.y > py ) ) && p.next.y !== p.y &&
					( px < ( p.next.x - p.x ) * ( py - p.y ) / ( p.next.y - p.y ) + p.x ) )
				inside = ! inside;
			p = p.next;

		} while ( p !== a );

		return inside;

	}

	// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
	// if one belongs to the outer ring and another to a hole, it merges it into a single ring
	function splitPolygon( a, b ) {

		const a2 = new Node( a.i, a.x, a.y ),
			b2 = new Node( b.i, b.x, b.y ),
			an = a.next,
			bp = b.prev;

		a.next = b;
		b.prev = a;

		a2.next = an;
		an.prev = a2;

		b2.next = a2;
		a2.prev = b2;

		bp.next = b2;
		b2.prev = bp;

		return b2;

	}

	// create a node and optionally link it with previous one (in a circular doubly linked list)
	function insertNode( i, x, y, last ) {

		const p = new Node( i, x, y );

		if ( ! last ) {

			p.prev = p;
			p.next = p;

		} else {

			p.next = last.next;
			p.prev = last;
			last.next.prev = p;
			last.next = p;

		}

		return p;

	}

	function removeNode( p ) {

		p.next.prev = p.prev;
		p.prev.next = p.next;

		if ( p.prevZ ) p.prevZ.nextZ = p.nextZ;
		if ( p.nextZ ) p.nextZ.prevZ = p.prevZ;

	}

	function Node( i, x, y ) {

		// vertex index in coordinates array
		this.i = i;

		// vertex coordinates
		this.x = x;
		this.y = y;

		// previous and next vertex nodes in a polygon ring
		this.prev = null;
		this.next = null;

		// z-order curve value
		this.z = null;

		// previous and next nodes in z-order
		this.prevZ = null;
		this.nextZ = null;

		// indicates whether this is a steiner point
		this.steiner = false;

	}

	function signedArea( data, start, end, dim ) {

		let sum = 0;
		for ( let i = start, j = end - dim; i < end; i += dim ) {

			sum += ( data[ j ] - data[ i ] ) * ( data[ i + 1 ] + data[ j + 1 ] );
			j = i;

		}

		return sum;

	}

	const ShapeUtils = {

		// calculate area of the contour polygon

		area: function ( contour ) {

			const n = contour.length;
			let a = 0.0;

			for ( let p = n - 1, q = 0; q < n; p = q ++ ) {

				a += contour[ p ].x * contour[ q ].y - contour[ q ].x * contour[ p ].y;

			}

			return a * 0.5;

		},

		isClockWise: function ( pts ) {

			return ShapeUtils.area( pts ) < 0;

		},

		triangulateShape: function ( contour, holes ) {

			const vertices = []; // flat array of vertices like [ x0,y0, x1,y1, x2,y2, ... ]
			const holeIndices = []; // array of hole indices
			const faces = []; // final array of vertex indices like [ [ a,b,d ], [ b,c,d ] ]

			removeDupEndPts( contour );
			addContour( vertices, contour );

			//

			let holeIndex = contour.length;

			holes.forEach( removeDupEndPts );

			for ( let i = 0; i < holes.length; i ++ ) {

				holeIndices.push( holeIndex );
				holeIndex += holes[ i ].length;
				addContour( vertices, holes[ i ] );

			}

			//

			const triangles = Earcut.triangulate( vertices, holeIndices );

			//

			for ( let i = 0; i < triangles.length; i += 3 ) {

				faces.push( triangles.slice( i, i + 3 ) );

			}

			return faces;

		}

	};

	function removeDupEndPts( points ) {

		const l = points.length;

		if ( l > 2 && points[ l - 1 ].equals( points[ 0 ] ) ) {

			points.pop();

		}

	}

	function addContour( vertices, contour ) {

		for ( let i = 0; i < contour.length; i ++ ) {

			vertices.push( contour[ i ].x );
			vertices.push( contour[ i ].y );

		}

	}

	/**
	 * Creates extruded geometry from a path shape.
	 *
	 * parameters = {
	 *
	 *  curveSegments: <int>, // number of points on the curves
	 *  steps: <int>, // number of points for z-side extrusions / used for subdividing segments of extrude spline too
	 *  depth: <float>, // Depth to extrude the shape
	 *
	 *  bevelEnabled: <bool>, // turn on bevel
	 *  bevelThickness: <float>, // how deep into the original shape bevel goes
	 *  bevelSize: <float>, // how far from shape outline (including bevelOffset) is bevel
	 *  bevelOffset: <float>, // how far from shape outline does bevel start
	 *  bevelSegments: <int>, // number of bevel layers
	 *
	 *  extrudePath: <THREE.Curve> // curve to extrude shape along
	 *
	 *  UVGenerator: <Object> // object that provides UV generator functions
	 *
	 * }
	 */

	class ExtrudeBufferGeometry extends BufferGeometry {

		constructor( shapes, options ) {

			super();

			this.type = 'ExtrudeBufferGeometry';

			this.parameters = {
				shapes: shapes,
				options: options
			};

			shapes = Array.isArray( shapes ) ? shapes : [ shapes ];

			const scope = this;

			const verticesArray = [];
			const uvArray = [];

			for ( let i = 0, l = shapes.length; i < l; i ++ ) {

				const shape = shapes[ i ];
				addShape( shape );

			}

			// build geometry

			this.setAttribute( 'position', new Float32BufferAttribute( verticesArray, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvArray, 2 ) );

			this.computeVertexNormals();

			// functions

			function addShape( shape ) {

				const placeholder = [];

				// options

				const curveSegments = options.curveSegments !== undefined ? options.curveSegments : 12;
				const steps = options.steps !== undefined ? options.steps : 1;
				let depth = options.depth !== undefined ? options.depth : 100;

				let bevelEnabled = options.bevelEnabled !== undefined ? options.bevelEnabled : true;
				let bevelThickness = options.bevelThickness !== undefined ? options.bevelThickness : 6;
				let bevelSize = options.bevelSize !== undefined ? options.bevelSize : bevelThickness - 2;
				let bevelOffset = options.bevelOffset !== undefined ? options.bevelOffset : 0;
				let bevelSegments = options.bevelSegments !== undefined ? options.bevelSegments : 3;

				const extrudePath = options.extrudePath;

				const uvgen = options.UVGenerator !== undefined ? options.UVGenerator : WorldUVGenerator;

				// deprecated options

				if ( options.amount !== undefined ) {

					console.warn( 'THREE.ExtrudeBufferGeometry: amount has been renamed to depth.' );
					depth = options.amount;

				}

				//

				let extrudePts, extrudeByPath = false;
				let splineTube, binormal, normal, position2;

				if ( extrudePath ) {

					extrudePts = extrudePath.getSpacedPoints( steps );

					extrudeByPath = true;
					bevelEnabled = false; // bevels not supported for path extrusion

					// SETUP TNB variables

					// TODO1 - have a .isClosed in spline?

					splineTube = extrudePath.computeFrenetFrames( steps, false );

					// console.log(splineTube, 'splineTube', splineTube.normals.length, 'steps', steps, 'extrudePts', extrudePts.length);

					binormal = new Vector3();
					normal = new Vector3();
					position2 = new Vector3();

				}

				// Safeguards if bevels are not enabled

				if ( ! bevelEnabled ) {

					bevelSegments = 0;
					bevelThickness = 0;
					bevelSize = 0;
					bevelOffset = 0;

				}

				// Variables initialization

				const shapePoints = shape.extractPoints( curveSegments );

				let vertices = shapePoints.shape;
				const holes = shapePoints.holes;

				const reverse = ! ShapeUtils.isClockWise( vertices );

				if ( reverse ) {

					vertices = vertices.reverse();

					// Maybe we should also check if holes are in the opposite direction, just to be safe ...

					for ( let h = 0, hl = holes.length; h < hl; h ++ ) {

						const ahole = holes[ h ];

						if ( ShapeUtils.isClockWise( ahole ) ) {

							holes[ h ] = ahole.reverse();

						}

					}

				}


				const faces = ShapeUtils.triangulateShape( vertices, holes );

				/* Vertices */

				const contour = vertices; // vertices has all points but contour has only points of circumference

				for ( let h = 0, hl = holes.length; h < hl; h ++ ) {

					const ahole = holes[ h ];

					vertices = vertices.concat( ahole );

				}


				function scalePt2( pt, vec, size ) {

					if ( ! vec ) console.error( 'THREE.ExtrudeGeometry: vec does not exist' );

					return vec.clone().multiplyScalar( size ).add( pt );

				}

				const vlen = vertices.length, flen = faces.length;


				// Find directions for point movement


				function getBevelVec( inPt, inPrev, inNext ) {

					// computes for inPt the corresponding point inPt' on a new contour
					//   shifted by 1 unit (length of normalized vector) to the left
					// if we walk along contour clockwise, this new contour is outside the old one
					//
					// inPt' is the intersection of the two lines parallel to the two
					//  adjacent edges of inPt at a distance of 1 unit on the left side.

					let v_trans_x, v_trans_y, shrink_by; // resulting translation vector for inPt

					// good reading for geometry algorithms (here: line-line intersection)
					// http://geomalgorithms.com/a05-_intersect-1.html

					const v_prev_x = inPt.x - inPrev.x,
						v_prev_y = inPt.y - inPrev.y;
					const v_next_x = inNext.x - inPt.x,
						v_next_y = inNext.y - inPt.y;

					const v_prev_lensq = ( v_prev_x * v_prev_x + v_prev_y * v_prev_y );

					// check for collinear edges
					const collinear0 = ( v_prev_x * v_next_y - v_prev_y * v_next_x );

					if ( Math.abs( collinear0 ) > Number.EPSILON ) {

						// not collinear

						// length of vectors for normalizing

						const v_prev_len = Math.sqrt( v_prev_lensq );
						const v_next_len = Math.sqrt( v_next_x * v_next_x + v_next_y * v_next_y );

						// shift adjacent points by unit vectors to the left

						const ptPrevShift_x = ( inPrev.x - v_prev_y / v_prev_len );
						const ptPrevShift_y = ( inPrev.y + v_prev_x / v_prev_len );

						const ptNextShift_x = ( inNext.x - v_next_y / v_next_len );
						const ptNextShift_y = ( inNext.y + v_next_x / v_next_len );

						// scaling factor for v_prev to intersection point

						const sf = ( ( ptNextShift_x - ptPrevShift_x ) * v_next_y -
								( ptNextShift_y - ptPrevShift_y ) * v_next_x ) /
							( v_prev_x * v_next_y - v_prev_y * v_next_x );

						// vector from inPt to intersection point

						v_trans_x = ( ptPrevShift_x + v_prev_x * sf - inPt.x );
						v_trans_y = ( ptPrevShift_y + v_prev_y * sf - inPt.y );

						// Don't normalize!, otherwise sharp corners become ugly
						//  but prevent crazy spikes
						const v_trans_lensq = ( v_trans_x * v_trans_x + v_trans_y * v_trans_y );
						if ( v_trans_lensq <= 2 ) {

							return new Vector2( v_trans_x, v_trans_y );

						} else {

							shrink_by = Math.sqrt( v_trans_lensq / 2 );

						}

					} else {

						// handle special case of collinear edges

						let direction_eq = false; // assumes: opposite

						if ( v_prev_x > Number.EPSILON ) {

							if ( v_next_x > Number.EPSILON ) {

								direction_eq = true;

							}

						} else {

							if ( v_prev_x < - Number.EPSILON ) {

								if ( v_next_x < - Number.EPSILON ) {

									direction_eq = true;

								}

							} else {

								if ( Math.sign( v_prev_y ) === Math.sign( v_next_y ) ) {

									direction_eq = true;

								}

							}

						}

						if ( direction_eq ) {

							// console.log("Warning: lines are a straight sequence");
							v_trans_x = - v_prev_y;
							v_trans_y = v_prev_x;
							shrink_by = Math.sqrt( v_prev_lensq );

						} else {

							// console.log("Warning: lines are a straight spike");
							v_trans_x = v_prev_x;
							v_trans_y = v_prev_y;
							shrink_by = Math.sqrt( v_prev_lensq / 2 );

						}

					}

					return new Vector2( v_trans_x / shrink_by, v_trans_y / shrink_by );

				}


				const contourMovements = [];

				for ( let i = 0, il = contour.length, j = il - 1, k = i + 1; i < il; i ++, j ++, k ++ ) {

					if ( j === il ) j = 0;
					if ( k === il ) k = 0;

					//  (j)---(i)---(k)
					// console.log('i,j,k', i, j , k)

					contourMovements[ i ] = getBevelVec( contour[ i ], contour[ j ], contour[ k ] );

				}

				const holesMovements = [];
				let oneHoleMovements, verticesMovements = contourMovements.concat();

				for ( let h = 0, hl = holes.length; h < hl; h ++ ) {

					const ahole = holes[ h ];

					oneHoleMovements = [];

					for ( let i = 0, il = ahole.length, j = il - 1, k = i + 1; i < il; i ++, j ++, k ++ ) {

						if ( j === il ) j = 0;
						if ( k === il ) k = 0;

						//  (j)---(i)---(k)
						oneHoleMovements[ i ] = getBevelVec( ahole[ i ], ahole[ j ], ahole[ k ] );

					}

					holesMovements.push( oneHoleMovements );
					verticesMovements = verticesMovements.concat( oneHoleMovements );

				}


				// Loop bevelSegments, 1 for the front, 1 for the back

				for ( let b = 0; b < bevelSegments; b ++ ) {

					//for ( b = bevelSegments; b > 0; b -- ) {

					const t = b / bevelSegments;
					const z = bevelThickness * Math.cos( t * Math.PI / 2 );
					const bs = bevelSize * Math.sin( t * Math.PI / 2 ) + bevelOffset;

					// contract shape

					for ( let i = 0, il = contour.length; i < il; i ++ ) {

						const vert = scalePt2( contour[ i ], contourMovements[ i ], bs );

						v( vert.x, vert.y, - z );

					}

					// expand holes

					for ( let h = 0, hl = holes.length; h < hl; h ++ ) {

						const ahole = holes[ h ];
						oneHoleMovements = holesMovements[ h ];

						for ( let i = 0, il = ahole.length; i < il; i ++ ) {

							const vert = scalePt2( ahole[ i ], oneHoleMovements[ i ], bs );

							v( vert.x, vert.y, - z );

						}

					}

				}

				const bs = bevelSize + bevelOffset;

				// Back facing vertices

				for ( let i = 0; i < vlen; i ++ ) {

					const vert = bevelEnabled ? scalePt2( vertices[ i ], verticesMovements[ i ], bs ) : vertices[ i ];

					if ( ! extrudeByPath ) {

						v( vert.x, vert.y, 0 );

					} else {

						// v( vert.x, vert.y + extrudePts[ 0 ].y, extrudePts[ 0 ].x );

						normal.copy( splineTube.normals[ 0 ] ).multiplyScalar( vert.x );
						binormal.copy( splineTube.binormals[ 0 ] ).multiplyScalar( vert.y );

						position2.copy( extrudePts[ 0 ] ).add( normal ).add( binormal );

						v( position2.x, position2.y, position2.z );

					}

				}

				// Add stepped vertices...
				// Including front facing vertices

				for ( let s = 1; s <= steps; s ++ ) {

					for ( let i = 0; i < vlen; i ++ ) {

						const vert = bevelEnabled ? scalePt2( vertices[ i ], verticesMovements[ i ], bs ) : vertices[ i ];

						if ( ! extrudeByPath ) {

							v( vert.x, vert.y, depth / steps * s );

						} else {

							// v( vert.x, vert.y + extrudePts[ s - 1 ].y, extrudePts[ s - 1 ].x );

							normal.copy( splineTube.normals[ s ] ).multiplyScalar( vert.x );
							binormal.copy( splineTube.binormals[ s ] ).multiplyScalar( vert.y );

							position2.copy( extrudePts[ s ] ).add( normal ).add( binormal );

							v( position2.x, position2.y, position2.z );

						}

					}

				}


				// Add bevel segments planes

				//for ( b = 1; b <= bevelSegments; b ++ ) {
				for ( let b = bevelSegments - 1; b >= 0; b -- ) {

					const t = b / bevelSegments;
					const z = bevelThickness * Math.cos( t * Math.PI / 2 );
					const bs = bevelSize * Math.sin( t * Math.PI / 2 ) + bevelOffset;

					// contract shape

					for ( let i = 0, il = contour.length; i < il; i ++ ) {

						const vert = scalePt2( contour[ i ], contourMovements[ i ], bs );
						v( vert.x, vert.y, depth + z );

					}

					// expand holes

					for ( let h = 0, hl = holes.length; h < hl; h ++ ) {

						const ahole = holes[ h ];
						oneHoleMovements = holesMovements[ h ];

						for ( let i = 0, il = ahole.length; i < il; i ++ ) {

							const vert = scalePt2( ahole[ i ], oneHoleMovements[ i ], bs );

							if ( ! extrudeByPath ) {

								v( vert.x, vert.y, depth + z );

							} else {

								v( vert.x, vert.y + extrudePts[ steps - 1 ].y, extrudePts[ steps - 1 ].x + z );

							}

						}

					}

				}

				/* Faces */

				// Top and bottom faces

				buildLidFaces();

				// Sides faces

				buildSideFaces();


				/////  Internal functions

				function buildLidFaces() {

					const start = verticesArray.length / 3;

					if ( bevelEnabled ) {

						let layer = 0; // steps + 1
						let offset = vlen * layer;

						// Bottom faces

						for ( let i = 0; i < flen; i ++ ) {

							const face = faces[ i ];
							f3( face[ 2 ] + offset, face[ 1 ] + offset, face[ 0 ] + offset );

						}

						layer = steps + bevelSegments * 2;
						offset = vlen * layer;

						// Top faces

						for ( let i = 0; i < flen; i ++ ) {

							const face = faces[ i ];
							f3( face[ 0 ] + offset, face[ 1 ] + offset, face[ 2 ] + offset );

						}

					} else {

						// Bottom faces

						for ( let i = 0; i < flen; i ++ ) {

							const face = faces[ i ];
							f3( face[ 2 ], face[ 1 ], face[ 0 ] );

						}

						// Top faces

						for ( let i = 0; i < flen; i ++ ) {

							const face = faces[ i ];
							f3( face[ 0 ] + vlen * steps, face[ 1 ] + vlen * steps, face[ 2 ] + vlen * steps );

						}

					}

					scope.addGroup( start, verticesArray.length / 3 - start, 0 );

				}

				// Create faces for the z-sides of the shape

				function buildSideFaces() {

					const start = verticesArray.length / 3;
					let layeroffset = 0;
					sidewalls( contour, layeroffset );
					layeroffset += contour.length;

					for ( let h = 0, hl = holes.length; h < hl; h ++ ) {

						const ahole = holes[ h ];
						sidewalls( ahole, layeroffset );

						//, true
						layeroffset += ahole.length;

					}


					scope.addGroup( start, verticesArray.length / 3 - start, 1 );


				}

				function sidewalls( contour, layeroffset ) {

					let i = contour.length;

					while ( -- i >= 0 ) {

						const j = i;
						let k = i - 1;
						if ( k < 0 ) k = contour.length - 1;

						//console.log('b', i,j, i-1, k,vertices.length);

						for ( let s = 0, sl = ( steps + bevelSegments * 2 ); s < sl; s ++ ) {

							const slen1 = vlen * s;
							const slen2 = vlen * ( s + 1 );

							const a = layeroffset + j + slen1,
								b = layeroffset + k + slen1,
								c = layeroffset + k + slen2,
								d = layeroffset + j + slen2;

							f4( a, b, c, d );

						}

					}

				}

				function v( x, y, z ) {

					placeholder.push( x );
					placeholder.push( y );
					placeholder.push( z );

				}


				function f3( a, b, c ) {

					addVertex( a );
					addVertex( b );
					addVertex( c );

					const nextIndex = verticesArray.length / 3;
					const uvs = uvgen.generateTopUV( scope, verticesArray, nextIndex - 3, nextIndex - 2, nextIndex - 1 );

					addUV( uvs[ 0 ] );
					addUV( uvs[ 1 ] );
					addUV( uvs[ 2 ] );

				}

				function f4( a, b, c, d ) {

					addVertex( a );
					addVertex( b );
					addVertex( d );

					addVertex( b );
					addVertex( c );
					addVertex( d );


					const nextIndex = verticesArray.length / 3;
					const uvs = uvgen.generateSideWallUV( scope, verticesArray, nextIndex - 6, nextIndex - 3, nextIndex - 2, nextIndex - 1 );

					addUV( uvs[ 0 ] );
					addUV( uvs[ 1 ] );
					addUV( uvs[ 3 ] );

					addUV( uvs[ 1 ] );
					addUV( uvs[ 2 ] );
					addUV( uvs[ 3 ] );

				}

				function addVertex( index ) {

					verticesArray.push( placeholder[ index * 3 + 0 ] );
					verticesArray.push( placeholder[ index * 3 + 1 ] );
					verticesArray.push( placeholder[ index * 3 + 2 ] );

				}


				function addUV( vector2 ) {

					uvArray.push( vector2.x );
					uvArray.push( vector2.y );

				}

			}

		}

		toJSON() {

			const data = BufferGeometry.prototype.toJSON.call( this );

			const shapes = this.parameters.shapes;
			const options = this.parameters.options;

			return toJSON( shapes, options, data );

		}

	}

	const WorldUVGenerator = {

		generateTopUV: function ( geometry, vertices, indexA, indexB, indexC ) {

			const a_x = vertices[ indexA * 3 ];
			const a_y = vertices[ indexA * 3 + 1 ];
			const b_x = vertices[ indexB * 3 ];
			const b_y = vertices[ indexB * 3 + 1 ];
			const c_x = vertices[ indexC * 3 ];
			const c_y = vertices[ indexC * 3 + 1 ];

			return [
				new Vector2( a_x, a_y ),
				new Vector2( b_x, b_y ),
				new Vector2( c_x, c_y )
			];

		},

		generateSideWallUV: function ( geometry, vertices, indexA, indexB, indexC, indexD ) {

			const a_x = vertices[ indexA * 3 ];
			const a_y = vertices[ indexA * 3 + 1 ];
			const a_z = vertices[ indexA * 3 + 2 ];
			const b_x = vertices[ indexB * 3 ];
			const b_y = vertices[ indexB * 3 + 1 ];
			const b_z = vertices[ indexB * 3 + 2 ];
			const c_x = vertices[ indexC * 3 ];
			const c_y = vertices[ indexC * 3 + 1 ];
			const c_z = vertices[ indexC * 3 + 2 ];
			const d_x = vertices[ indexD * 3 ];
			const d_y = vertices[ indexD * 3 + 1 ];
			const d_z = vertices[ indexD * 3 + 2 ];

			if ( Math.abs( a_y - b_y ) < 0.01 ) {

				return [
					new Vector2( a_x, 1 - a_z ),
					new Vector2( b_x, 1 - b_z ),
					new Vector2( c_x, 1 - c_z ),
					new Vector2( d_x, 1 - d_z )
				];

			} else {

				return [
					new Vector2( a_y, 1 - a_z ),
					new Vector2( b_y, 1 - b_z ),
					new Vector2( c_y, 1 - c_z ),
					new Vector2( d_y, 1 - d_z )
				];

			}

		}

	};

	function toJSON( shapes, options, data ) {

		data.shapes = [];

		if ( Array.isArray( shapes ) ) {

			for ( let i = 0, l = shapes.length; i < l; i ++ ) {

				const shape = shapes[ i ];

				data.shapes.push( shape.uuid );

			}

		} else {

			data.shapes.push( shapes.uuid );

		}

		if ( options.extrudePath !== undefined ) data.options.extrudePath = options.extrudePath.toJSON();

		return data;

	}

	/**
	 * Creates extruded geometry from a path shape.
	 *
	 * parameters = {
	 *
	 *  curveSegments: <int>, // number of points on the curves
	 *  steps: <int>, // number of points for z-side extrusions / used for subdividing segments of extrude spline too
	 *  depth: <float>, // Depth to extrude the shape
	 *
	 *  bevelEnabled: <bool>, // turn on bevel
	 *  bevelThickness: <float>, // how deep into the original shape bevel goes
	 *  bevelSize: <float>, // how far from shape outline (including bevelOffset) is bevel
	 *  bevelOffset: <float>, // how far from shape outline does bevel start
	 *  bevelSegments: <int>, // number of bevel layers
	 *
	 *  extrudePath: <THREE.Curve> // curve to extrude shape along
	 *
	 *  UVGenerator: <Object> // object that provides UV generator functions
	 *
	 * }
	 */

	class ExtrudeGeometry extends Geometry {

		constructor( shapes, options ) {

			super();

			this.type = 'ExtrudeGeometry';

			this.parameters = {
				shapes: shapes,
				options: options
			};

			this.fromBufferGeometry( new ExtrudeBufferGeometry( shapes, options ) );
			this.mergeVertices();

		}

		toJSON() {

			const data = super.toJSON();

			const shapes = this.parameters.shapes;
			const options = this.parameters.options;

			return toJSON$1( shapes, options, data );

		}

	}

	function toJSON$1( shapes, options, data ) {

		data.shapes = [];

		if ( Array.isArray( shapes ) ) {

			for ( let i = 0, l = shapes.length; i < l; i ++ ) {

				const shape = shapes[ i ];

				data.shapes.push( shape.uuid );

			}

		} else {

			data.shapes.push( shapes.uuid );

		}

		if ( options.extrudePath !== undefined ) data.options.extrudePath = options.extrudePath.toJSON();

		return data;

	}

	class IcosahedronBufferGeometry extends PolyhedronBufferGeometry {

		constructor( radius = 1, detail = 0 ) {

			const t = ( 1 + Math.sqrt( 5 ) ) / 2;

			const vertices = [
				- 1, t, 0, 	1, t, 0, 	- 1, - t, 0, 	1, - t, 0,
				0, - 1, t, 	0, 1, t,	0, - 1, - t, 	0, 1, - t,
				t, 0, - 1, 	t, 0, 1, 	- t, 0, - 1, 	- t, 0, 1
			];

			const indices = [
				0, 11, 5, 	0, 5, 1, 	0, 1, 7, 	0, 7, 10, 	0, 10, 11,
				1, 5, 9, 	5, 11, 4,	11, 10, 2,	10, 7, 6,	7, 1, 8,
				3, 9, 4, 	3, 4, 2,	3, 2, 6,	3, 6, 8,	3, 8, 9,
				4, 9, 5, 	2, 4, 11,	6, 2, 10,	8, 6, 7,	9, 8, 1
			];

			super( vertices, indices, radius, detail );

			this.type = 'IcosahedronBufferGeometry';

			this.parameters = {
				radius: radius,
				detail: detail
			};

		}

	}

	class IcosahedronGeometry extends Geometry {

		constructor( radius, detail ) {

			super();

			this.type = 'IcosahedronGeometry';

			this.parameters = {
				radius: radius,
				detail: detail
			};

			this.fromBufferGeometry( new IcosahedronBufferGeometry( radius, detail ) );
			this.mergeVertices();

		}

	}

	class LatheBufferGeometry extends BufferGeometry {

		constructor( points, segments = 12, phiStart = 0, phiLength = Math.PI * 2 ) {

			super();

			this.type = 'LatheBufferGeometry';

			this.parameters = {
				points: points,
				segments: segments,
				phiStart: phiStart,
				phiLength: phiLength
			};

			segments = Math.floor( segments );

			// clamp phiLength so it's in range of [ 0, 2PI ]

			phiLength = MathUtils.clamp( phiLength, 0, Math.PI * 2 );

			// buffers

			const indices = [];
			const vertices = [];
			const uvs = [];

			// helper variables

			const inverseSegments = 1.0 / segments;
			const vertex = new Vector3();
			const uv = new Vector2();

			// generate vertices and uvs

			for ( let i = 0; i <= segments; i ++ ) {

				const phi = phiStart + i * inverseSegments * phiLength;

				const sin = Math.sin( phi );
				const cos = Math.cos( phi );

				for ( let j = 0; j <= ( points.length - 1 ); j ++ ) {

					// vertex

					vertex.x = points[ j ].x * sin;
					vertex.y = points[ j ].y;
					vertex.z = points[ j ].x * cos;

					vertices.push( vertex.x, vertex.y, vertex.z );

					// uv

					uv.x = i / segments;
					uv.y = j / ( points.length - 1 );

					uvs.push( uv.x, uv.y );


				}

			}

			// indices

			for ( let i = 0; i < segments; i ++ ) {

				for ( let j = 0; j < ( points.length - 1 ); j ++ ) {

					const base = j + i * points.length;

					const a = base;
					const b = base + points.length;
					const c = base + points.length + 1;
					const d = base + 1;

					// faces

					indices.push( a, b, d );
					indices.push( b, c, d );

				}

			}

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

			// generate normals

			this.computeVertexNormals();

			// if the geometry is closed, we need to average the normals along the seam.
			// because the corresponding vertices are identical (but still have different UVs).

			if ( phiLength === Math.PI * 2 ) {

				const normals = this.attributes.normal.array;
				const n1 = new Vector3();
				const n2 = new Vector3();
				const n = new Vector3();

				// this is the buffer offset for the last line of vertices

				const base = segments * points.length * 3;

				for ( let i = 0, j = 0; i < points.length; i ++, j += 3 ) {

					// select the normal of the vertex in the first line

					n1.x = normals[ j + 0 ];
					n1.y = normals[ j + 1 ];
					n1.z = normals[ j + 2 ];

					// select the normal of the vertex in the last line

					n2.x = normals[ base + j + 0 ];
					n2.y = normals[ base + j + 1 ];
					n2.z = normals[ base + j + 2 ];

					// average normals

					n.addVectors( n1, n2 ).normalize();

					// assign the new values to both normals

					normals[ j + 0 ] = normals[ base + j + 0 ] = n.x;
					normals[ j + 1 ] = normals[ base + j + 1 ] = n.y;
					normals[ j + 2 ] = normals[ base + j + 2 ] = n.z;

				}

			}

		}

	}

	class LatheGeometry extends Geometry {

		constructor( points, segments, phiStart, phiLength ) {

			super();

			this.type = 'LatheGeometry';

			this.parameters = {
				points: points,
				segments: segments,
				phiStart: phiStart,
				phiLength: phiLength
			};

			this.fromBufferGeometry( new LatheBufferGeometry( points, segments, phiStart, phiLength ) );
			this.mergeVertices();

		}

	}

	class OctahedronBufferGeometry extends PolyhedronBufferGeometry {

		constructor( radius = 1, detail = 0 ) {

			const vertices = [
				1, 0, 0, 	- 1, 0, 0,	0, 1, 0,
				0, - 1, 0, 	0, 0, 1,	0, 0, - 1
			];

			const indices = [
				0, 2, 4,	0, 4, 3,	0, 3, 5,
				0, 5, 2,	1, 2, 5,	1, 5, 3,
				1, 3, 4,	1, 4, 2
			];

			super( vertices, indices, radius, detail );

			this.type = 'OctahedronBufferGeometry';

			this.parameters = {
				radius: radius,
				detail: detail
			};

		}

	}

	class OctahedronGeometry extends Geometry {

		constructor( radius, detail ) {

			super();

			this.type = 'OctahedronGeometry';

			this.parameters = {
				radius: radius,
				detail: detail
			};

			this.fromBufferGeometry( new OctahedronBufferGeometry( radius, detail ) );
			this.mergeVertices();

		}

	}

	/**
	 * Parametric Surfaces Geometry
	 * based on the brilliant article by @prideout https://prideout.net/blog/old/blog/index.html@p=44.html
	 */

	function ParametricBufferGeometry( func, slices, stacks ) {

		BufferGeometry.call( this );

		this.type = 'ParametricBufferGeometry';

		this.parameters = {
			func: func,
			slices: slices,
			stacks: stacks
		};

		// buffers

		const indices = [];
		const vertices = [];
		const normals = [];
		const uvs = [];

		const EPS = 0.00001;

		const normal = new Vector3();

		const p0 = new Vector3(), p1 = new Vector3();
		const pu = new Vector3(), pv = new Vector3();

		if ( func.length < 3 ) {

			console.error( 'THREE.ParametricGeometry: Function must now modify a Vector3 as third parameter.' );

		}

		// generate vertices, normals and uvs

		const sliceCount = slices + 1;

		for ( let i = 0; i <= stacks; i ++ ) {

			const v = i / stacks;

			for ( let j = 0; j <= slices; j ++ ) {

				const u = j / slices;

				// vertex

				func( u, v, p0 );
				vertices.push( p0.x, p0.y, p0.z );

				// normal

				// approximate tangent vectors via finite differences

				if ( u - EPS >= 0 ) {

					func( u - EPS, v, p1 );
					pu.subVectors( p0, p1 );

				} else {

					func( u + EPS, v, p1 );
					pu.subVectors( p1, p0 );

				}

				if ( v - EPS >= 0 ) {

					func( u, v - EPS, p1 );
					pv.subVectors( p0, p1 );

				} else {

					func( u, v + EPS, p1 );
					pv.subVectors( p1, p0 );

				}

				// cross product of tangent vectors returns surface normal

				normal.crossVectors( pu, pv ).normalize();
				normals.push( normal.x, normal.y, normal.z );

				// uv

				uvs.push( u, v );

			}

		}

		// generate indices

		for ( let i = 0; i < stacks; i ++ ) {

			for ( let j = 0; j < slices; j ++ ) {

				const a = i * sliceCount + j;
				const b = i * sliceCount + j + 1;
				const c = ( i + 1 ) * sliceCount + j + 1;
				const d = ( i + 1 ) * sliceCount + j;

				// faces one and two

				indices.push( a, b, d );
				indices.push( b, c, d );

			}

		}

		// build geometry

		this.setIndex( indices );
		this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
		this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

	}

	ParametricBufferGeometry.prototype = Object.create( BufferGeometry.prototype );
	ParametricBufferGeometry.prototype.constructor = ParametricBufferGeometry;

	/**
	 * Parametric Surfaces Geometry
	 * based on the brilliant article by @prideout https://prideout.net/blog/old/blog/index.html@p=44.html
	 */

	function ParametricGeometry( func, slices, stacks ) {

		Geometry.call( this );

		this.type = 'ParametricGeometry';

		this.parameters = {
			func: func,
			slices: slices,
			stacks: stacks
		};

		this.fromBufferGeometry( new ParametricBufferGeometry( func, slices, stacks ) );
		this.mergeVertices();

	}

	ParametricGeometry.prototype = Object.create( Geometry.prototype );
	ParametricGeometry.prototype.constructor = ParametricGeometry;

	class PlaneGeometry extends Geometry {

		constructor( width, height, widthSegments, heightSegments ) {

			super();

			this.type = 'PlaneGeometry';

			this.parameters = {
				width: width,
				height: height,
				widthSegments: widthSegments,
				heightSegments: heightSegments
			};

			this.fromBufferGeometry( new PlaneBufferGeometry( width, height, widthSegments, heightSegments ) );
			this.mergeVertices();

		}

	}

	class PolyhedronGeometry extends Geometry {

		constructor( vertices, indices, radius, detail ) {

			super();

			this.type = 'PolyhedronGeometry';

			this.parameters = {
				vertices: vertices,
				indices: indices,
				radius: radius,
				detail: detail
			};

			this.fromBufferGeometry( new PolyhedronBufferGeometry( vertices, indices, radius, detail ) );
			this.mergeVertices();

		}

	}

	class RingBufferGeometry extends BufferGeometry {

		constructor( innerRadius = 0.5, outerRadius = 1, thetaSegments = 8, phiSegments = 1, thetaStart = 0, thetaLength = Math.PI * 2 ) {

			super();

			this.type = 'RingBufferGeometry';

			this.parameters = {
				innerRadius: innerRadius,
				outerRadius: outerRadius,
				thetaSegments: thetaSegments,
				phiSegments: phiSegments,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

			thetaSegments = Math.max( 3, thetaSegments );
			phiSegments = Math.max( 1, phiSegments );

			// buffers

			const indices = [];
			const vertices = [];
			const normals = [];
			const uvs = [];

			// some helper variables

			let radius = innerRadius;
			const radiusStep = ( ( outerRadius - innerRadius ) / phiSegments );
			const vertex = new Vector3();
			const uv = new Vector2();

			// generate vertices, normals and uvs

			for ( let j = 0; j <= phiSegments; j ++ ) {

				for ( let i = 0; i <= thetaSegments; i ++ ) {

					// values are generate from the inside of the ring to the outside

					const segment = thetaStart + i / thetaSegments * thetaLength;

					// vertex

					vertex.x = radius * Math.cos( segment );
					vertex.y = radius * Math.sin( segment );

					vertices.push( vertex.x, vertex.y, vertex.z );

					// normal

					normals.push( 0, 0, 1 );

					// uv

					uv.x = ( vertex.x / outerRadius + 1 ) / 2;
					uv.y = ( vertex.y / outerRadius + 1 ) / 2;

					uvs.push( uv.x, uv.y );

				}

				// increase the radius for next row of vertices

				radius += radiusStep;

			}

			// indices

			for ( let j = 0; j < phiSegments; j ++ ) {

				const thetaSegmentLevel = j * ( thetaSegments + 1 );

				for ( let i = 0; i < thetaSegments; i ++ ) {

					const segment = i + thetaSegmentLevel;

					const a = segment;
					const b = segment + thetaSegments + 1;
					const c = segment + thetaSegments + 2;
					const d = segment + 1;

					// faces

					indices.push( a, b, d );
					indices.push( b, c, d );

				}

			}

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

		}

	}

	class RingGeometry extends Geometry {

		constructor( innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength ) {

			super();

			this.type = 'RingGeometry';

			this.parameters = {
				innerRadius: innerRadius,
				outerRadius: outerRadius,
				thetaSegments: thetaSegments,
				phiSegments: phiSegments,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

			this.fromBufferGeometry( new RingBufferGeometry( innerRadius, outerRadius, thetaSegments, phiSegments, thetaStart, thetaLength ) );
			this.mergeVertices();

		}

	}

	class ShapeBufferGeometry extends BufferGeometry {

		constructor( shapes, curveSegments = 12 ) {

			super();
			this.type = 'ShapeBufferGeometry';

			this.parameters = {
				shapes: shapes,
				curveSegments: curveSegments
			};

			// buffers

			const indices = [];
			const vertices = [];
			const normals = [];
			const uvs = [];

			// helper variables

			let groupStart = 0;
			let groupCount = 0;

			// allow single and array values for "shapes" parameter

			if ( Array.isArray( shapes ) === false ) {

				addShape( shapes );

			} else {

				for ( let i = 0; i < shapes.length; i ++ ) {

					addShape( shapes[ i ] );

					this.addGroup( groupStart, groupCount, i ); // enables MultiMaterial support

					groupStart += groupCount;
					groupCount = 0;

				}

			}

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );


			// helper functions

			function addShape( shape ) {

				const indexOffset = vertices.length / 3;
				const points = shape.extractPoints( curveSegments );

				let shapeVertices = points.shape;
				const shapeHoles = points.holes;

				// check direction of vertices

				if ( ShapeUtils.isClockWise( shapeVertices ) === false ) {

					shapeVertices = shapeVertices.reverse();

				}

				for ( let i = 0, l = shapeHoles.length; i < l; i ++ ) {

					const shapeHole = shapeHoles[ i ];

					if ( ShapeUtils.isClockWise( shapeHole ) === true ) {

						shapeHoles[ i ] = shapeHole.reverse();

					}

				}

				const faces = ShapeUtils.triangulateShape( shapeVertices, shapeHoles );

				// join vertices of inner and outer paths to a single array

				for ( let i = 0, l = shapeHoles.length; i < l; i ++ ) {

					const shapeHole = shapeHoles[ i ];
					shapeVertices = shapeVertices.concat( shapeHole );

				}

				// vertices, normals, uvs

				for ( let i = 0, l = shapeVertices.length; i < l; i ++ ) {

					const vertex = shapeVertices[ i ];

					vertices.push( vertex.x, vertex.y, 0 );
					normals.push( 0, 0, 1 );
					uvs.push( vertex.x, vertex.y ); // world uvs

				}

				// incides

				for ( let i = 0, l = faces.length; i < l; i ++ ) {

					const face = faces[ i ];

					const a = face[ 0 ] + indexOffset;
					const b = face[ 1 ] + indexOffset;
					const c = face[ 2 ] + indexOffset;

					indices.push( a, b, c );
					groupCount += 3;

				}

			}

		}

		toJSON() {

			const data = BufferGeometry.prototype.toJSON.call( this );

			const shapes = this.parameters.shapes;

			return toJSON$2( shapes, data );

		}

	}

	function toJSON$2( shapes, data ) {

		data.shapes = [];

		if ( Array.isArray( shapes ) ) {

			for ( let i = 0, l = shapes.length; i < l; i ++ ) {

				const shape = shapes[ i ];

				data.shapes.push( shape.uuid );

			}

		} else {

			data.shapes.push( shapes.uuid );

		}

		return data;

	}

	class ShapeGeometry extends Geometry {

		constructor( shapes, curveSegments ) {

			super();
			this.type = 'ShapeGeometry';

			if ( typeof curveSegments === 'object' ) {

				console.warn( 'THREE.ShapeGeometry: Options parameter has been removed.' );

				curveSegments = curveSegments.curveSegments;

			}

			this.parameters = {
				shapes: shapes,
				curveSegments: curveSegments
			};

			this.fromBufferGeometry( new ShapeBufferGeometry( shapes, curveSegments ) );
			this.mergeVertices();

		}

		toJSON() {

			const data = Geometry.prototype.toJSON.call( this );

			const shapes = this.parameters.shapes;

			return toJSON$3( shapes, data );

		}

	}

	function toJSON$3( shapes, data ) {

		data.shapes = [];

		if ( Array.isArray( shapes ) ) {

			for ( let i = 0, l = shapes.length; i < l; i ++ ) {

				const shape = shapes[ i ];

				data.shapes.push( shape.uuid );

			}

		} else {

			data.shapes.push( shapes.uuid );

		}

		return data;

	}

	class SphereBufferGeometry extends BufferGeometry {

		constructor( radius = 1, widthSegments = 8, heightSegments = 6, phiStart = 0, phiLength = Math.PI * 2, thetaStart = 0, thetaLength = Math.PI ) {

			super();
			this.type = 'SphereBufferGeometry';

			this.parameters = {
				radius: radius,
				widthSegments: widthSegments,
				heightSegments: heightSegments,
				phiStart: phiStart,
				phiLength: phiLength,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

			widthSegments = Math.max( 3, Math.floor( widthSegments ) );
			heightSegments = Math.max( 2, Math.floor( heightSegments ) );

			const thetaEnd = Math.min( thetaStart + thetaLength, Math.PI );

			let index = 0;
			const grid = [];

			const vertex = new Vector3();
			const normal = new Vector3();

			// buffers

			const indices = [];
			const vertices = [];
			const normals = [];
			const uvs = [];

			// generate vertices, normals and uvs

			for ( let iy = 0; iy <= heightSegments; iy ++ ) {

				const verticesRow = [];

				const v = iy / heightSegments;

				// special case for the poles

				let uOffset = 0;

				if ( iy == 0 && thetaStart == 0 ) {

					uOffset = 0.5 / widthSegments;

				} else if ( iy == heightSegments && thetaEnd == Math.PI ) {

					uOffset = - 0.5 / widthSegments;

				}

				for ( let ix = 0; ix <= widthSegments; ix ++ ) {

					const u = ix / widthSegments;

					// vertex

					vertex.x = - radius * Math.cos( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );
					vertex.y = radius * Math.cos( thetaStart + v * thetaLength );
					vertex.z = radius * Math.sin( phiStart + u * phiLength ) * Math.sin( thetaStart + v * thetaLength );

					vertices.push( vertex.x, vertex.y, vertex.z );

					// normal

					normal.copy( vertex ).normalize();
					normals.push( normal.x, normal.y, normal.z );

					// uv

					uvs.push( u + uOffset, 1 - v );

					verticesRow.push( index ++ );

				}

				grid.push( verticesRow );

			}

			// indices

			for ( let iy = 0; iy < heightSegments; iy ++ ) {

				for ( let ix = 0; ix < widthSegments; ix ++ ) {

					const a = grid[ iy ][ ix + 1 ];
					const b = grid[ iy ][ ix ];
					const c = grid[ iy + 1 ][ ix ];
					const d = grid[ iy + 1 ][ ix + 1 ];

					if ( iy !== 0 || thetaStart > 0 ) indices.push( a, b, d );
					if ( iy !== heightSegments - 1 || thetaEnd < Math.PI ) indices.push( b, c, d );

				}

			}

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

		}

	}

	class SphereGeometry extends Geometry {

		constructor( radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength ) {

			super();
			this.type = 'SphereGeometry';

			this.parameters = {
				radius: radius,
				widthSegments: widthSegments,
				heightSegments: heightSegments,
				phiStart: phiStart,
				phiLength: phiLength,
				thetaStart: thetaStart,
				thetaLength: thetaLength
			};

			this.fromBufferGeometry( new SphereBufferGeometry( radius, widthSegments, heightSegments, phiStart, phiLength, thetaStart, thetaLength ) );
			this.mergeVertices();

		}

	}

	class TetrahedronBufferGeometry extends PolyhedronBufferGeometry {

		constructor( radius = 1, detail = 0 ) {

			const vertices = [
				1, 1, 1, 	- 1, - 1, 1, 	- 1, 1, - 1, 	1, - 1, - 1
			];

			const indices = [
				2, 1, 0, 	0, 3, 2,	1, 3, 0,	2, 3, 1
			];

			super( vertices, indices, radius, detail );

			this.type = 'TetrahedronBufferGeometry';

			this.parameters = {
				radius: radius,
				detail: detail
			};

		}

	}

	class TetrahedronGeometry extends Geometry {

		constructor( radius, detail ) {

			super();
			this.type = 'TetrahedronGeometry';

			this.parameters = {
				radius: radius,
				detail: detail
			};

			this.fromBufferGeometry( new TetrahedronBufferGeometry( radius, detail ) );
			this.mergeVertices();

		}

	}

	/**
	 * Text = 3D Text
	 *
	 * parameters = {
	 *  font: <THREE.Font>, // font
	 *
	 *  size: <float>, // size of the text
	 *  height: <float>, // thickness to extrude text
	 *  curveSegments: <int>, // number of points on the curves
	 *
	 *  bevelEnabled: <bool>, // turn on bevel
	 *  bevelThickness: <float>, // how deep into text bevel goes
	 *  bevelSize: <float>, // how far from text outline (including bevelOffset) is bevel
	 *  bevelOffset: <float> // how far from text outline does bevel start
	 * }
	 */

	class TextBufferGeometry extends ExtrudeBufferGeometry {

		constructor( text, parameters = {} ) {

			const font = parameters.font;

			if ( ! ( font && font.isFont ) ) {

				console.error( 'THREE.TextGeometry: font parameter is not an instance of THREE.Font.' );
				return new BufferGeometry();

			}

			const shapes = font.generateShapes( text, parameters.size );

			// translate parameters to ExtrudeGeometry API

			parameters.depth = parameters.height !== undefined ? parameters.height : 50;

			// defaults

			if ( parameters.bevelThickness === undefined ) parameters.bevelThickness = 10;
			if ( parameters.bevelSize === undefined ) parameters.bevelSize = 8;
			if ( parameters.bevelEnabled === undefined ) parameters.bevelEnabled = false;

			super( shapes, parameters );

			this.type = 'TextBufferGeometry';

		}

	}

	/**
	 * Text = 3D Text
	 *
	 * parameters = {
	 *  font: <THREE.Font>, // font
	 *
	 *  size: <float>, // size of the text
	 *  height: <float>, // thickness to extrude text
	 *  curveSegments: <int>, // number of points on the curves
	 *
	 *  bevelEnabled: <bool>, // turn on bevel
	 *  bevelThickness: <float>, // how deep into text bevel goes
	 *  bevelSize: <float>, // how far from text outline (including bevelOffset) is bevel
	 *  bevelOffset: <float> // how far from text outline does bevel start
	 * }
	 */

	class TextGeometry extends Geometry {

		constructor( text, parameters ) {

			super();
			this.type = 'TextGeometry';

			this.parameters = {
				text: text,
				parameters: parameters
			};

			this.fromBufferGeometry( new TextBufferGeometry( text, parameters ) );
			this.mergeVertices();

		}

	}

	class TorusBufferGeometry extends BufferGeometry {

		constructor( radius = 1, tube = 0.4, radialSegments = 8, tubularSegments = 6, arc = Math.PI * 2 ) {

			super();
			this.type = 'TorusBufferGeometry';

			this.parameters = {
				radius: radius,
				tube: tube,
				radialSegments: radialSegments,
				tubularSegments: tubularSegments,
				arc: arc
			};

			radialSegments = Math.floor( radialSegments );
			tubularSegments = Math.floor( tubularSegments );

			// buffers

			const indices = [];
			const vertices = [];
			const normals = [];
			const uvs = [];

			// helper variables

			const center = new Vector3();
			const vertex = new Vector3();
			const normal = new Vector3();

			// generate vertices, normals and uvs

			for ( let j = 0; j <= radialSegments; j ++ ) {

				for ( let i = 0; i <= tubularSegments; i ++ ) {

					const u = i / tubularSegments * arc;
					const v = j / radialSegments * Math.PI * 2;

					// vertex

					vertex.x = ( radius + tube * Math.cos( v ) ) * Math.cos( u );
					vertex.y = ( radius + tube * Math.cos( v ) ) * Math.sin( u );
					vertex.z = tube * Math.sin( v );

					vertices.push( vertex.x, vertex.y, vertex.z );

					// normal

					center.x = radius * Math.cos( u );
					center.y = radius * Math.sin( u );
					normal.subVectors( vertex, center ).normalize();

					normals.push( normal.x, normal.y, normal.z );

					// uv

					uvs.push( i / tubularSegments );
					uvs.push( j / radialSegments );

				}

			}

			// generate indices

			for ( let j = 1; j <= radialSegments; j ++ ) {

				for ( let i = 1; i <= tubularSegments; i ++ ) {

					// indices

					const a = ( tubularSegments + 1 ) * j + i - 1;
					const b = ( tubularSegments + 1 ) * ( j - 1 ) + i - 1;
					const c = ( tubularSegments + 1 ) * ( j - 1 ) + i;
					const d = ( tubularSegments + 1 ) * j + i;

					// faces

					indices.push( a, b, d );
					indices.push( b, c, d );

				}

			}

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

		}

	}

	class TorusGeometry extends Geometry {

		constructor( radius, tube, radialSegments, tubularSegments, arc ) {

			super();
			this.type = 'TorusGeometry';

			this.parameters = {
				radius: radius,
				tube: tube,
				radialSegments: radialSegments,
				tubularSegments: tubularSegments,
				arc: arc
			};

			this.fromBufferGeometry( new TorusBufferGeometry( radius, tube, radialSegments, tubularSegments, arc ) );
			this.mergeVertices();

		}

	}

	class TorusKnotBufferGeometry extends BufferGeometry {

		constructor( radius = 1, tube = 0.4, tubularSegments = 64, radialSegments = 8, p = 2, q = 3 ) {

			super();
			this.type = 'TorusKnotBufferGeometry';

			this.parameters = {
				radius: radius,
				tube: tube,
				tubularSegments: tubularSegments,
				radialSegments: radialSegments,
				p: p,
				q: q
			};

			tubularSegments = Math.floor( tubularSegments );
			radialSegments = Math.floor( radialSegments );

			// buffers

			const indices = [];
			const vertices = [];
			const normals = [];
			const uvs = [];

			// helper variables

			const vertex = new Vector3();
			const normal = new Vector3();

			const P1 = new Vector3();
			const P2 = new Vector3();

			const B = new Vector3();
			const T = new Vector3();
			const N = new Vector3();

			// generate vertices, normals and uvs

			for ( let i = 0; i <= tubularSegments; ++ i ) {

				// the radian "u" is used to calculate the position on the torus curve of the current tubular segement

				const u = i / tubularSegments * p * Math.PI * 2;

				// now we calculate two points. P1 is our current position on the curve, P2 is a little farther ahead.
				// these points are used to create a special "coordinate space", which is necessary to calculate the correct vertex positions

				calculatePositionOnCurve( u, p, q, radius, P1 );
				calculatePositionOnCurve( u + 0.01, p, q, radius, P2 );

				// calculate orthonormal basis

				T.subVectors( P2, P1 );
				N.addVectors( P2, P1 );
				B.crossVectors( T, N );
				N.crossVectors( B, T );

				// normalize B, N. T can be ignored, we don't use it

				B.normalize();
				N.normalize();

				for ( let j = 0; j <= radialSegments; ++ j ) {

					// now calculate the vertices. they are nothing more than an extrusion of the torus curve.
					// because we extrude a shape in the xy-plane, there is no need to calculate a z-value.

					const v = j / radialSegments * Math.PI * 2;
					const cx = - tube * Math.cos( v );
					const cy = tube * Math.sin( v );

					// now calculate the final vertex position.
					// first we orient the extrusion with our basis vectos, then we add it to the current position on the curve

					vertex.x = P1.x + ( cx * N.x + cy * B.x );
					vertex.y = P1.y + ( cx * N.y + cy * B.y );
					vertex.z = P1.z + ( cx * N.z + cy * B.z );

					vertices.push( vertex.x, vertex.y, vertex.z );

					// normal (P1 is always the center/origin of the extrusion, thus we can use it to calculate the normal)

					normal.subVectors( vertex, P1 ).normalize();

					normals.push( normal.x, normal.y, normal.z );

					// uv

					uvs.push( i / tubularSegments );
					uvs.push( j / radialSegments );

				}

			}

			// generate indices

			for ( let j = 1; j <= tubularSegments; j ++ ) {

				for ( let i = 1; i <= radialSegments; i ++ ) {

					// indices

					const a = ( radialSegments + 1 ) * ( j - 1 ) + ( i - 1 );
					const b = ( radialSegments + 1 ) * j + ( i - 1 );
					const c = ( radialSegments + 1 ) * j + i;
					const d = ( radialSegments + 1 ) * ( j - 1 ) + i;

					// faces

					indices.push( a, b, d );
					indices.push( b, c, d );

				}

			}

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

			// this function calculates the current position on the torus curve

			function calculatePositionOnCurve( u, p, q, radius, position ) {

				const cu = Math.cos( u );
				const su = Math.sin( u );
				const quOverP = q / p * u;
				const cs = Math.cos( quOverP );

				position.x = radius * ( 2 + cs ) * 0.5 * cu;
				position.y = radius * ( 2 + cs ) * su * 0.5;
				position.z = radius * Math.sin( quOverP ) * 0.5;

			}

		}

	}

	class TorusKnotGeometry extends Geometry {

		constructor( radius, tube, tubularSegments, radialSegments, p, q, heightScale ) {

			super();
			this.type = 'TorusKnotGeometry';

			this.parameters = {
				radius: radius,
				tube: tube,
				tubularSegments: tubularSegments,
				radialSegments: radialSegments,
				p: p,
				q: q
			};

			if ( heightScale !== undefined ) console.warn( 'THREE.TorusKnotGeometry: heightScale has been deprecated. Use .scale( x, y, z ) instead.' );

			this.fromBufferGeometry( new TorusKnotBufferGeometry( radius, tube, tubularSegments, radialSegments, p, q ) );
			this.mergeVertices();

		}

	}

	class TubeBufferGeometry extends BufferGeometry {

		constructor( path, tubularSegments = 64, radius = 1, radialSegments = 8, closed = false ) {

			super();
			this.type = 'TubeBufferGeometry';

			this.parameters = {
				path: path,
				tubularSegments: tubularSegments,
				radius: radius,
				radialSegments: radialSegments,
				closed: closed
			};

			const frames = path.computeFrenetFrames( tubularSegments, closed );

			// expose internals

			this.tangents = frames.tangents;
			this.normals = frames.normals;
			this.binormals = frames.binormals;

			// helper variables

			const vertex = new Vector3();
			const normal = new Vector3();
			const uv = new Vector2();
			let P = new Vector3();

			// buffer

			const vertices = [];
			const normals = [];
			const uvs = [];
			const indices = [];

			// create buffer data

			generateBufferData();

			// build geometry

			this.setIndex( indices );
			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
			this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

			// functions

			function generateBufferData() {

				for ( let i = 0; i < tubularSegments; i ++ ) {

					generateSegment( i );

				}

				// if the geometry is not closed, generate the last row of vertices and normals
				// at the regular position on the given path
				//
				// if the geometry is closed, duplicate the first row of vertices and normals (uvs will differ)

				generateSegment( ( closed === false ) ? tubularSegments : 0 );

				// uvs are generated in a separate function.
				// this makes it easy compute correct values for closed geometries

				generateUVs();

				// finally create faces

				generateIndices();

			}

			function generateSegment( i ) {

				// we use getPointAt to sample evenly distributed points from the given path

				P = path.getPointAt( i / tubularSegments, P );

				// retrieve corresponding normal and binormal

				const N = frames.normals[ i ];
				const B = frames.binormals[ i ];

				// generate normals and vertices for the current segment

				for ( let j = 0; j <= radialSegments; j ++ ) {

					const v = j / radialSegments * Math.PI * 2;

					const sin = Math.sin( v );
					const cos = - Math.cos( v );

					// normal

					normal.x = ( cos * N.x + sin * B.x );
					normal.y = ( cos * N.y + sin * B.y );
					normal.z = ( cos * N.z + sin * B.z );
					normal.normalize();

					normals.push( normal.x, normal.y, normal.z );

					// vertex

					vertex.x = P.x + radius * normal.x;
					vertex.y = P.y + radius * normal.y;
					vertex.z = P.z + radius * normal.z;

					vertices.push( vertex.x, vertex.y, vertex.z );

				}

			}

			function generateIndices() {

				for ( let j = 1; j <= tubularSegments; j ++ ) {

					for ( let i = 1; i <= radialSegments; i ++ ) {

						const a = ( radialSegments + 1 ) * ( j - 1 ) + ( i - 1 );
						const b = ( radialSegments + 1 ) * j + ( i - 1 );
						const c = ( radialSegments + 1 ) * j + i;
						const d = ( radialSegments + 1 ) * ( j - 1 ) + i;

						// faces

						indices.push( a, b, d );
						indices.push( b, c, d );

					}

				}

			}

			function generateUVs() {

				for ( let i = 0; i <= tubularSegments; i ++ ) {

					for ( let j = 0; j <= radialSegments; j ++ ) {

						uv.x = i / tubularSegments;
						uv.y = j / radialSegments;

						uvs.push( uv.x, uv.y );

					}

				}

			}

		}
		toJSON() {

			const data = BufferGeometry.prototype.toJSON.call( this );

			data.path = this.parameters.path.toJSON();

			return data;

		}

	}

	class TubeGeometry extends Geometry {

		constructor( path, tubularSegments, radius, radialSegments, closed, taper ) {

			super();
			this.type = 'TubeGeometry';

			this.parameters = {
				path: path,
				tubularSegments: tubularSegments,
				radius: radius,
				radialSegments: radialSegments,
				closed: closed
			};

			if ( taper !== undefined ) console.warn( 'THREE.TubeGeometry: taper has been removed.' );

			const bufferGeometry = new TubeBufferGeometry( path, tubularSegments, radius, radialSegments, closed );

			// expose internals

			this.tangents = bufferGeometry.tangents;
			this.normals = bufferGeometry.normals;
			this.binormals = bufferGeometry.binormals;

			// create geometry

			this.fromBufferGeometry( bufferGeometry );
			this.mergeVertices();

		}

	}

	class WireframeGeometry extends BufferGeometry {

		constructor( geometry ) {

			super();
			this.type = 'WireframeGeometry';

			// buffer

			const vertices = [];

			// helper variables

			const edge = [ 0, 0 ], edges = {};
			const keys = [ 'a', 'b', 'c' ];

			// different logic for Geometry and BufferGeometry

			if ( geometry && geometry.isGeometry ) {

				// create a data structure that contains all edges without duplicates

				const faces = geometry.faces;

				for ( let i = 0, l = faces.length; i < l; i ++ ) {

					const face = faces[ i ];

					for ( let j = 0; j < 3; j ++ ) {

						const edge1 = face[ keys[ j ] ];
						const edge2 = face[ keys[ ( j + 1 ) % 3 ] ];
						edge[ 0 ] = Math.min( edge1, edge2 ); // sorting prevents duplicates
						edge[ 1 ] = Math.max( edge1, edge2 );

						const key = edge[ 0 ] + ',' + edge[ 1 ];

						if ( edges[ key ] === undefined ) {

							edges[ key ] = { index1: edge[ 0 ], index2: edge[ 1 ] };

						}

					}

				}

				// generate vertices

				for ( const key in edges ) {

					const e = edges[ key ];

					let vertex = geometry.vertices[ e.index1 ];
					vertices.push( vertex.x, vertex.y, vertex.z );

					vertex = geometry.vertices[ e.index2 ];
					vertices.push( vertex.x, vertex.y, vertex.z );

				}

			} else if ( geometry && geometry.isBufferGeometry ) {

				const vertex = new Vector3();

				if ( geometry.index !== null ) {

					// indexed BufferGeometry

					const position = geometry.attributes.position;
					const indices = geometry.index;
					let groups = geometry.groups;

					if ( groups.length === 0 ) {

						groups = [ { start: 0, count: indices.count, materialIndex: 0 } ];

					}

					// create a data structure that contains all eges without duplicates

					for ( let o = 0, ol = groups.length; o < ol; ++ o ) {

						const group = groups[ o ];

						const start = group.start;
						const count = group.count;

						for ( let i = start, l = ( start + count ); i < l; i += 3 ) {

							for ( let j = 0; j < 3; j ++ ) {

								const edge1 = indices.getX( i + j );
								const edge2 = indices.getX( i + ( j + 1 ) % 3 );
								edge[ 0 ] = Math.min( edge1, edge2 ); // sorting prevents duplicates
								edge[ 1 ] = Math.max( edge1, edge2 );

								const key = edge[ 0 ] + ',' + edge[ 1 ];

								if ( edges[ key ] === undefined ) {

									edges[ key ] = { index1: edge[ 0 ], index2: edge[ 1 ] };

								}

							}

						}

					}

					// generate vertices

					for ( const key in edges ) {

						const e = edges[ key ];

						vertex.fromBufferAttribute( position, e.index1 );
						vertices.push( vertex.x, vertex.y, vertex.z );

						vertex.fromBufferAttribute( position, e.index2 );
						vertices.push( vertex.x, vertex.y, vertex.z );

					}

				} else {

					// non-indexed BufferGeometry

					const position = geometry.attributes.position;

					for ( let i = 0, l = ( position.count / 3 ); i < l; i ++ ) {

						for ( let j = 0; j < 3; j ++ ) {

							// three edges per triangle, an edge is represented as (index1, index2)
							// e.g. the first triangle has the following edges: (0,1),(1,2),(2,0)

							const index1 = 3 * i + j;
							vertex.fromBufferAttribute( position, index1 );
							vertices.push( vertex.x, vertex.y, vertex.z );

							const index2 = 3 * i + ( ( j + 1 ) % 3 );
							vertex.fromBufferAttribute( position, index2 );
							vertices.push( vertex.x, vertex.y, vertex.z );

						}

					}

				}

			}

			// build geometry

			this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );

		}

	}

	var Geometries = /*#__PURE__*/Object.freeze({
		__proto__: null,
		BoxGeometry: BoxGeometry,
		BoxBufferGeometry: BoxBufferGeometry,
		CircleGeometry: CircleGeometry,
		CircleBufferGeometry: CircleBufferGeometry,
		ConeGeometry: ConeGeometry,
		ConeBufferGeometry: ConeBufferGeometry,
		CylinderGeometry: CylinderGeometry,
		CylinderBufferGeometry: CylinderBufferGeometry,
		DodecahedronGeometry: DodecahedronGeometry,
		DodecahedronBufferGeometry: DodecahedronBufferGeometry,
		EdgesGeometry: EdgesGeometry,
		ExtrudeGeometry: ExtrudeGeometry,
		ExtrudeBufferGeometry: ExtrudeBufferGeometry,
		IcosahedronGeometry: IcosahedronGeometry,
		IcosahedronBufferGeometry: IcosahedronBufferGeometry,
		LatheGeometry: LatheGeometry,
		LatheBufferGeometry: LatheBufferGeometry,
		OctahedronGeometry: OctahedronGeometry,
		OctahedronBufferGeometry: OctahedronBufferGeometry,
		ParametricGeometry: ParametricGeometry,
		ParametricBufferGeometry: ParametricBufferGeometry,
		PlaneGeometry: PlaneGeometry,
		PlaneBufferGeometry: PlaneBufferGeometry,
		PolyhedronGeometry: PolyhedronGeometry,
		PolyhedronBufferGeometry: PolyhedronBufferGeometry,
		RingGeometry: RingGeometry,
		RingBufferGeometry: RingBufferGeometry,
		ShapeGeometry: ShapeGeometry,
		ShapeBufferGeometry: ShapeBufferGeometry,
		SphereGeometry: SphereGeometry,
		SphereBufferGeometry: SphereBufferGeometry,
		TetrahedronGeometry: TetrahedronGeometry,
		TetrahedronBufferGeometry: TetrahedronBufferGeometry,
		TextGeometry: TextGeometry,
		TextBufferGeometry: TextBufferGeometry,
		TorusGeometry: TorusGeometry,
		TorusBufferGeometry: TorusBufferGeometry,
		TorusKnotGeometry: TorusKnotGeometry,
		TorusKnotBufferGeometry: TorusKnotBufferGeometry,
		TubeGeometry: TubeGeometry,
		TubeBufferGeometry: TubeBufferGeometry,
		WireframeGeometry: WireframeGeometry
	});

	/**
	 * parameters = {
	 *  color: <THREE.Color>
	 * }
	 */

	function ShadowMaterial( parameters ) {

		Material.call( this );

		this.type = 'ShadowMaterial';

		this.color = new Color( 0x000000 );
		this.transparent = true;

		this.setValues( parameters );

	}

	ShadowMaterial.prototype = Object.create( Material.prototype );
	ShadowMaterial.prototype.constructor = ShadowMaterial;

	ShadowMaterial.prototype.isShadowMaterial = true;

	ShadowMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.color.copy( source.color );

		return this;

	};

	function RawShaderMaterial( parameters ) {

		ShaderMaterial.call( this, parameters );

		this.type = 'RawShaderMaterial';

	}

	RawShaderMaterial.prototype = Object.create( ShaderMaterial.prototype );
	RawShaderMaterial.prototype.constructor = RawShaderMaterial;

	RawShaderMaterial.prototype.isRawShaderMaterial = true;

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  roughness: <float>,
	 *  metalness: <float>,
	 *  opacity: <float>,
	 *
	 *  map: new THREE.Texture( <Image> ),
	 *
	 *  lightMap: new THREE.Texture( <Image> ),
	 *  lightMapIntensity: <float>
	 *
	 *  aoMap: new THREE.Texture( <Image> ),
	 *  aoMapIntensity: <float>
	 *
	 *  emissive: <hex>,
	 *  emissiveIntensity: <float>
	 *  emissiveMap: new THREE.Texture( <Image> ),
	 *
	 *  bumpMap: new THREE.Texture( <Image> ),
	 *  bumpScale: <float>,
	 *
	 *  normalMap: new THREE.Texture( <Image> ),
	 *  normalMapType: THREE.TangentSpaceNormalMap,
	 *  normalScale: <Vector2>,
	 *
	 *  displacementMap: new THREE.Texture( <Image> ),
	 *  displacementScale: <float>,
	 *  displacementBias: <float>,
	 *
	 *  roughnessMap: new THREE.Texture( <Image> ),
	 *
	 *  metalnessMap: new THREE.Texture( <Image> ),
	 *
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *
	 *  envMap: new THREE.CubeTexture( [posx, negx, posy, negy, posz, negz] ),
	 *  envMapIntensity: <float>
	 *
	 *  refractionRatio: <float>,
	 *
	 *  wireframe: <boolean>,
	 *  wireframeLinewidth: <float>,
	 *
	 *  skinning: <bool>,
	 *  morphTargets: <bool>,
	 *  morphNormals: <bool>
	 * }
	 */

	function MeshStandardMaterial( parameters ) {

		Material.call( this );

		this.defines = { 'STANDARD': '' };

		this.type = 'MeshStandardMaterial';

		this.color = new Color( 0xffffff ); // diffuse
		this.roughness = 1.0;
		this.metalness = 0.0;

		this.map = null;

		this.lightMap = null;
		this.lightMapIntensity = 1.0;

		this.aoMap = null;
		this.aoMapIntensity = 1.0;

		this.emissive = new Color( 0x000000 );
		this.emissiveIntensity = 1.0;
		this.emissiveMap = null;

		this.bumpMap = null;
		this.bumpScale = 1;

		this.normalMap = null;
		this.normalMapType = TangentSpaceNormalMap;
		this.normalScale = new Vector2( 1, 1 );

		this.displacementMap = null;
		this.displacementScale = 1;
		this.displacementBias = 0;

		this.roughnessMap = null;

		this.metalnessMap = null;

		this.alphaMap = null;

		this.envMap = null;
		this.envMapIntensity = 1.0;

		this.refractionRatio = 0.98;

		this.wireframe = false;
		this.wireframeLinewidth = 1;
		this.wireframeLinecap = 'round';
		this.wireframeLinejoin = 'round';

		this.skinning = false;
		this.morphTargets = false;
		this.morphNormals = false;

		this.vertexTangents = false;

		this.setValues( parameters );

	}

	MeshStandardMaterial.prototype = Object.create( Material.prototype );
	MeshStandardMaterial.prototype.constructor = MeshStandardMaterial;

	MeshStandardMaterial.prototype.isMeshStandardMaterial = true;

	MeshStandardMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.defines = { 'STANDARD': '' };

		this.color.copy( source.color );
		this.roughness = source.roughness;
		this.metalness = source.metalness;

		this.map = source.map;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.emissive.copy( source.emissive );
		this.emissiveMap = source.emissiveMap;
		this.emissiveIntensity = source.emissiveIntensity;

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.roughnessMap = source.roughnessMap;

		this.metalnessMap = source.metalnessMap;

		this.alphaMap = source.alphaMap;

		this.envMap = source.envMap;
		this.envMapIntensity = source.envMapIntensity;

		this.refractionRatio = source.refractionRatio;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.skinning = source.skinning;
		this.morphTargets = source.morphTargets;
		this.morphNormals = source.morphNormals;

		this.vertexTangents = source.vertexTangents;

		return this;

	};

	/**
	 * parameters = {
	 *  clearcoat: <float>,
	 *  clearcoatMap: new THREE.Texture( <Image> ),
	 *  clearcoatRoughness: <float>,
	 *  clearcoatRoughnessMap: new THREE.Texture( <Image> ),
	 *  clearcoatNormalScale: <Vector2>,
	 *  clearcoatNormalMap: new THREE.Texture( <Image> ),
	 *
	 *  reflectivity: <float>,
	 *  ior: <float>,
	 *
	 *  sheen: <Color>,
	 *
	 *  transmission: <float>,
	 *  transmissionMap: new THREE.Texture( <Image> )
	 * }
	 */

	function MeshPhysicalMaterial( parameters ) {

		MeshStandardMaterial.call( this );

		this.defines = {

			'STANDARD': '',
			'PHYSICAL': ''

		};

		this.type = 'MeshPhysicalMaterial';

		this.clearcoat = 0.0;
		this.clearcoatMap = null;
		this.clearcoatRoughness = 0.0;
		this.clearcoatRoughnessMap = null;
		this.clearcoatNormalScale = new Vector2( 1, 1 );
		this.clearcoatNormalMap = null;

		this.reflectivity = 0.5; // maps to F0 = 0.04

		Object.defineProperty( this, 'ior', {
			get: function () {

				return ( 1 + 0.4 * this.reflectivity ) / ( 1 - 0.4 * this.reflectivity );

			},
			set: function ( ior ) {

				this.reflectivity = MathUtils.clamp( 2.5 * ( ior - 1 ) / ( ior + 1 ), 0, 1 );

			}
		} );

		this.sheen = null; // null will disable sheen bsdf

		this.transmission = 0.0;
		this.transmissionMap = null;

		this.setValues( parameters );

	}

	MeshPhysicalMaterial.prototype = Object.create( MeshStandardMaterial.prototype );
	MeshPhysicalMaterial.prototype.constructor = MeshPhysicalMaterial;

	MeshPhysicalMaterial.prototype.isMeshPhysicalMaterial = true;

	MeshPhysicalMaterial.prototype.copy = function ( source ) {

		MeshStandardMaterial.prototype.copy.call( this, source );

		this.defines = {

			'STANDARD': '',
			'PHYSICAL': ''

		};

		this.clearcoat = source.clearcoat;
		this.clearcoatMap = source.clearcoatMap;
		this.clearcoatRoughness = source.clearcoatRoughness;
		this.clearcoatRoughnessMap = source.clearcoatRoughnessMap;
		this.clearcoatNormalMap = source.clearcoatNormalMap;
		this.clearcoatNormalScale.copy( source.clearcoatNormalScale );

		this.reflectivity = source.reflectivity;

		if ( source.sheen ) {

			this.sheen = ( this.sheen || new Color() ).copy( source.sheen );

		} else {

			this.sheen = null;

		}

		this.transmission = source.transmission;
		this.transmissionMap = source.transmissionMap;

		return this;

	};

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  specular: <hex>,
	 *  shininess: <float>,
	 *  opacity: <float>,
	 *
	 *  map: new THREE.Texture( <Image> ),
	 *
	 *  lightMap: new THREE.Texture( <Image> ),
	 *  lightMapIntensity: <float>
	 *
	 *  aoMap: new THREE.Texture( <Image> ),
	 *  aoMapIntensity: <float>
	 *
	 *  emissive: <hex>,
	 *  emissiveIntensity: <float>
	 *  emissiveMap: new THREE.Texture( <Image> ),
	 *
	 *  bumpMap: new THREE.Texture( <Image> ),
	 *  bumpScale: <float>,
	 *
	 *  normalMap: new THREE.Texture( <Image> ),
	 *  normalMapType: THREE.TangentSpaceNormalMap,
	 *  normalScale: <Vector2>,
	 *
	 *  displacementMap: new THREE.Texture( <Image> ),
	 *  displacementScale: <float>,
	 *  displacementBias: <float>,
	 *
	 *  specularMap: new THREE.Texture( <Image> ),
	 *
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *
	 *  envMap: new THREE.CubeTexture( [posx, negx, posy, negy, posz, negz] ),
	 *  combine: THREE.MultiplyOperation,
	 *  reflectivity: <float>,
	 *  refractionRatio: <float>,
	 *
	 *  wireframe: <boolean>,
	 *  wireframeLinewidth: <float>,
	 *
	 *  skinning: <bool>,
	 *  morphTargets: <bool>,
	 *  morphNormals: <bool>
	 * }
	 */

	function MeshPhongMaterial( parameters ) {

		Material.call( this );

		this.type = 'MeshPhongMaterial';

		this.color = new Color( 0xffffff ); // diffuse
		this.specular = new Color( 0x111111 );
		this.shininess = 30;

		this.map = null;

		this.lightMap = null;
		this.lightMapIntensity = 1.0;

		this.aoMap = null;
		this.aoMapIntensity = 1.0;

		this.emissive = new Color( 0x000000 );
		this.emissiveIntensity = 1.0;
		this.emissiveMap = null;

		this.bumpMap = null;
		this.bumpScale = 1;

		this.normalMap = null;
		this.normalMapType = TangentSpaceNormalMap;
		this.normalScale = new Vector2( 1, 1 );

		this.displacementMap = null;
		this.displacementScale = 1;
		this.displacementBias = 0;

		this.specularMap = null;

		this.alphaMap = null;

		this.envMap = null;
		this.combine = MultiplyOperation;
		this.reflectivity = 1;
		this.refractionRatio = 0.98;

		this.wireframe = false;
		this.wireframeLinewidth = 1;
		this.wireframeLinecap = 'round';
		this.wireframeLinejoin = 'round';

		this.skinning = false;
		this.morphTargets = false;
		this.morphNormals = false;

		this.setValues( parameters );

	}

	MeshPhongMaterial.prototype = Object.create( Material.prototype );
	MeshPhongMaterial.prototype.constructor = MeshPhongMaterial;

	MeshPhongMaterial.prototype.isMeshPhongMaterial = true;

	MeshPhongMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.color.copy( source.color );
		this.specular.copy( source.specular );
		this.shininess = source.shininess;

		this.map = source.map;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.emissive.copy( source.emissive );
		this.emissiveMap = source.emissiveMap;
		this.emissiveIntensity = source.emissiveIntensity;

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.specularMap = source.specularMap;

		this.alphaMap = source.alphaMap;

		this.envMap = source.envMap;
		this.combine = source.combine;
		this.reflectivity = source.reflectivity;
		this.refractionRatio = source.refractionRatio;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.skinning = source.skinning;
		this.morphTargets = source.morphTargets;
		this.morphNormals = source.morphNormals;

		return this;

	};

	/**
	 * parameters = {
	 *  color: <hex>,
	 *
	 *  map: new THREE.Texture( <Image> ),
	 *  gradientMap: new THREE.Texture( <Image> ),
	 *
	 *  lightMap: new THREE.Texture( <Image> ),
	 *  lightMapIntensity: <float>
	 *
	 *  aoMap: new THREE.Texture( <Image> ),
	 *  aoMapIntensity: <float>
	 *
	 *  emissive: <hex>,
	 *  emissiveIntensity: <float>
	 *  emissiveMap: new THREE.Texture( <Image> ),
	 *
	 *  bumpMap: new THREE.Texture( <Image> ),
	 *  bumpScale: <float>,
	 *
	 *  normalMap: new THREE.Texture( <Image> ),
	 *  normalMapType: THREE.TangentSpaceNormalMap,
	 *  normalScale: <Vector2>,
	 *
	 *  displacementMap: new THREE.Texture( <Image> ),
	 *  displacementScale: <float>,
	 *  displacementBias: <float>,
	 *
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *
	 *  wireframe: <boolean>,
	 *  wireframeLinewidth: <float>,
	 *
	 *  skinning: <bool>,
	 *  morphTargets: <bool>,
	 *  morphNormals: <bool>
	 * }
	 */

	function MeshToonMaterial( parameters ) {

		Material.call( this );

		this.defines = { 'TOON': '' };

		this.type = 'MeshToonMaterial';

		this.color = new Color( 0xffffff );

		this.map = null;
		this.gradientMap = null;

		this.lightMap = null;
		this.lightMapIntensity = 1.0;

		this.aoMap = null;
		this.aoMapIntensity = 1.0;

		this.emissive = new Color( 0x000000 );
		this.emissiveIntensity = 1.0;
		this.emissiveMap = null;

		this.bumpMap = null;
		this.bumpScale = 1;

		this.normalMap = null;
		this.normalMapType = TangentSpaceNormalMap;
		this.normalScale = new Vector2( 1, 1 );

		this.displacementMap = null;
		this.displacementScale = 1;
		this.displacementBias = 0;

		this.alphaMap = null;

		this.wireframe = false;
		this.wireframeLinewidth = 1;
		this.wireframeLinecap = 'round';
		this.wireframeLinejoin = 'round';

		this.skinning = false;
		this.morphTargets = false;
		this.morphNormals = false;

		this.setValues( parameters );

	}

	MeshToonMaterial.prototype = Object.create( Material.prototype );
	MeshToonMaterial.prototype.constructor = MeshToonMaterial;

	MeshToonMaterial.prototype.isMeshToonMaterial = true;

	MeshToonMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.color.copy( source.color );

		this.map = source.map;
		this.gradientMap = source.gradientMap;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.emissive.copy( source.emissive );
		this.emissiveMap = source.emissiveMap;
		this.emissiveIntensity = source.emissiveIntensity;

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.alphaMap = source.alphaMap;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.skinning = source.skinning;
		this.morphTargets = source.morphTargets;
		this.morphNormals = source.morphNormals;

		return this;

	};

	/**
	 * parameters = {
	 *  opacity: <float>,
	 *
	 *  bumpMap: new THREE.Texture( <Image> ),
	 *  bumpScale: <float>,
	 *
	 *  normalMap: new THREE.Texture( <Image> ),
	 *  normalMapType: THREE.TangentSpaceNormalMap,
	 *  normalScale: <Vector2>,
	 *
	 *  displacementMap: new THREE.Texture( <Image> ),
	 *  displacementScale: <float>,
	 *  displacementBias: <float>,
	 *
	 *  wireframe: <boolean>,
	 *  wireframeLinewidth: <float>
	 *
	 *  skinning: <bool>,
	 *  morphTargets: <bool>,
	 *  morphNormals: <bool>
	 * }
	 */

	function MeshNormalMaterial( parameters ) {

		Material.call( this );

		this.type = 'MeshNormalMaterial';

		this.bumpMap = null;
		this.bumpScale = 1;

		this.normalMap = null;
		this.normalMapType = TangentSpaceNormalMap;
		this.normalScale = new Vector2( 1, 1 );

		this.displacementMap = null;
		this.displacementScale = 1;
		this.displacementBias = 0;

		this.wireframe = false;
		this.wireframeLinewidth = 1;

		this.fog = false;

		this.skinning = false;
		this.morphTargets = false;
		this.morphNormals = false;

		this.setValues( parameters );

	}

	MeshNormalMaterial.prototype = Object.create( Material.prototype );
	MeshNormalMaterial.prototype.constructor = MeshNormalMaterial;

	MeshNormalMaterial.prototype.isMeshNormalMaterial = true;

	MeshNormalMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;

		this.skinning = source.skinning;
		this.morphTargets = source.morphTargets;
		this.morphNormals = source.morphNormals;

		return this;

	};

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  opacity: <float>,
	 *
	 *  map: new THREE.Texture( <Image> ),
	 *
	 *  lightMap: new THREE.Texture( <Image> ),
	 *  lightMapIntensity: <float>
	 *
	 *  aoMap: new THREE.Texture( <Image> ),
	 *  aoMapIntensity: <float>
	 *
	 *  emissive: <hex>,
	 *  emissiveIntensity: <float>
	 *  emissiveMap: new THREE.Texture( <Image> ),
	 *
	 *  specularMap: new THREE.Texture( <Image> ),
	 *
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *
	 *  envMap: new THREE.CubeTexture( [posx, negx, posy, negy, posz, negz] ),
	 *  combine: THREE.Multiply,
	 *  reflectivity: <float>,
	 *  refractionRatio: <float>,
	 *
	 *  wireframe: <boolean>,
	 *  wireframeLinewidth: <float>,
	 *
	 *  skinning: <bool>,
	 *  morphTargets: <bool>,
	 *  morphNormals: <bool>
	 * }
	 */

	function MeshLambertMaterial( parameters ) {

		Material.call( this );

		this.type = 'MeshLambertMaterial';

		this.color = new Color( 0xffffff ); // diffuse

		this.map = null;

		this.lightMap = null;
		this.lightMapIntensity = 1.0;

		this.aoMap = null;
		this.aoMapIntensity = 1.0;

		this.emissive = new Color( 0x000000 );
		this.emissiveIntensity = 1.0;
		this.emissiveMap = null;

		this.specularMap = null;

		this.alphaMap = null;

		this.envMap = null;
		this.combine = MultiplyOperation;
		this.reflectivity = 1;
		this.refractionRatio = 0.98;

		this.wireframe = false;
		this.wireframeLinewidth = 1;
		this.wireframeLinecap = 'round';
		this.wireframeLinejoin = 'round';

		this.skinning = false;
		this.morphTargets = false;
		this.morphNormals = false;

		this.setValues( parameters );

	}

	MeshLambertMaterial.prototype = Object.create( Material.prototype );
	MeshLambertMaterial.prototype.constructor = MeshLambertMaterial;

	MeshLambertMaterial.prototype.isMeshLambertMaterial = true;

	MeshLambertMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.color.copy( source.color );

		this.map = source.map;

		this.lightMap = source.lightMap;
		this.lightMapIntensity = source.lightMapIntensity;

		this.aoMap = source.aoMap;
		this.aoMapIntensity = source.aoMapIntensity;

		this.emissive.copy( source.emissive );
		this.emissiveMap = source.emissiveMap;
		this.emissiveIntensity = source.emissiveIntensity;

		this.specularMap = source.specularMap;

		this.alphaMap = source.alphaMap;

		this.envMap = source.envMap;
		this.combine = source.combine;
		this.reflectivity = source.reflectivity;
		this.refractionRatio = source.refractionRatio;

		this.wireframe = source.wireframe;
		this.wireframeLinewidth = source.wireframeLinewidth;
		this.wireframeLinecap = source.wireframeLinecap;
		this.wireframeLinejoin = source.wireframeLinejoin;

		this.skinning = source.skinning;
		this.morphTargets = source.morphTargets;
		this.morphNormals = source.morphNormals;

		return this;

	};

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  opacity: <float>,
	 *
	 *  matcap: new THREE.Texture( <Image> ),
	 *
	 *  map: new THREE.Texture( <Image> ),
	 *
	 *  bumpMap: new THREE.Texture( <Image> ),
	 *  bumpScale: <float>,
	 *
	 *  normalMap: new THREE.Texture( <Image> ),
	 *  normalMapType: THREE.TangentSpaceNormalMap,
	 *  normalScale: <Vector2>,
	 *
	 *  displacementMap: new THREE.Texture( <Image> ),
	 *  displacementScale: <float>,
	 *  displacementBias: <float>,
	 *
	 *  alphaMap: new THREE.Texture( <Image> ),
	 *
	 *  skinning: <bool>,
	 *  morphTargets: <bool>,
	 *  morphNormals: <bool>
	 * }
	 */

	function MeshMatcapMaterial( parameters ) {

		Material.call( this );

		this.defines = { 'MATCAP': '' };

		this.type = 'MeshMatcapMaterial';

		this.color = new Color( 0xffffff ); // diffuse

		this.matcap = null;

		this.map = null;

		this.bumpMap = null;
		this.bumpScale = 1;

		this.normalMap = null;
		this.normalMapType = TangentSpaceNormalMap;
		this.normalScale = new Vector2( 1, 1 );

		this.displacementMap = null;
		this.displacementScale = 1;
		this.displacementBias = 0;

		this.alphaMap = null;

		this.skinning = false;
		this.morphTargets = false;
		this.morphNormals = false;

		this.setValues( parameters );

	}

	MeshMatcapMaterial.prototype = Object.create( Material.prototype );
	MeshMatcapMaterial.prototype.constructor = MeshMatcapMaterial;

	MeshMatcapMaterial.prototype.isMeshMatcapMaterial = true;

	MeshMatcapMaterial.prototype.copy = function ( source ) {

		Material.prototype.copy.call( this, source );

		this.defines = { 'MATCAP': '' };

		this.color.copy( source.color );

		this.matcap = source.matcap;

		this.map = source.map;

		this.bumpMap = source.bumpMap;
		this.bumpScale = source.bumpScale;

		this.normalMap = source.normalMap;
		this.normalMapType = source.normalMapType;
		this.normalScale.copy( source.normalScale );

		this.displacementMap = source.displacementMap;
		this.displacementScale = source.displacementScale;
		this.displacementBias = source.displacementBias;

		this.alphaMap = source.alphaMap;

		this.skinning = source.skinning;
		this.morphTargets = source.morphTargets;
		this.morphNormals = source.morphNormals;

		return this;

	};

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  opacity: <float>,
	 *
	 *  linewidth: <float>,
	 *
	 *  scale: <float>,
	 *  dashSize: <float>,
	 *  gapSize: <float>
	 * }
	 */

	function LineDashedMaterial( parameters ) {

		LineBasicMaterial.call( this );

		this.type = 'LineDashedMaterial';

		this.scale = 1;
		this.dashSize = 3;
		this.gapSize = 1;

		this.setValues( parameters );

	}

	LineDashedMaterial.prototype = Object.create( LineBasicMaterial.prototype );
	LineDashedMaterial.prototype.constructor = LineDashedMaterial;

	LineDashedMaterial.prototype.isLineDashedMaterial = true;

	LineDashedMaterial.prototype.copy = function ( source ) {

		LineBasicMaterial.prototype.copy.call( this, source );

		this.scale = source.scale;
		this.dashSize = source.dashSize;
		this.gapSize = source.gapSize;

		return this;

	};

	var Materials = /*#__PURE__*/Object.freeze({
		__proto__: null,
		ShadowMaterial: ShadowMaterial,
		SpriteMaterial: SpriteMaterial,
		RawShaderMaterial: RawShaderMaterial,
		ShaderMaterial: ShaderMaterial,
		PointsMaterial: PointsMaterial,
		MeshPhysicalMaterial: MeshPhysicalMaterial,
		MeshStandardMaterial: MeshStandardMaterial,
		MeshPhongMaterial: MeshPhongMaterial,
		MeshToonMaterial: MeshToonMaterial,
		MeshNormalMaterial: MeshNormalMaterial,
		MeshLambertMaterial: MeshLambertMaterial,
		MeshDepthMaterial: MeshDepthMaterial,
		MeshDistanceMaterial: MeshDistanceMaterial,
		MeshBasicMaterial: MeshBasicMaterial,
		MeshMatcapMaterial: MeshMatcapMaterial,
		LineDashedMaterial: LineDashedMaterial,
		LineBasicMaterial: LineBasicMaterial,
		Material: Material
	});

	const AnimationUtils = {

		// same as Array.prototype.slice, but also works on typed arrays
		arraySlice: function ( array, from, to ) {

			if ( AnimationUtils.isTypedArray( array ) ) {

				// in ios9 array.subarray(from, undefined) will return empty array
				// but array.subarray(from) or array.subarray(from, len) is correct
				return new array.constructor( array.subarray( from, to !== undefined ? to : array.length ) );

			}

			return array.slice( from, to );

		},

		// converts an array to a specific type
		convertArray: function ( array, type, forceClone ) {

			if ( ! array || // let 'undefined' and 'null' pass
				! forceClone && array.constructor === type ) return array;

			if ( typeof type.BYTES_PER_ELEMENT === 'number' ) {

				return new type( array ); // create typed array

			}

			return Array.prototype.slice.call( array ); // create Array

		},

		isTypedArray: function ( object ) {

			return ArrayBuffer.isView( object ) &&
				! ( object instanceof DataView );

		},

		// returns an array by which times and values can be sorted
		getKeyframeOrder: function ( times ) {

			function compareTime( i, j ) {

				return times[ i ] - times[ j ];

			}

			const n = times.length;
			const result = new Array( n );
			for ( let i = 0; i !== n; ++ i ) result[ i ] = i;

			result.sort( compareTime );

			return result;

		},

		// uses the array previously returned by 'getKeyframeOrder' to sort data
		sortedArray: function ( values, stride, order ) {

			const nValues = values.length;
			const result = new values.constructor( nValues );

			for ( let i = 0, dstOffset = 0; dstOffset !== nValues; ++ i ) {

				const srcOffset = order[ i ] * stride;

				for ( let j = 0; j !== stride; ++ j ) {

					result[ dstOffset ++ ] = values[ srcOffset + j ];

				}

			}

			return result;

		},

		// function for parsing AOS keyframe formats
		flattenJSON: function ( jsonKeys, times, values, valuePropertyName ) {

			let i = 1, key = jsonKeys[ 0 ];

			while ( key !== undefined && key[ valuePropertyName ] === undefined ) {

				key = jsonKeys[ i ++ ];

			}

			if ( key === undefined ) return; // no data

			let value = key[ valuePropertyName ];
			if ( value === undefined ) return; // no data

			if ( Array.isArray( value ) ) {

				do {

					value = key[ valuePropertyName ];

					if ( value !== undefined ) {

						times.push( key.time );
						values.push.apply( values, value ); // push all elements

					}

					key = jsonKeys[ i ++ ];

				} while ( key !== undefined );

			} else if ( value.toArray !== undefined ) {

				// ...assume THREE.Math-ish

				do {

					value = key[ valuePropertyName ];

					if ( value !== undefined ) {

						times.push( key.time );
						value.toArray( values, values.length );

					}

					key = jsonKeys[ i ++ ];

				} while ( key !== undefined );

			} else {

				// otherwise push as-is

				do {

					value = key[ valuePropertyName ];

					if ( value !== undefined ) {

						times.push( key.time );
						values.push( value );

					}

					key = jsonKeys[ i ++ ];

				} while ( key !== undefined );

			}

		},

		subclip: function ( sourceClip, name, startFrame, endFrame, fps = 30 ) {

			const clip = sourceClip.clone();

			clip.name = name;

			const tracks = [];

			for ( let i = 0; i < clip.tracks.length; ++ i ) {

				const track = clip.tracks[ i ];
				const valueSize = track.getValueSize();

				const times = [];
				const values = [];

				for ( let j = 0; j < track.times.length; ++ j ) {

					const frame = track.times[ j ] * fps;

					if ( frame < startFrame || frame >= endFrame ) continue;

					times.push( track.times[ j ] );

					for ( let k = 0; k < valueSize; ++ k ) {

						values.push( track.values[ j * valueSize + k ] );

					}

				}

				if ( times.length === 0 ) continue;

				track.times = AnimationUtils.convertArray( times, track.times.constructor );
				track.values = AnimationUtils.convertArray( values, track.values.constructor );

				tracks.push( track );

			}

			clip.tracks = tracks;

			// find minimum .times value across all tracks in the trimmed clip

			let minStartTime = Infinity;

			for ( let i = 0; i < clip.tracks.length; ++ i ) {

				if ( minStartTime > clip.tracks[ i ].times[ 0 ] ) {

					minStartTime = clip.tracks[ i ].times[ 0 ];

				}

			}

			// shift all tracks such that clip begins at t=0

			for ( let i = 0; i < clip.tracks.length; ++ i ) {

				clip.tracks[ i ].shift( - 1 * minStartTime );

			}

			clip.resetDuration();

			return clip;

		},

		makeClipAdditive: function ( targetClip, referenceFrame = 0, referenceClip = targetClip, fps = 30 ) {

			if ( fps <= 0 ) fps = 30;

			const numTracks = referenceClip.tracks.length;
			const referenceTime = referenceFrame / fps;

			// Make each track's values relative to the values at the reference frame
			for ( let i = 0; i < numTracks; ++ i ) {

				const referenceTrack = referenceClip.tracks[ i ];
				const referenceTrackType = referenceTrack.ValueTypeName;

				// Skip this track if it's non-numeric
				if ( referenceTrackType === 'bool' || referenceTrackType === 'string' ) continue;

				// Find the track in the target clip whose name and type matches the reference track
				const targetTrack = targetClip.tracks.find( function ( track ) {

					return track.name === referenceTrack.name
						&& track.ValueTypeName === referenceTrackType;

				} );

				if ( targetTrack === undefined ) continue;

				let referenceOffset = 0;
				const referenceValueSize = referenceTrack.getValueSize();

				if ( referenceTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {

					referenceOffset = referenceValueSize / 3;

				}

				let targetOffset = 0;
				const targetValueSize = targetTrack.getValueSize();

				if ( targetTrack.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline ) {

					targetOffset = targetValueSize / 3;

				}

				const lastIndex = referenceTrack.times.length - 1;
				let referenceValue;

				// Find the value to subtract out of the track
				if ( referenceTime <= referenceTrack.times[ 0 ] ) {

					// Reference frame is earlier than the first keyframe, so just use the first keyframe
					const startIndex = referenceOffset;
					const endIndex = referenceValueSize - referenceOffset;
					referenceValue = AnimationUtils.arraySlice( referenceTrack.values, startIndex, endIndex );

				} else if ( referenceTime >= referenceTrack.times[ lastIndex ] ) {

					// Reference frame is after the last keyframe, so just use the last keyframe
					const startIndex = lastIndex * referenceValueSize + referenceOffset;
					const endIndex = startIndex + referenceValueSize - referenceOffset;
					referenceValue = AnimationUtils.arraySlice( referenceTrack.values, startIndex, endIndex );

				} else {

					// Interpolate to the reference value
					const interpolant = referenceTrack.createInterpolant();
					const startIndex = referenceOffset;
					const endIndex = referenceValueSize - referenceOffset;
					interpolant.evaluate( referenceTime );
					referenceValue = AnimationUtils.arraySlice( interpolant.resultBuffer, startIndex, endIndex );

				}

				// Conjugate the quaternion
				if ( referenceTrackType === 'quaternion' ) {

					const referenceQuat = new Quaternion().fromArray( referenceValue ).normalize().conjugate();
					referenceQuat.toArray( referenceValue );

				}

				// Subtract the reference value from all of the track values

				const numTimes = targetTrack.times.length;
				for ( let j = 0; j < numTimes; ++ j ) {

					const valueStart = j * targetValueSize + targetOffset;

					if ( referenceTrackType === 'quaternion' ) {

						// Multiply the conjugate for quaternion track types
						Quaternion.multiplyQuaternionsFlat(
							targetTrack.values,
							valueStart,
							referenceValue,
							0,
							targetTrack.values,
							valueStart
						);

					} else {

						const valueEnd = targetValueSize - targetOffset * 2;

						// Subtract each value for all other numeric track types
						for ( let k = 0; k < valueEnd; ++ k ) {

							targetTrack.values[ valueStart + k ] -= referenceValue[ k ];

						}

					}

				}

			}

			targetClip.blendMode = AdditiveAnimationBlendMode;

			return targetClip;

		}

	};

	/**
	 * Abstract base class of interpolants over parametric samples.
	 *
	 * The parameter domain is one dimensional, typically the time or a path
	 * along a curve defined by the data.
	 *
	 * The sample values can have any dimensionality and derived classes may
	 * apply special interpretations to the data.
	 *
	 * This class provides the interval seek in a Template Method, deferring
	 * the actual interpolation to derived classes.
	 *
	 * Time complexity is O(1) for linear access crossing at most two points
	 * and O(log N) for random access, where N is the number of positions.
	 *
	 * References:
	 *
	 * 		http://www.oodesign.com/template-method-pattern.html
	 *
	 */

	function Interpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) {

		this.parameterPositions = parameterPositions;
		this._cachedIndex = 0;

		this.resultBuffer = resultBuffer !== undefined ?
			resultBuffer : new sampleValues.constructor( sampleSize );
		this.sampleValues = sampleValues;
		this.valueSize = sampleSize;

	}

	Object.assign( Interpolant.prototype, {

		evaluate: function ( t ) {

			const pp = this.parameterPositions;
			let i1 = this._cachedIndex,
				t1 = pp[ i1 ],
				t0 = pp[ i1 - 1 ];

			validate_interval: {

				seek: {

					let right;

					linear_scan: {

						//- See http://jsperf.com/comparison-to-undefined/3
						//- slower code:
						//-
						//- 				if ( t >= t1 || t1 === undefined ) {
						forward_scan: if ( ! ( t < t1 ) ) {

							for ( let giveUpAt = i1 + 2; ; ) {

								if ( t1 === undefined ) {

									if ( t < t0 ) break forward_scan;

									// after end

									i1 = pp.length;
									this._cachedIndex = i1;
									return this.afterEnd_( i1 - 1, t, t0 );

								}

								if ( i1 === giveUpAt ) break; // this loop

								t0 = t1;
								t1 = pp[ ++ i1 ];

								if ( t < t1 ) {

									// we have arrived at the sought interval
									break seek;

								}

							}

							// prepare binary search on the right side of the index
							right = pp.length;
							break linear_scan;

						}

						//- slower code:
						//-					if ( t < t0 || t0 === undefined ) {
						if ( ! ( t >= t0 ) ) {

							// looping?

							const t1global = pp[ 1 ];

							if ( t < t1global ) {

								i1 = 2; // + 1, using the scan for the details
								t0 = t1global;

							}

							// linear reverse scan

							for ( let giveUpAt = i1 - 2; ; ) {

								if ( t0 === undefined ) {

									// before start

									this._cachedIndex = 0;
									return this.beforeStart_( 0, t, t1 );

								}

								if ( i1 === giveUpAt ) break; // this loop

								t1 = t0;
								t0 = pp[ -- i1 - 1 ];

								if ( t >= t0 ) {

									// we have arrived at the sought interval
									break seek;

								}

							}

							// prepare binary search on the left side of the index
							right = i1;
							i1 = 0;
							break linear_scan;

						}

						// the interval is valid

						break validate_interval;

					} // linear scan

					// binary search

					while ( i1 < right ) {

						const mid = ( i1 + right ) >>> 1;

						if ( t < pp[ mid ] ) {

							right = mid;

						} else {

							i1 = mid + 1;

						}

					}

					t1 = pp[ i1 ];
					t0 = pp[ i1 - 1 ];

					// check boundary cases, again

					if ( t0 === undefined ) {

						this._cachedIndex = 0;
						return this.beforeStart_( 0, t, t1 );

					}

					if ( t1 === undefined ) {

						i1 = pp.length;
						this._cachedIndex = i1;
						return this.afterEnd_( i1 - 1, t0, t );

					}

				} // seek

				this._cachedIndex = i1;

				this.intervalChanged_( i1, t0, t1 );

			} // validate_interval

			return this.interpolate_( i1, t0, t, t1 );

		},

		settings: null, // optional, subclass-specific settings structure
		// Note: The indirection allows central control of many interpolants.

		// --- Protected interface

		DefaultSettings_: {},

		getSettings_: function () {

			return this.settings || this.DefaultSettings_;

		},

		copySampleValue_: function ( index ) {

			// copies a sample value to the result buffer

			const result = this.resultBuffer,
				values = this.sampleValues,
				stride = this.valueSize,
				offset = index * stride;

			for ( let i = 0; i !== stride; ++ i ) {

				result[ i ] = values[ offset + i ];

			}

			return result;

		},

		// Template methods for derived classes:

		interpolate_: function ( /* i1, t0, t, t1 */ ) {

			throw new Error( 'call to abstract method' );
			// implementations shall return this.resultBuffer

		},

		intervalChanged_: function ( /* i1, t0, t1 */ ) {

			// empty

		}

	} );

	// DECLARE ALIAS AFTER assign prototype
	Object.assign( Interpolant.prototype, {

		//( 0, t, t0 ), returns this.resultBuffer
		beforeStart_: Interpolant.prototype.copySampleValue_,

		//( N-1, tN-1, t ), returns this.resultBuffer
		afterEnd_: Interpolant.prototype.copySampleValue_,

	} );

	/**
	 * Fast and simple cubic spline interpolant.
	 *
	 * It was derived from a Hermitian construction setting the first derivative
	 * at each sample position to the linear slope between neighboring positions
	 * over their parameter interval.
	 */

	function CubicInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) {

		Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer );

		this._weightPrev = - 0;
		this._offsetPrev = - 0;
		this._weightNext = - 0;
		this._offsetNext = - 0;

	}

	CubicInterpolant.prototype = Object.assign( Object.create( Interpolant.prototype ), {

		constructor: CubicInterpolant,

		DefaultSettings_: {

			endingStart: ZeroCurvatureEnding,
			endingEnd: ZeroCurvatureEnding

		},

		intervalChanged_: function ( i1, t0, t1 ) {

			const pp = this.parameterPositions;
			let iPrev = i1 - 2,
				iNext = i1 + 1,

				tPrev = pp[ iPrev ],
				tNext = pp[ iNext ];

			if ( tPrev === undefined ) {

				switch ( this.getSettings_().endingStart ) {

					case ZeroSlopeEnding:

						// f'(t0) = 0
						iPrev = i1;
						tPrev = 2 * t0 - t1;

						break;

					case WrapAroundEnding:

						// use the other end of the curve
						iPrev = pp.length - 2;
						tPrev = t0 + pp[ iPrev ] - pp[ iPrev + 1 ];

						break;

					default: // ZeroCurvatureEnding

						// f''(t0) = 0 a.k.a. Natural Spline
						iPrev = i1;
						tPrev = t1;

				}

			}

			if ( tNext === undefined ) {

				switch ( this.getSettings_().endingEnd ) {

					case ZeroSlopeEnding:

						// f'(tN) = 0
						iNext = i1;
						tNext = 2 * t1 - t0;

						break;

					case WrapAroundEnding:

						// use the other end of the curve
						iNext = 1;
						tNext = t1 + pp[ 1 ] - pp[ 0 ];

						break;

					default: // ZeroCurvatureEnding

						// f''(tN) = 0, a.k.a. Natural Spline
						iNext = i1 - 1;
						tNext = t0;

				}

			}

			const halfDt = ( t1 - t0 ) * 0.5,
				stride = this.valueSize;

			this._weightPrev = halfDt / ( t0 - tPrev );
			this._weightNext = halfDt / ( tNext - t1 );
			this._offsetPrev = iPrev * stride;
			this._offsetNext = iNext * stride;

		},

		interpolate_: function ( i1, t0, t, t1 ) {

			const result = this.resultBuffer,
				values = this.sampleValues,
				stride = this.valueSize,

				o1 = i1 * stride,		o0 = o1 - stride,
				oP = this._offsetPrev, 	oN = this._offsetNext,
				wP = this._weightPrev,	wN = this._weightNext,

				p = ( t - t0 ) / ( t1 - t0 ),
				pp = p * p,
				ppp = pp * p;

			// evaluate polynomials

			const sP = - wP * ppp + 2 * wP * pp - wP * p;
			const s0 = ( 1 + wP ) * ppp + ( - 1.5 - 2 * wP ) * pp + ( - 0.5 + wP ) * p + 1;
			const s1 = ( - 1 - wN ) * ppp + ( 1.5 + wN ) * pp + 0.5 * p;
			const sN = wN * ppp - wN * pp;

			// combine data linearly

			for ( let i = 0; i !== stride; ++ i ) {

				result[ i ] =
						sP * values[ oP + i ] +
						s0 * values[ o0 + i ] +
						s1 * values[ o1 + i ] +
						sN * values[ oN + i ];

			}

			return result;

		}

	} );

	function LinearInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) {

		Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer );

	}

	LinearInterpolant.prototype = Object.assign( Object.create( Interpolant.prototype ), {

		constructor: LinearInterpolant,

		interpolate_: function ( i1, t0, t, t1 ) {

			const result = this.resultBuffer,
				values = this.sampleValues,
				stride = this.valueSize,

				offset1 = i1 * stride,
				offset0 = offset1 - stride,

				weight1 = ( t - t0 ) / ( t1 - t0 ),
				weight0 = 1 - weight1;

			for ( let i = 0; i !== stride; ++ i ) {

				result[ i ] =
						values[ offset0 + i ] * weight0 +
						values[ offset1 + i ] * weight1;

			}

			return result;

		}

	} );

	/**
	 *
	 * Interpolant that evaluates to the sample value at the position preceeding
	 * the parameter.
	 */

	function DiscreteInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) {

		Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer );

	}

	DiscreteInterpolant.prototype = Object.assign( Object.create( Interpolant.prototype ), {

		constructor: DiscreteInterpolant,

		interpolate_: function ( i1 /*, t0, t, t1 */ ) {

			return this.copySampleValue_( i1 - 1 );

		}

	} );

	function KeyframeTrack( name, times, values, interpolation ) {

		if ( name === undefined ) throw new Error( 'THREE.KeyframeTrack: track name is undefined' );
		if ( times === undefined || times.length === 0 ) throw new Error( 'THREE.KeyframeTrack: no keyframes in track named ' + name );

		this.name = name;

		this.times = AnimationUtils.convertArray( times, this.TimeBufferType );
		this.values = AnimationUtils.convertArray( values, this.ValueBufferType );

		this.setInterpolation( interpolation || this.DefaultInterpolation );

	}

	// Static methods

	Object.assign( KeyframeTrack, {

		// Serialization (in static context, because of constructor invocation
		// and automatic invocation of .toJSON):

		toJSON: function ( track ) {

			const trackType = track.constructor;

			let json;

			// derived classes can define a static toJSON method
			if ( trackType.toJSON !== undefined ) {

				json = trackType.toJSON( track );

			} else {

				// by default, we assume the data can be serialized as-is
				json = {

					'name': track.name,
					'times': AnimationUtils.convertArray( track.times, Array ),
					'values': AnimationUtils.convertArray( track.values, Array )

				};

				const interpolation = track.getInterpolation();

				if ( interpolation !== track.DefaultInterpolation ) {

					json.interpolation = interpolation;

				}

			}

			json.type = track.ValueTypeName; // mandatory

			return json;

		}

	} );

	Object.assign( KeyframeTrack.prototype, {

		constructor: KeyframeTrack,

		TimeBufferType: Float32Array,

		ValueBufferType: Float32Array,

		DefaultInterpolation: InterpolateLinear,

		InterpolantFactoryMethodDiscrete: function ( result ) {

			return new DiscreteInterpolant( this.times, this.values, this.getValueSize(), result );

		},

		InterpolantFactoryMethodLinear: function ( result ) {

			return new LinearInterpolant( this.times, this.values, this.getValueSize(), result );

		},

		InterpolantFactoryMethodSmooth: function ( result ) {

			return new CubicInterpolant( this.times, this.values, this.getValueSize(), result );

		},

		setInterpolation: function ( interpolation ) {

			let factoryMethod;

			switch ( interpolation ) {

				case InterpolateDiscrete:

					factoryMethod = this.InterpolantFactoryMethodDiscrete;

					break;

				case InterpolateLinear:

					factoryMethod = this.InterpolantFactoryMethodLinear;

					break;

				case InterpolateSmooth:

					factoryMethod = this.InterpolantFactoryMethodSmooth;

					break;

			}

			if ( factoryMethod === undefined ) {

				const message = 'unsupported interpolation for ' +
					this.ValueTypeName + ' keyframe track named ' + this.name;

				if ( this.createInterpolant === undefined ) {

					// fall back to default, unless the default itself is messed up
					if ( interpolation !== this.DefaultInterpolation ) {

						this.setInterpolation( this.DefaultInterpolation );

					} else {

						throw new Error( message ); // fatal, in this case

					}

				}

				console.warn( 'THREE.KeyframeTrack:', message );
				return this;

			}

			this.createInterpolant = factoryMethod;

			return this;

		},

		getInterpolation: function () {

			switch ( this.createInterpolant ) {

				case this.InterpolantFactoryMethodDiscrete:

					return InterpolateDiscrete;

				case this.InterpolantFactoryMethodLinear:

					return InterpolateLinear;

				case this.InterpolantFactoryMethodSmooth:

					return InterpolateSmooth;

			}

		},

		getValueSize: function () {

			return this.values.length / this.times.length;

		},

		// move all keyframes either forwards or backwards in time
		shift: function ( timeOffset ) {

			if ( timeOffset !== 0.0 ) {

				const times = this.times;

				for ( let i = 0, n = times.length; i !== n; ++ i ) {

					times[ i ] += timeOffset;

				}

			}

			return this;

		},

		// scale all keyframe times by a factor (useful for frame <-> seconds conversions)
		scale: function ( timeScale ) {

			if ( timeScale !== 1.0 ) {

				const times = this.times;

				for ( let i = 0, n = times.length; i !== n; ++ i ) {

					times[ i ] *= timeScale;

				}

			}

			return this;

		},

		// removes keyframes before and after animation without changing any values within the range [startTime, endTime].
		// IMPORTANT: We do not shift around keys to the start of the track time, because for interpolated keys this will change their values
		trim: function ( startTime, endTime ) {

			const times = this.times,
				nKeys = times.length;

			let from = 0,
				to = nKeys - 1;

			while ( from !== nKeys && times[ from ] < startTime ) {

				++ from;

			}

			while ( to !== - 1 && times[ to ] > endTime ) {

				-- to;

			}

			++ to; // inclusive -> exclusive bound

			if ( from !== 0 || to !== nKeys ) {

				// empty tracks are forbidden, so keep at least one keyframe
				if ( from >= to ) {

					to = Math.max( to, 1 );
					from = to - 1;

				}

				const stride = this.getValueSize();
				this.times = AnimationUtils.arraySlice( times, from, to );
				this.values = AnimationUtils.arraySlice( this.values, from * stride, to * stride );

			}

			return this;

		},

		// ensure we do not get a GarbageInGarbageOut situation, make sure tracks are at least minimally viable
		validate: function () {

			let valid = true;

			const valueSize = this.getValueSize();
			if ( valueSize - Math.floor( valueSize ) !== 0 ) {

				console.error( 'THREE.KeyframeTrack: Invalid value size in track.', this );
				valid = false;

			}

			const times = this.times,
				values = this.values,

				nKeys = times.length;

			if ( nKeys === 0 ) {

				console.error( 'THREE.KeyframeTrack: Track is empty.', this );
				valid = false;

			}

			let prevTime = null;

			for ( let i = 0; i !== nKeys; i ++ ) {

				const currTime = times[ i ];

				if ( typeof currTime === 'number' && isNaN( currTime ) ) {

					console.error( 'THREE.KeyframeTrack: Time is not a valid number.', this, i, currTime );
					valid = false;
					break;

				}

				if ( prevTime !== null && prevTime > currTime ) {

					console.error( 'THREE.KeyframeTrack: Out of order keys.', this, i, currTime, prevTime );
					valid = false;
					break;

				}

				prevTime = currTime;

			}

			if ( values !== undefined ) {

				if ( AnimationUtils.isTypedArray( values ) ) {

					for ( let i = 0, n = values.length; i !== n; ++ i ) {

						const value = values[ i ];

						if ( isNaN( value ) ) {

							console.error( 'THREE.KeyframeTrack: Value is not a valid number.', this, i, value );
							valid = false;
							break;

						}

					}

				}

			}

			return valid;

		},

		// removes equivalent sequential keys as common in morph target sequences
		// (0,0,0,0,1,1,1,0,0,0,0,0,0,0) --> (0,0,1,1,0,0)
		optimize: function () {

			// times or values may be shared with other tracks, so overwriting is unsafe
			const times = AnimationUtils.arraySlice( this.times ),
				values = AnimationUtils.arraySlice( this.values ),
				stride = this.getValueSize(),

				smoothInterpolation = this.getInterpolation() === InterpolateSmooth,

				lastIndex = times.length - 1;

			let writeIndex = 1;

			for ( let i = 1; i < lastIndex; ++ i ) {

				let keep = false;

				const time = times[ i ];
				const timeNext = times[ i + 1 ];

				// remove adjacent keyframes scheduled at the same time

				if ( time !== timeNext && ( i !== 1 || time !== time[ 0 ] ) ) {

					if ( ! smoothInterpolation ) {

						// remove unnecessary keyframes same as their neighbors

						const offset = i * stride,
							offsetP = offset - stride,
							offsetN = offset + stride;

						for ( let j = 0; j !== stride; ++ j ) {

							const value = values[ offset + j ];

							if ( value !== values[ offsetP + j ] ||
								value !== values[ offsetN + j ] ) {

								keep = true;
								break;

							}

						}

					} else {

						keep = true;

					}

				}

				// in-place compaction

				if ( keep ) {

					if ( i !== writeIndex ) {

						times[ writeIndex ] = times[ i ];

						const readOffset = i * stride,
							writeOffset = writeIndex * stride;

						for ( let j = 0; j !== stride; ++ j ) {

							values[ writeOffset + j ] = values[ readOffset + j ];

						}

					}

					++ writeIndex;

				}

			}

			// flush last keyframe (compaction looks ahead)

			if ( lastIndex > 0 ) {

				times[ writeIndex ] = times[ lastIndex ];

				for ( let readOffset = lastIndex * stride, writeOffset = writeIndex * stride, j = 0; j !== stride; ++ j ) {

					values[ writeOffset + j ] = values[ readOffset + j ];

				}

				++ writeIndex;

			}

			if ( writeIndex !== times.length ) {

				this.times = AnimationUtils.arraySlice( times, 0, writeIndex );
				this.values = AnimationUtils.arraySlice( values, 0, writeIndex * stride );

			} else {

				this.times = times;
				this.values = values;

			}

			return this;

		},

		clone: function () {

			const times = AnimationUtils.arraySlice( this.times, 0 );
			const values = AnimationUtils.arraySlice( this.values, 0 );

			const TypedKeyframeTrack = this.constructor;
			const track = new TypedKeyframeTrack( this.name, times, values );

			// Interpolant argument to constructor is not saved, so copy the factory method directly.
			track.createInterpolant = this.createInterpolant;

			return track;

		}

	} );

	/**
	 * A Track of Boolean keyframe values.
	 */

	function BooleanKeyframeTrack( name, times, values ) {

		KeyframeTrack.call( this, name, times, values );

	}

	BooleanKeyframeTrack.prototype = Object.assign( Object.create( KeyframeTrack.prototype ), {

		constructor: BooleanKeyframeTrack,

		ValueTypeName: 'bool',
		ValueBufferType: Array,

		DefaultInterpolation: InterpolateDiscrete,

		InterpolantFactoryMethodLinear: undefined,
		InterpolantFactoryMethodSmooth: undefined

		// Note: Actually this track could have a optimized / compressed
		// representation of a single value and a custom interpolant that
		// computes "firstValue ^ isOdd( index )".

	} );

	/**
	 * A Track of keyframe values that represent color.
	 */

	function ColorKeyframeTrack( name, times, values, interpolation ) {

		KeyframeTrack.call( this, name, times, values, interpolation );

	}

	ColorKeyframeTrack.prototype = Object.assign( Object.create( KeyframeTrack.prototype ), {

		constructor: ColorKeyframeTrack,

		ValueTypeName: 'color'

		// ValueBufferType is inherited

		// DefaultInterpolation is inherited

		// Note: Very basic implementation and nothing special yet.
		// However, this is the place for color space parameterization.

	} );

	/**
	 * A Track of numeric keyframe values.
	 */

	function NumberKeyframeTrack( name, times, values, interpolation ) {

		KeyframeTrack.call( this, name, times, values, interpolation );

	}

	NumberKeyframeTrack.prototype = Object.assign( Object.create( KeyframeTrack.prototype ), {

		constructor: NumberKeyframeTrack,

		ValueTypeName: 'number'

		// ValueBufferType is inherited

		// DefaultInterpolation is inherited

	} );

	/**
	 * Spherical linear unit quaternion interpolant.
	 */

	function QuaternionLinearInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) {

		Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer );

	}

	QuaternionLinearInterpolant.prototype = Object.assign( Object.create( Interpolant.prototype ), {

		constructor: QuaternionLinearInterpolant,

		interpolate_: function ( i1, t0, t, t1 ) {

			const result = this.resultBuffer,
				values = this.sampleValues,
				stride = this.valueSize,

				alpha = ( t - t0 ) / ( t1 - t0 );

			let offset = i1 * stride;

			for ( let end = offset + stride; offset !== end; offset += 4 ) {

				Quaternion.slerpFlat( result, 0, values, offset - stride, values, offset, alpha );

			}

			return result;

		}

	} );

	/**
	 * A Track of quaternion keyframe values.
	 */

	function QuaternionKeyframeTrack( name, times, values, interpolation ) {

		KeyframeTrack.call( this, name, times, values, interpolation );

	}

	QuaternionKeyframeTrack.prototype = Object.assign( Object.create( KeyframeTrack.prototype ), {

		constructor: QuaternionKeyframeTrack,

		ValueTypeName: 'quaternion',

		// ValueBufferType is inherited

		DefaultInterpolation: InterpolateLinear,

		InterpolantFactoryMethodLinear: function ( result ) {

			return new QuaternionLinearInterpolant( this.times, this.values, this.getValueSize(), result );

		},

		InterpolantFactoryMethodSmooth: undefined // not yet implemented

	} );

	/**
	 * A Track that interpolates Strings
	 */

	function StringKeyframeTrack( name, times, values, interpolation ) {

		KeyframeTrack.call( this, name, times, values, interpolation );

	}

	StringKeyframeTrack.prototype = Object.assign( Object.create( KeyframeTrack.prototype ), {

		constructor: StringKeyframeTrack,

		ValueTypeName: 'string',
		ValueBufferType: Array,

		DefaultInterpolation: InterpolateDiscrete,

		InterpolantFactoryMethodLinear: undefined,

		InterpolantFactoryMethodSmooth: undefined

	} );

	/**
	 * A Track of vectored keyframe values.
	 */

	function VectorKeyframeTrack( name, times, values, interpolation ) {

		KeyframeTrack.call( this, name, times, values, interpolation );

	}

	VectorKeyframeTrack.prototype = Object.assign( Object.create( KeyframeTrack.prototype ), {

		constructor: VectorKeyframeTrack,

		ValueTypeName: 'vector'

		// ValueBufferType is inherited

		// DefaultInterpolation is inherited

	} );

	function AnimationClip( name, duration = - 1, tracks, blendMode = NormalAnimationBlendMode ) {

		this.name = name;
		this.tracks = tracks;
		this.duration = duration;
		this.blendMode = blendMode;

		this.uuid = MathUtils.generateUUID();

		// this means it should figure out its duration by scanning the tracks
		if ( this.duration < 0 ) {

			this.resetDuration();

		}

	}

	function getTrackTypeForValueTypeName( typeName ) {

		switch ( typeName.toLowerCase() ) {

			case 'scalar':
			case 'double':
			case 'float':
			case 'number':
			case 'integer':

				return NumberKeyframeTrack;

			case 'vector':
			case 'vector2':
			case 'vector3':
			case 'vector4':

				return VectorKeyframeTrack;

			case 'color':

				return ColorKeyframeTrack;

			case 'quaternion':

				return QuaternionKeyframeTrack;

			case 'bool':
			case 'boolean':

				return BooleanKeyframeTrack;

			case 'string':

				return StringKeyframeTrack;

		}

		throw new Error( 'THREE.KeyframeTrack: Unsupported typeName: ' + typeName );

	}

	function parseKeyframeTrack( json ) {

		if ( json.type === undefined ) {

			throw new Error( 'THREE.KeyframeTrack: track type undefined, can not parse' );

		}

		const trackType = getTrackTypeForValueTypeName( json.type );

		if ( json.times === undefined ) {

			const times = [], values = [];

			AnimationUtils.flattenJSON( json.keys, times, values, 'value' );

			json.times = times;
			json.values = values;

		}

		// derived classes can define a static parse method
		if ( trackType.parse !== undefined ) {

			return trackType.parse( json );

		} else {

			// by default, we assume a constructor compatible with the base
			return new trackType( json.name, json.times, json.values, json.interpolation );

		}

	}

	Object.assign( AnimationClip, {

		parse: function ( json ) {

			const tracks = [],
				jsonTracks = json.tracks,
				frameTime = 1.0 / ( json.fps || 1.0 );

			for ( let i = 0, n = jsonTracks.length; i !== n; ++ i ) {

				tracks.push( parseKeyframeTrack( jsonTracks[ i ] ).scale( frameTime ) );

			}

			const clip = new AnimationClip( json.name, json.duration, tracks, json.blendMode );
			clip.uuid = json.uuid;

			return clip;

		},

		toJSON: function ( clip ) {

			const tracks = [],
				clipTracks = clip.tracks;

			const json = {

				'name': clip.name,
				'duration': clip.duration,
				'tracks': tracks,
				'uuid': clip.uuid,
				'blendMode': clip.blendMode

			};

			for ( let i = 0, n = clipTracks.length; i !== n; ++ i ) {

				tracks.push( KeyframeTrack.toJSON( clipTracks[ i ] ) );

			}

			return json;

		},

		CreateFromMorphTargetSequence: function ( name, morphTargetSequence, fps, noLoop ) {

			const numMorphTargets = morphTargetSequence.length;
			const tracks = [];

			for ( let i = 0; i < numMorphTargets; i ++ ) {

				let times = [];
				let values = [];

				times.push(
					( i + numMorphTargets - 1 ) % numMorphTargets,
					i,
					( i + 1 ) % numMorphTargets );

				values.push( 0, 1, 0 );

				const order = AnimationUtils.getKeyframeOrder( times );
				times = AnimationUtils.sortedArray( times, 1, order );
				values = AnimationUtils.sortedArray( values, 1, order );

				// if there is a key at the first frame, duplicate it as the
				// last frame as well for perfect loop.
				if ( ! noLoop && times[ 0 ] === 0 ) {

					times.push( numMorphTargets );
					values.push( values[ 0 ] );

				}

				tracks.push(
					new NumberKeyframeTrack(
						'.morphTargetInfluences[' + morphTargetSequence[ i ].name + ']',
						times, values
					).scale( 1.0 / fps ) );

			}

			return new AnimationClip( name, - 1, tracks );

		},

		findByName: function ( objectOrClipArray, name ) {

			let clipArray = objectOrClipArray;

			if ( ! Array.isArray( objectOrClipArray ) ) {

				const o = objectOrClipArray;
				clipArray = o.geometry && o.geometry.animations || o.animations;

			}

			for ( let i = 0; i < clipArray.length; i ++ ) {

				if ( clipArray[ i ].name === name ) {

					return clipArray[ i ];

				}

			}

			return null;

		},

		CreateClipsFromMorphTargetSequences: function ( morphTargets, fps, noLoop ) {

			const animationToMorphTargets = {};

			// tested with https://regex101.com/ on trick sequences
			// such flamingo_flyA_003, flamingo_run1_003, crdeath0059
			const pattern = /^([\w-]*?)([\d]+)$/;

			// sort morph target names into animation groups based
			// patterns like Walk_001, Walk_002, Run_001, Run_002
			for ( let i = 0, il = morphTargets.length; i < il; i ++ ) {

				const morphTarget = morphTargets[ i ];
				const parts = morphTarget.name.match( pattern );

				if ( parts && parts.length > 1 ) {

					const name = parts[ 1 ];

					let animationMorphTargets = animationToMorphTargets[ name ];

					if ( ! animationMorphTargets ) {

						animationToMorphTargets[ name ] = animationMorphTargets = [];

					}

					animationMorphTargets.push( morphTarget );

				}

			}

			const clips = [];

			for ( const name in animationToMorphTargets ) {

				clips.push( AnimationClip.CreateFromMorphTargetSequence( name, animationToMorphTargets[ name ], fps, noLoop ) );

			}

			return clips;

		},

		// parse the animation.hierarchy format
		parseAnimation: function ( animation, bones ) {

			if ( ! animation ) {

				console.error( 'THREE.AnimationClip: No animation in JSONLoader data.' );
				return null;

			}

			const addNonemptyTrack = function ( trackType, trackName, animationKeys, propertyName, destTracks ) {

				// only return track if there are actually keys.
				if ( animationKeys.length !== 0 ) {

					const times = [];
					const values = [];

					AnimationUtils.flattenJSON( animationKeys, times, values, propertyName );

					// empty keys are filtered out, so check again
					if ( times.length !== 0 ) {

						destTracks.push( new trackType( trackName, times, values ) );

					}

				}

			};

			const tracks = [];

			const clipName = animation.name || 'default';
			const fps = animation.fps || 30;
			const blendMode = animation.blendMode;

			// automatic length determination in AnimationClip.
			let duration = animation.length || - 1;

			const hierarchyTracks = animation.hierarchy || [];

			for ( let h = 0; h < hierarchyTracks.length; h ++ ) {

				const animationKeys = hierarchyTracks[ h ].keys;

				// skip empty tracks
				if ( ! animationKeys || animationKeys.length === 0 ) continue;

				// process morph targets
				if ( animationKeys[ 0 ].morphTargets ) {

					// figure out all morph targets used in this track
					const morphTargetNames = {};

					let k;

					for ( k = 0; k < animationKeys.length; k ++ ) {

						if ( animationKeys[ k ].morphTargets ) {

							for ( let m = 0; m < animationKeys[ k ].morphTargets.length; m ++ ) {

								morphTargetNames[ animationKeys[ k ].morphTargets[ m ] ] = - 1;

							}

						}

					}

					// create a track for each morph target with all zero
					// morphTargetInfluences except for the keys in which
					// the morphTarget is named.
					for ( const morphTargetName in morphTargetNames ) {

						const times = [];
						const values = [];

						for ( let m = 0; m !== animationKeys[ k ].morphTargets.length; ++ m ) {

							const animationKey = animationKeys[ k ];

							times.push( animationKey.time );
							values.push( ( animationKey.morphTarget === morphTargetName ) ? 1 : 0 );

						}

						tracks.push( new NumberKeyframeTrack( '.morphTargetInfluence[' + morphTargetName + ']', times, values ) );

					}

					duration = morphTargetNames.length * ( fps || 1.0 );

				} else {

					// ...assume skeletal animation

					const boneName = '.bones[' + bones[ h ].name + ']';

					addNonemptyTrack(
						VectorKeyframeTrack, boneName + '.position',
						animationKeys, 'pos', tracks );

					addNonemptyTrack(
						QuaternionKeyframeTrack, boneName + '.quaternion',
						animationKeys, 'rot', tracks );

					addNonemptyTrack(
						VectorKeyframeTrack, boneName + '.scale',
						animationKeys, 'scl', tracks );

				}

			}

			if ( tracks.length === 0 ) {

				return null;

			}

			const clip = new AnimationClip( clipName, duration, tracks, blendMode );

			return clip;

		}

	} );

	Object.assign( AnimationClip.prototype, {

		resetDuration: function () {

			const tracks = this.tracks;
			let duration = 0;

			for ( let i = 0, n = tracks.length; i !== n; ++ i ) {

				const track = this.tracks[ i ];

				duration = Math.max( duration, track.times[ track.times.length - 1 ] );

			}

			this.duration = duration;

			return this;

		},

		trim: function () {

			for ( let i = 0; i < this.tracks.length; i ++ ) {

				this.tracks[ i ].trim( 0, this.duration );

			}

			return this;

		},

		validate: function () {

			let valid = true;

			for ( let i = 0; i < this.tracks.length; i ++ ) {

				valid = valid && this.tracks[ i ].validate();

			}

			return valid;

		},

		optimize: function () {

			for ( let i = 0; i < this.tracks.length; i ++ ) {

				this.tracks[ i ].optimize();

			}

			return this;

		},

		clone: function () {

			const tracks = [];

			for ( let i = 0; i < this.tracks.length; i ++ ) {

				tracks.push( this.tracks[ i ].clone() );

			}

			return new AnimationClip( this.name, this.duration, tracks, this.blendMode );

		},

		toJSON: function () {

			return AnimationClip.toJSON( this );

		}

	} );

	const Cache = {

		enabled: false,

		files: {},

		add: function ( key, file ) {

			if ( this.enabled === false ) return;

			// console.log( 'THREE.Cache', 'Adding key:', key );

			this.files[ key ] = file;

		},

		get: function ( key ) {

			if ( this.enabled === false ) return;

			// console.log( 'THREE.Cache', 'Checking key:', key );

			return this.files[ key ];

		},

		remove: function ( key ) {

			delete this.files[ key ];

		},

		clear: function () {

			this.files = {};

		}

	};

	function LoadingManager( onLoad, onProgress, onError ) {

		const scope = this;

		let isLoading = false;
		let itemsLoaded = 0;
		let itemsTotal = 0;
		let urlModifier = undefined;
		const handlers = [];

		// Refer to #5689 for the reason why we don't set .onStart
		// in the constructor

		this.onStart = undefined;
		this.onLoad = onLoad;
		this.onProgress = onProgress;
		this.onError = onError;

		this.itemStart = function ( url ) {

			itemsTotal ++;

			if ( isLoading === false ) {

				if ( scope.onStart !== undefined ) {

					scope.onStart( url, itemsLoaded, itemsTotal );

				}

			}

			isLoading = true;

		};

		this.itemEnd = function ( url ) {

			itemsLoaded ++;

			if ( scope.onProgress !== undefined ) {

				scope.onProgress( url, itemsLoaded, itemsTotal );

			}

			if ( itemsLoaded === itemsTotal ) {

				isLoading = false;

				if ( scope.onLoad !== undefined ) {

					scope.onLoad();

				}

			}

		};

		this.itemError = function ( url ) {

			if ( scope.onError !== undefined ) {

				scope.onError( url );

			}

		};

		this.resolveURL = function ( url ) {

			if ( urlModifier ) {

				return urlModifier( url );

			}

			return url;

		};

		this.setURLModifier = function ( transform ) {

			urlModifier = transform;

			return this;

		};

		this.addHandler = function ( regex, loader ) {

			handlers.push( regex, loader );

			return this;

		};

		this.removeHandler = function ( regex ) {

			const index = handlers.indexOf( regex );

			if ( index !== - 1 ) {

				handlers.splice( index, 2 );

			}

			return this;

		};

		this.getHandler = function ( file ) {

			for ( let i = 0, l = handlers.length; i < l; i += 2 ) {

				const regex = handlers[ i ];
				const loader = handlers[ i + 1 ];

				if ( regex.global ) regex.lastIndex = 0; // see #17920

				if ( regex.test( file ) ) {

					return loader;

				}

			}

			return null;

		};

	}

	const DefaultLoadingManager = new LoadingManager();

	function Loader( manager ) {

		this.manager = ( manager !== undefined ) ? manager : DefaultLoadingManager;

		this.crossOrigin = 'anonymous';
		this.withCredentials = false;
		this.path = '';
		this.resourcePath = '';
		this.requestHeader = {};

	}

	Object.assign( Loader.prototype, {

		load: function ( /* url, onLoad, onProgress, onError */ ) {},

		loadAsync: function ( url, onProgress ) {

			const scope = this;

			return new Promise( function ( resolve, reject ) {

				scope.load( url, resolve, onProgress, reject );

			} );

		},

		parse: function ( /* data */ ) {},

		setCrossOrigin: function ( crossOrigin ) {

			this.crossOrigin = crossOrigin;
			return this;

		},

		setWithCredentials: function ( value ) {

			this.withCredentials = value;
			return this;

		},

		setPath: function ( path ) {

			this.path = path;
			return this;

		},

		setResourcePath: function ( resourcePath ) {

			this.resourcePath = resourcePath;
			return this;

		},

		setRequestHeader: function ( requestHeader ) {

			this.requestHeader = requestHeader;
			return this;

		}

	} );

	const loading = {};

	function FileLoader( manager ) {

		Loader.call( this, manager );

	}

	FileLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: FileLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			if ( url === undefined ) url = '';

			if ( this.path !== undefined ) url = this.path + url;

			url = this.manager.resolveURL( url );

			const scope = this;

			const cached = Cache.get( url );

			if ( cached !== undefined ) {

				scope.manager.itemStart( url );

				setTimeout( function () {

					if ( onLoad ) onLoad( cached );

					scope.manager.itemEnd( url );

				}, 0 );

				return cached;

			}

			// Check if request is duplicate

			if ( loading[ url ] !== undefined ) {

				loading[ url ].push( {

					onLoad: onLoad,
					onProgress: onProgress,
					onError: onError

				} );

				return;

			}

			// Check for data: URI
			const dataUriRegex = /^data:(.*?)(;base64)?,(.*)$/;
			const dataUriRegexResult = url.match( dataUriRegex );
			let request;

			// Safari can not handle Data URIs through XMLHttpRequest so process manually
			if ( dataUriRegexResult ) {

				const mimeType = dataUriRegexResult[ 1 ];
				const isBase64 = !! dataUriRegexResult[ 2 ];

				let data = dataUriRegexResult[ 3 ];
				data = decodeURIComponent( data );

				if ( isBase64 ) data = atob( data );

				try {

					let response;
					const responseType = ( this.responseType || '' ).toLowerCase();

					switch ( responseType ) {

						case 'arraybuffer':
						case 'blob':

							const view = new Uint8Array( data.length );

							for ( let i = 0; i < data.length; i ++ ) {

								view[ i ] = data.charCodeAt( i );

							}

							if ( responseType === 'blob' ) {

								response = new Blob( [ view.buffer ], { type: mimeType } );

							} else {

								response = view.buffer;

							}

							break;

						case 'document':

							const parser = new DOMParser();
							response = parser.parseFromString( data, mimeType );

							break;

						case 'json':

							response = JSON.parse( data );

							break;

						default: // 'text' or other

							response = data;

							break;

					}

					// Wait for next browser tick like standard XMLHttpRequest event dispatching does
					setTimeout( function () {

						if ( onLoad ) onLoad( response );

						scope.manager.itemEnd( url );

					}, 0 );

				} catch ( error ) {

					// Wait for next browser tick like standard XMLHttpRequest event dispatching does
					setTimeout( function () {

						if ( onError ) onError( error );

						scope.manager.itemError( url );
						scope.manager.itemEnd( url );

					}, 0 );

				}

			} else {

				// Initialise array for duplicate requests

				loading[ url ] = [];

				loading[ url ].push( {

					onLoad: onLoad,
					onProgress: onProgress,
					onError: onError

				} );

				request = new XMLHttpRequest();

				request.open( 'GET', url, true );

				request.addEventListener( 'load', function ( event ) {

					const response = this.response;

					const callbacks = loading[ url ];

					delete loading[ url ];

					if ( this.status === 200 || this.status === 0 ) {

						// Some browsers return HTTP Status 0 when using non-http protocol
						// e.g. 'file://' or 'data://'. Handle as success.

						if ( this.status === 0 ) console.warn( 'THREE.FileLoader: HTTP Status 0 received.' );

						// Add to cache only on HTTP success, so that we do not cache
						// error response bodies as proper responses to requests.
						Cache.add( url, response );

						for ( let i = 0, il = callbacks.length; i < il; i ++ ) {

							const callback = callbacks[ i ];
							if ( callback.onLoad ) callback.onLoad( response );

						}

						scope.manager.itemEnd( url );

					} else {

						for ( let i = 0, il = callbacks.length; i < il; i ++ ) {

							const callback = callbacks[ i ];
							if ( callback.onError ) callback.onError( event );

						}

						scope.manager.itemError( url );
						scope.manager.itemEnd( url );

					}

				}, false );

				request.addEventListener( 'progress', function ( event ) {

					const callbacks = loading[ url ];

					for ( let i = 0, il = callbacks.length; i < il; i ++ ) {

						const callback = callbacks[ i ];
						if ( callback.onProgress ) callback.onProgress( event );

					}

				}, false );

				request.addEventListener( 'error', function ( event ) {

					const callbacks = loading[ url ];

					delete loading[ url ];

					for ( let i = 0, il = callbacks.length; i < il; i ++ ) {

						const callback = callbacks[ i ];
						if ( callback.onError ) callback.onError( event );

					}

					scope.manager.itemError( url );
					scope.manager.itemEnd( url );

				}, false );

				request.addEventListener( 'abort', function ( event ) {

					const callbacks = loading[ url ];

					delete loading[ url ];

					for ( let i = 0, il = callbacks.length; i < il; i ++ ) {

						const callback = callbacks[ i ];
						if ( callback.onError ) callback.onError( event );

					}

					scope.manager.itemError( url );
					scope.manager.itemEnd( url );

				}, false );

				if ( this.responseType !== undefined ) request.responseType = this.responseType;
				if ( this.withCredentials !== undefined ) request.withCredentials = this.withCredentials;

				if ( request.overrideMimeType ) request.overrideMimeType( this.mimeType !== undefined ? this.mimeType : 'text/plain' );

				for ( const header in this.requestHeader ) {

					request.setRequestHeader( header, this.requestHeader[ header ] );

				}

				request.send( null );

			}

			scope.manager.itemStart( url );

			return request;

		},

		setResponseType: function ( value ) {

			this.responseType = value;
			return this;

		},

		setMimeType: function ( value ) {

			this.mimeType = value;
			return this;

		}

	} );

	function AnimationLoader( manager ) {

		Loader.call( this, manager );

	}

	AnimationLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: AnimationLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			const scope = this;

			const loader = new FileLoader( scope.manager );
			loader.setPath( scope.path );
			loader.setRequestHeader( scope.requestHeader );
			loader.setWithCredentials( scope.withCredentials );
			loader.load( url, function ( text ) {

				try {

					onLoad( scope.parse( JSON.parse( text ) ) );

				} catch ( e ) {

					if ( onError ) {

						onError( e );

					} else {

						console.error( e );

					}

					scope.manager.itemError( url );

				}

			}, onProgress, onError );

		},

		parse: function ( json ) {

			const animations = [];

			for ( let i = 0; i < json.length; i ++ ) {

				const clip = AnimationClip.parse( json[ i ] );

				animations.push( clip );

			}

			return animations;

		}

	} );

	/**
	 * Abstract Base class to block based textures loader (dds, pvr, ...)
	 *
	 * Sub classes have to implement the parse() method which will be used in load().
	 */

	function CompressedTextureLoader( manager ) {

		Loader.call( this, manager );

	}

	CompressedTextureLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: CompressedTextureLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			const scope = this;

			const images = [];

			const texture = new CompressedTexture();

			const loader = new FileLoader( this.manager );
			loader.setPath( this.path );
			loader.setResponseType( 'arraybuffer' );
			loader.setRequestHeader( this.requestHeader );
			loader.setWithCredentials( scope.withCredentials );

			let loaded = 0;

			function loadTexture( i ) {

				loader.load( url[ i ], function ( buffer ) {

					const texDatas = scope.parse( buffer, true );

					images[ i ] = {
						width: texDatas.width,
						height: texDatas.height,
						format: texDatas.format,
						mipmaps: texDatas.mipmaps
					};

					loaded += 1;

					if ( loaded === 6 ) {

						if ( texDatas.mipmapCount === 1 ) texture.minFilter = LinearFilter;

						texture.image = images;
						texture.format = texDatas.format;
						texture.needsUpdate = true;

						if ( onLoad ) onLoad( texture );

					}

				}, onProgress, onError );

			}

			if ( Array.isArray( url ) ) {

				for ( let i = 0, il = url.length; i < il; ++ i ) {

					loadTexture( i );

				}

			} else {

				// compressed cubemap texture stored in a single DDS file

				loader.load( url, function ( buffer ) {

					const texDatas = scope.parse( buffer, true );

					if ( texDatas.isCubemap ) {

						const faces = texDatas.mipmaps.length / texDatas.mipmapCount;

						for ( let f = 0; f < faces; f ++ ) {

							images[ f ] = { mipmaps: [] };

							for ( let i = 0; i < texDatas.mipmapCount; i ++ ) {

								images[ f ].mipmaps.push( texDatas.mipmaps[ f * texDatas.mipmapCount + i ] );
								images[ f ].format = texDatas.format;
								images[ f ].width = texDatas.width;
								images[ f ].height = texDatas.height;

							}

						}

						texture.image = images;

					} else {

						texture.image.width = texDatas.width;
						texture.image.height = texDatas.height;
						texture.mipmaps = texDatas.mipmaps;

					}

					if ( texDatas.mipmapCount === 1 ) {

						texture.minFilter = LinearFilter;

					}

					texture.format = texDatas.format;
					texture.needsUpdate = true;

					if ( onLoad ) onLoad( texture );

				}, onProgress, onError );

			}

			return texture;

		}

	} );

	function ImageLoader( manager ) {

		Loader.call( this, manager );

	}

	ImageLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: ImageLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			if ( this.path !== undefined ) url = this.path + url;

			url = this.manager.resolveURL( url );

			const scope = this;

			const cached = Cache.get( url );

			if ( cached !== undefined ) {

				scope.manager.itemStart( url );

				setTimeout( function () {

					if ( onLoad ) onLoad( cached );

					scope.manager.itemEnd( url );

				}, 0 );

				return cached;

			}

			const image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );

			function onImageLoad() {

				image.removeEventListener( 'load', onImageLoad, false );
				image.removeEventListener( 'error', onImageError, false );

				Cache.add( url, this );

				if ( onLoad ) onLoad( this );

				scope.manager.itemEnd( url );

			}

			function onImageError( event ) {

				image.removeEventListener( 'load', onImageLoad, false );
				image.removeEventListener( 'error', onImageError, false );

				if ( onError ) onError( event );

				scope.manager.itemError( url );
				scope.manager.itemEnd( url );

			}

			image.addEventListener( 'load', onImageLoad, false );
			image.addEventListener( 'error', onImageError, false );

			if ( url.substr( 0, 5 ) !== 'data:' ) {

				if ( this.crossOrigin !== undefined ) image.crossOrigin = this.crossOrigin;

			}

			scope.manager.itemStart( url );

			image.src = url;

			return image;

		}

	} );

	function CubeTextureLoader( manager ) {

		Loader.call( this, manager );

	}

	CubeTextureLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: CubeTextureLoader,

		load: function ( urls, onLoad, onProgress, onError ) {

			const texture = new CubeTexture();

			const loader = new ImageLoader( this.manager );
			loader.setCrossOrigin( this.crossOrigin );
			loader.setPath( this.path );

			let loaded = 0;

			function loadTexture( i ) {

				loader.load( urls[ i ], function ( image ) {

					texture.images[ i ] = image;

					loaded ++;

					if ( loaded === 6 ) {

						texture.needsUpdate = true;

						if ( onLoad ) onLoad( texture );

					}

				}, undefined, onError );

			}

			for ( let i = 0; i < urls.length; ++ i ) {

				loadTexture( i );

			}

			return texture;

		}

	} );

	/**
	 * Abstract Base class to load generic binary textures formats (rgbe, hdr, ...)
	 *
	 * Sub classes have to implement the parse() method which will be used in load().
	 */

	function DataTextureLoader( manager ) {

		Loader.call( this, manager );

	}

	DataTextureLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: DataTextureLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			const scope = this;

			const texture = new DataTexture();

			const loader = new FileLoader( this.manager );
			loader.setResponseType( 'arraybuffer' );
			loader.setRequestHeader( this.requestHeader );
			loader.setPath( this.path );
			loader.setWithCredentials( scope.withCredentials );
			loader.load( url, function ( buffer ) {

				const texData = scope.parse( buffer );

				if ( ! texData ) return;

				if ( texData.image !== undefined ) {

					texture.image = texData.image;

				} else if ( texData.data !== undefined ) {

					texture.image.width = texData.width;
					texture.image.height = texData.height;
					texture.image.data = texData.data;

				}

				texture.wrapS = texData.wrapS !== undefined ? texData.wrapS : ClampToEdgeWrapping;
				texture.wrapT = texData.wrapT !== undefined ? texData.wrapT : ClampToEdgeWrapping;

				texture.magFilter = texData.magFilter !== undefined ? texData.magFilter : LinearFilter;
				texture.minFilter = texData.minFilter !== undefined ? texData.minFilter : LinearFilter;

				texture.anisotropy = texData.anisotropy !== undefined ? texData.anisotropy : 1;

				if ( texData.format !== undefined ) {

					texture.format = texData.format;

				}

				if ( texData.type !== undefined ) {

					texture.type = texData.type;

				}

				if ( texData.mipmaps !== undefined ) {

					texture.mipmaps = texData.mipmaps;
					texture.minFilter = LinearMipmapLinearFilter; // presumably...

				}

				if ( texData.mipmapCount === 1 ) {

					texture.minFilter = LinearFilter;

				}

				texture.needsUpdate = true;

				if ( onLoad ) onLoad( texture, texData );

			}, onProgress, onError );


			return texture;

		}

	} );

	function TextureLoader( manager ) {

		Loader.call( this, manager );

	}

	TextureLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: TextureLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			const texture = new Texture();

			const loader = new ImageLoader( this.manager );
			loader.setCrossOrigin( this.crossOrigin );
			loader.setPath( this.path );

			loader.load( url, function ( image ) {

				texture.image = image;

				// JPEGs can't have an alpha channel, so memory can be saved by storing them as RGB.
				const isJPEG = url.search( /\.jpe?g($|\?)/i ) > 0 || url.search( /^data\:image\/jpeg/ ) === 0;

				texture.format = isJPEG ? RGBFormat : RGBAFormat;
				texture.needsUpdate = true;

				if ( onLoad !== undefined ) {

					onLoad( texture );

				}

			}, onProgress, onError );

			return texture;

		}

	} );

	/**
	 * Extensible curve object.
	 *
	 * Some common of curve methods:
	 * .getPoint( t, optionalTarget ), .getTangent( t, optionalTarget )
	 * .getPointAt( u, optionalTarget ), .getTangentAt( u, optionalTarget )
	 * .getPoints(), .getSpacedPoints()
	 * .getLength()
	 * .updateArcLengths()
	 *
	 * This following curves inherit from THREE.Curve:
	 *
	 * -- 2D curves --
	 * THREE.ArcCurve
	 * THREE.CubicBezierCurve
	 * THREE.EllipseCurve
	 * THREE.LineCurve
	 * THREE.QuadraticBezierCurve
	 * THREE.SplineCurve
	 *
	 * -- 3D curves --
	 * THREE.CatmullRomCurve3
	 * THREE.CubicBezierCurve3
	 * THREE.LineCurve3
	 * THREE.QuadraticBezierCurve3
	 *
	 * A series of curves can be represented as a THREE.CurvePath.
	 *
	 **/

	function Curve() {

		this.type = 'Curve';

		this.arcLengthDivisions = 200;

	}

	Object.assign( Curve.prototype, {

		// Virtual base class method to overwrite and implement in subclasses
		//	- t [0 .. 1]

		getPoint: function ( /* t, optionalTarget */ ) {

			console.warn( 'THREE.Curve: .getPoint() not implemented.' );
			return null;

		},

		// Get point at relative position in curve according to arc length
		// - u [0 .. 1]

		getPointAt: function ( u, optionalTarget ) {

			const t = this.getUtoTmapping( u );
			return this.getPoint( t, optionalTarget );

		},

		// Get sequence of points using getPoint( t )

		getPoints: function ( divisions = 5 ) {

			const points = [];

			for ( let d = 0; d <= divisions; d ++ ) {

				points.push( this.getPoint( d / divisions ) );

			}

			return points;

		},

		// Get sequence of points using getPointAt( u )

		getSpacedPoints: function ( divisions = 5 ) {

			const points = [];

			for ( let d = 0; d <= divisions; d ++ ) {

				points.push( this.getPointAt( d / divisions ) );

			}

			return points;

		},

		// Get total curve arc length

		getLength: function () {

			const lengths = this.getLengths();
			return lengths[ lengths.length - 1 ];

		},

		// Get list of cumulative segment lengths

		getLengths: function ( divisions ) {

			if ( divisions === undefined ) divisions = this.arcLengthDivisions;

			if ( this.cacheArcLengths &&
				( this.cacheArcLengths.length === divisions + 1 ) &&
				! this.needsUpdate ) {

				return this.cacheArcLengths;

			}

			this.needsUpdate = false;

			const cache = [];
			let current, last = this.getPoint( 0 );
			let sum = 0;

			cache.push( 0 );

			for ( let p = 1; p <= divisions; p ++ ) {

				current = this.getPoint( p / divisions );
				sum += current.distanceTo( last );
				cache.push( sum );
				last = current;

			}

			this.cacheArcLengths = cache;

			return cache; // { sums: cache, sum: sum }; Sum is in the last element.

		},

		updateArcLengths: function () {

			this.needsUpdate = true;
			this.getLengths();

		},

		// Given u ( 0 .. 1 ), get a t to find p. This gives you points which are equidistant

		getUtoTmapping: function ( u, distance ) {

			const arcLengths = this.getLengths();

			let i = 0;
			const il = arcLengths.length;

			let targetArcLength; // The targeted u distance value to get

			if ( distance ) {

				targetArcLength = distance;

			} else {

				targetArcLength = u * arcLengths[ il - 1 ];

			}

			// binary search for the index with largest value smaller than target u distance

			let low = 0, high = il - 1, comparison;

			while ( low <= high ) {

				i = Math.floor( low + ( high - low ) / 2 ); // less likely to overflow, though probably not issue here, JS doesn't really have integers, all numbers are floats

				comparison = arcLengths[ i ] - targetArcLength;

				if ( comparison < 0 ) {

					low = i + 1;

				} else if ( comparison > 0 ) {

					high = i - 1;

				} else {

					high = i;
					break;

					// DONE

				}

			}

			i = high;

			if ( arcLengths[ i ] === targetArcLength ) {

				return i / ( il - 1 );

			}

			// we could get finer grain at lengths, or use simple interpolation between two points

			const lengthBefore = arcLengths[ i ];
			const lengthAfter = arcLengths[ i + 1 ];

			const segmentLength = lengthAfter - lengthBefore;

			// determine where we are between the 'before' and 'after' points

			const segmentFraction = ( targetArcLength - lengthBefore ) / segmentLength;

			// add that fractional amount to t

			const t = ( i + segmentFraction ) / ( il - 1 );

			return t;

		},

		// Returns a unit vector tangent at t
		// In case any sub curve does not implement its tangent derivation,
		// 2 points a small delta apart will be used to find its gradient
		// which seems to give a reasonable approximation

		getTangent: function ( t, optionalTarget ) {

			const delta = 0.0001;
			let t1 = t - delta;
			let t2 = t + delta;

			// Capping in case of danger

			if ( t1 < 0 ) t1 = 0;
			if ( t2 > 1 ) t2 = 1;

			const pt1 = this.getPoint( t1 );
			const pt2 = this.getPoint( t2 );

			const tangent = optionalTarget || ( ( pt1.isVector2 ) ? new Vector2() : new Vector3() );

			tangent.copy( pt2 ).sub( pt1 ).normalize();

			return tangent;

		},

		getTangentAt: function ( u, optionalTarget ) {

			const t = this.getUtoTmapping( u );
			return this.getTangent( t, optionalTarget );

		},

		computeFrenetFrames: function ( segments, closed ) {

			// see http://www.cs.indiana.edu/pub/techreports/TR425.pdf

			const normal = new Vector3();

			const tangents = [];
			const normals = [];
			const binormals = [];

			const vec = new Vector3();
			const mat = new Matrix4();

			// compute the tangent vectors for each segment on the curve

			for ( let i = 0; i <= segments; i ++ ) {

				const u = i / segments;

				tangents[ i ] = this.getTangentAt( u, new Vector3() );
				tangents[ i ].normalize();

			}

			// select an initial normal vector perpendicular to the first tangent vector,
			// and in the direction of the minimum tangent xyz component

			normals[ 0 ] = new Vector3();
			binormals[ 0 ] = new Vector3();
			let min = Number.MAX_VALUE;
			const tx = Math.abs( tangents[ 0 ].x );
			const ty = Math.abs( tangents[ 0 ].y );
			const tz = Math.abs( tangents[ 0 ].z );

			if ( tx <= min ) {

				min = tx;
				normal.set( 1, 0, 0 );

			}

			if ( ty <= min ) {

				min = ty;
				normal.set( 0, 1, 0 );

			}

			if ( tz <= min ) {

				normal.set( 0, 0, 1 );

			}

			vec.crossVectors( tangents[ 0 ], normal ).normalize();

			normals[ 0 ].crossVectors( tangents[ 0 ], vec );
			binormals[ 0 ].crossVectors( tangents[ 0 ], normals[ 0 ] );


			// compute the slowly-varying normal and binormal vectors for each segment on the curve

			for ( let i = 1; i <= segments; i ++ ) {

				normals[ i ] = normals[ i - 1 ].clone();

				binormals[ i ] = binormals[ i - 1 ].clone();

				vec.crossVectors( tangents[ i - 1 ], tangents[ i ] );

				if ( vec.length() > Number.EPSILON ) {

					vec.normalize();

					const theta = Math.acos( MathUtils.clamp( tangents[ i - 1 ].dot( tangents[ i ] ), - 1, 1 ) ); // clamp for floating pt errors

					normals[ i ].applyMatrix4( mat.makeRotationAxis( vec, theta ) );

				}

				binormals[ i ].crossVectors( tangents[ i ], normals[ i ] );

			}

			// if the curve is closed, postprocess the vectors so the first and last normal vectors are the same

			if ( closed === true ) {

				let theta = Math.acos( MathUtils.clamp( normals[ 0 ].dot( normals[ segments ] ), - 1, 1 ) );
				theta /= segments;

				if ( tangents[ 0 ].dot( vec.crossVectors( normals[ 0 ], normals[ segments ] ) ) > 0 ) {

					theta = - theta;

				}

				for ( let i = 1; i <= segments; i ++ ) {

					// twist a little...
					normals[ i ].applyMatrix4( mat.makeRotationAxis( tangents[ i ], theta * i ) );
					binormals[ i ].crossVectors( tangents[ i ], normals[ i ] );

				}

			}

			return {
				tangents: tangents,
				normals: normals,
				binormals: binormals
			};

		},

		clone: function () {

			return new this.constructor().copy( this );

		},

		copy: function ( source ) {

			this.arcLengthDivisions = source.arcLengthDivisions;

			return this;

		},

		toJSON: function () {

			const data = {
				metadata: {
					version: 4.5,
					type: 'Curve',
					generator: 'Curve.toJSON'
				}
			};

			data.arcLengthDivisions = this.arcLengthDivisions;
			data.type = this.type;

			return data;

		},

		fromJSON: function ( json ) {

			this.arcLengthDivisions = json.arcLengthDivisions;

			return this;

		}

	} );

	function EllipseCurve( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation ) {

		Curve.call( this );

		this.type = 'EllipseCurve';

		this.aX = aX || 0;
		this.aY = aY || 0;

		this.xRadius = xRadius || 1;
		this.yRadius = yRadius || 1;

		this.aStartAngle = aStartAngle || 0;
		this.aEndAngle = aEndAngle || 2 * Math.PI;

		this.aClockwise = aClockwise || false;

		this.aRotation = aRotation || 0;

	}

	EllipseCurve.prototype = Object.create( Curve.prototype );
	EllipseCurve.prototype.constructor = EllipseCurve;

	EllipseCurve.prototype.isEllipseCurve = true;

	EllipseCurve.prototype.getPoint = function ( t, optionalTarget ) {

		const point = optionalTarget || new Vector2();

		const twoPi = Math.PI * 2;
		let deltaAngle = this.aEndAngle - this.aStartAngle;
		const samePoints = Math.abs( deltaAngle ) < Number.EPSILON;

		// ensures that deltaAngle is 0 .. 2 PI
		while ( deltaAngle < 0 ) deltaAngle += twoPi;
		while ( deltaAngle > twoPi ) deltaAngle -= twoPi;

		if ( deltaAngle < Number.EPSILON ) {

			if ( samePoints ) {

				deltaAngle = 0;

			} else {

				deltaAngle = twoPi;

			}

		}

		if ( this.aClockwise === true && ! samePoints ) {

			if ( deltaAngle === twoPi ) {

				deltaAngle = - twoPi;

			} else {

				deltaAngle = deltaAngle - twoPi;

			}

		}

		const angle = this.aStartAngle + t * deltaAngle;
		let x = this.aX + this.xRadius * Math.cos( angle );
		let y = this.aY + this.yRadius * Math.sin( angle );

		if ( this.aRotation !== 0 ) {

			const cos = Math.cos( this.aRotation );
			const sin = Math.sin( this.aRotation );

			const tx = x - this.aX;
			const ty = y - this.aY;

			// Rotate the point about the center of the ellipse.
			x = tx * cos - ty * sin + this.aX;
			y = tx * sin + ty * cos + this.aY;

		}

		return point.set( x, y );

	};

	EllipseCurve.prototype.copy = function ( source ) {

		Curve.prototype.copy.call( this, source );

		this.aX = source.aX;
		this.aY = source.aY;

		this.xRadius = source.xRadius;
		this.yRadius = source.yRadius;

		this.aStartAngle = source.aStartAngle;
		this.aEndAngle = source.aEndAngle;

		this.aClockwise = source.aClockwise;

		this.aRotation = source.aRotation;

		return this;

	};


	EllipseCurve.prototype.toJSON = function () {

		const data = Curve.prototype.toJSON.call( this );

		data.aX = this.aX;
		data.aY = this.aY;

		data.xRadius = this.xRadius;
		data.yRadius = this.yRadius;

		data.aStartAngle = this.aStartAngle;
		data.aEndAngle = this.aEndAngle;

		data.aClockwise = this.aClockwise;

		data.aRotation = this.aRotation;

		return data;

	};

	EllipseCurve.prototype.fromJSON = function ( json ) {

		Curve.prototype.fromJSON.call( this, json );

		this.aX = json.aX;
		this.aY = json.aY;

		this.xRadius = json.xRadius;
		this.yRadius = json.yRadius;

		this.aStartAngle = json.aStartAngle;
		this.aEndAngle = json.aEndAngle;

		this.aClockwise = json.aClockwise;

		this.aRotation = json.aRotation;

		return this;

	};

	function ArcCurve( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {

		EllipseCurve.call( this, aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise );

		this.type = 'ArcCurve';

	}

	ArcCurve.prototype = Object.create( EllipseCurve.prototype );
	ArcCurve.prototype.constructor = ArcCurve;

	ArcCurve.prototype.isArcCurve = true;

	/**
	 * Centripetal CatmullRom Curve - which is useful for avoiding
	 * cusps and self-intersections in non-uniform catmull rom curves.
	 * http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf
	 *
	 * curve.type accepts centripetal(default), chordal and catmullrom
	 * curve.tension is used for catmullrom which defaults to 0.5
	 */


	/*
	Based on an optimized c++ solution in
	 - http://stackoverflow.com/questions/9489736/catmull-rom-curve-with-no-cusps-and-no-self-intersections/
	 - http://ideone.com/NoEbVM

	This CubicPoly class could be used for reusing some variables and calculations,
	but for three.js curve use, it could be possible inlined and flatten into a single function call
	which can be placed in CurveUtils.
	*/

	function CubicPoly() {

		let c0 = 0, c1 = 0, c2 = 0, c3 = 0;

		/*
		 * Compute coefficients for a cubic polynomial
		 *   p(s) = c0 + c1*s + c2*s^2 + c3*s^3
		 * such that
		 *   p(0) = x0, p(1) = x1
		 *  and
		 *   p'(0) = t0, p'(1) = t1.
		 */
		function init( x0, x1, t0, t1 ) {

			c0 = x0;
			c1 = t0;
			c2 = - 3 * x0 + 3 * x1 - 2 * t0 - t1;
			c3 = 2 * x0 - 2 * x1 + t0 + t1;

		}

		return {

			initCatmullRom: function ( x0, x1, x2, x3, tension ) {

				init( x1, x2, tension * ( x2 - x0 ), tension * ( x3 - x1 ) );

			},

			initNonuniformCatmullRom: function ( x0, x1, x2, x3, dt0, dt1, dt2 ) {

				// compute tangents when parameterized in [t1,t2]
				let t1 = ( x1 - x0 ) / dt0 - ( x2 - x0 ) / ( dt0 + dt1 ) + ( x2 - x1 ) / dt1;
				let t2 = ( x2 - x1 ) / dt1 - ( x3 - x1 ) / ( dt1 + dt2 ) + ( x3 - x2 ) / dt2;

				// rescale tangents for parametrization in [0,1]
				t1 *= dt1;
				t2 *= dt1;

				init( x1, x2, t1, t2 );

			},

			calc: function ( t ) {

				const t2 = t * t;
				const t3 = t2 * t;
				return c0 + c1 * t + c2 * t2 + c3 * t3;

			}

		};

	}

	//

	const tmp = new Vector3();
	const px = new CubicPoly(), py = new CubicPoly(), pz = new CubicPoly();

	function CatmullRomCurve3( points = [], closed = false, curveType = 'centripetal', tension = 0.5 ) {

		Curve.call( this );

		this.type = 'CatmullRomCurve3';

		this.points = points;
		this.closed = closed;
		this.curveType = curveType;
		this.tension = tension;

	}

	CatmullRomCurve3.prototype = Object.create( Curve.prototype );
	CatmullRomCurve3.prototype.constructor = CatmullRomCurve3;

	CatmullRomCurve3.prototype.isCatmullRomCurve3 = true;

	CatmullRomCurve3.prototype.getPoint = function ( t, optionalTarget = new Vector3() ) {

		const point = optionalTarget;

		const points = this.points;
		const l = points.length;

		const p = ( l - ( this.closed ? 0 : 1 ) ) * t;
		let intPoint = Math.floor( p );
		let weight = p - intPoint;

		if ( this.closed ) {

			intPoint += intPoint > 0 ? 0 : ( Math.floor( Math.abs( intPoint ) / l ) + 1 ) * l;

		} else if ( weight === 0 && intPoint === l - 1 ) {

			intPoint = l - 2;
			weight = 1;

		}

		let p0, p3; // 4 points (p1 & p2 defined below)

		if ( this.closed || intPoint > 0 ) {

			p0 = points[ ( intPoint - 1 ) % l ];

		} else {

			// extrapolate first point
			tmp.subVectors( points[ 0 ], points[ 1 ] ).add( points[ 0 ] );
			p0 = tmp;

		}

		const p1 = points[ intPoint % l ];
		const p2 = points[ ( intPoint + 1 ) % l ];

		if ( this.closed || intPoint + 2 < l ) {

			p3 = points[ ( intPoint + 2 ) % l ];

		} else {

			// extrapolate last point
			tmp.subVectors( points[ l - 1 ], points[ l - 2 ] ).add( points[ l - 1 ] );
			p3 = tmp;

		}

		if ( this.curveType === 'centripetal' || this.curveType === 'chordal' ) {

			// init Centripetal / Chordal Catmull-Rom
			const pow = this.curveType === 'chordal' ? 0.5 : 0.25;
			let dt0 = Math.pow( p0.distanceToSquared( p1 ), pow );
			let dt1 = Math.pow( p1.distanceToSquared( p2 ), pow );
			let dt2 = Math.pow( p2.distanceToSquared( p3 ), pow );

			// safety check for repeated points
			if ( dt1 < 1e-4 ) dt1 = 1.0;
			if ( dt0 < 1e-4 ) dt0 = dt1;
			if ( dt2 < 1e-4 ) dt2 = dt1;

			px.initNonuniformCatmullRom( p0.x, p1.x, p2.x, p3.x, dt0, dt1, dt2 );
			py.initNonuniformCatmullRom( p0.y, p1.y, p2.y, p3.y, dt0, dt1, dt2 );
			pz.initNonuniformCatmullRom( p0.z, p1.z, p2.z, p3.z, dt0, dt1, dt2 );

		} else if ( this.curveType === 'catmullrom' ) {

			px.initCatmullRom( p0.x, p1.x, p2.x, p3.x, this.tension );
			py.initCatmullRom( p0.y, p1.y, p2.y, p3.y, this.tension );
			pz.initCatmullRom( p0.z, p1.z, p2.z, p3.z, this.tension );

		}

		point.set(
			px.calc( weight ),
			py.calc( weight ),
			pz.calc( weight )
		);

		return point;

	};

	CatmullRomCurve3.prototype.copy = function ( source ) {

		Curve.prototype.copy.call( this, source );

		this.points = [];

		for ( let i = 0, l = source.points.length; i < l; i ++ ) {

			const point = source.points[ i ];

			this.points.push( point.clone() );

		}

		this.closed = source.closed;
		this.curveType = source.curveType;
		this.tension = source.tension;

		return this;

	};

	CatmullRomCurve3.prototype.toJSON = function () {

		const data = Curve.prototype.toJSON.call( this );

		data.points = [];

		for ( let i = 0, l = this.points.length; i < l; i ++ ) {

			const point = this.points[ i ];
			data.points.push( point.toArray() );

		}

		data.closed = this.closed;
		data.curveType = this.curveType;
		data.tension = this.tension;

		return data;

	};

	CatmullRomCurve3.prototype.fromJSON = function ( json ) {

		Curve.prototype.fromJSON.call( this, json );

		this.points = [];

		for ( let i = 0, l = json.points.length; i < l; i ++ ) {

			const point = json.points[ i ];
			this.points.push( new Vector3().fromArray( point ) );

		}

		this.closed = json.closed;
		this.curveType = json.curveType;
		this.tension = json.tension;

		return this;

	};

	/**
	 * Bezier Curves formulas obtained from
	 * http://en.wikipedia.org/wiki/Bézier_curve
	 */

	function CatmullRom( t, p0, p1, p2, p3 ) {

		const v0 = ( p2 - p0 ) * 0.5;
		const v1 = ( p3 - p1 ) * 0.5;
		const t2 = t * t;
		const t3 = t * t2;
		return ( 2 * p1 - 2 * p2 + v0 + v1 ) * t3 + ( - 3 * p1 + 3 * p2 - 2 * v0 - v1 ) * t2 + v0 * t + p1;

	}

	//

	function QuadraticBezierP0( t, p ) {

		const k = 1 - t;
		return k * k * p;

	}

	function QuadraticBezierP1( t, p ) {

		return 2 * ( 1 - t ) * t * p;

	}

	function QuadraticBezierP2( t, p ) {

		return t * t * p;

	}

	function QuadraticBezier( t, p0, p1, p2 ) {

		return QuadraticBezierP0( t, p0 ) + QuadraticBezierP1( t, p1 ) +
			QuadraticBezierP2( t, p2 );

	}

	//

	function CubicBezierP0( t, p ) {

		const k = 1 - t;
		return k * k * k * p;

	}

	function CubicBezierP1( t, p ) {

		const k = 1 - t;
		return 3 * k * k * t * p;

	}

	function CubicBezierP2( t, p ) {

		return 3 * ( 1 - t ) * t * t * p;

	}

	function CubicBezierP3( t, p ) {

		return t * t * t * p;

	}

	function CubicBezier( t, p0, p1, p2, p3 ) {

		return CubicBezierP0( t, p0 ) + CubicBezierP1( t, p1 ) + CubicBezierP2( t, p2 ) +
			CubicBezierP3( t, p3 );

	}

	function CubicBezierCurve( v0 = new Vector2(), v1 = new Vector2(), v2 = new Vector2(), v3 = new Vector2() ) {

		Curve.call( this );

		this.type = 'CubicBezierCurve';

		this.v0 = v0;
		this.v1 = v1;
		this.v2 = v2;
		this.v3 = v3;

	}

	CubicBezierCurve.prototype = Object.create( Curve.prototype );
	CubicBezierCurve.prototype.constructor = CubicBezierCurve;

	CubicBezierCurve.prototype.isCubicBezierCurve = true;

	CubicBezierCurve.prototype.getPoint = function ( t, optionalTarget = new Vector2() ) {

		const point = optionalTarget;

		const v0 = this.v0, v1 = this.v1, v2 = this.v2, v3 = this.v3;

		point.set(
			CubicBezier( t, v0.x, v1.x, v2.x, v3.x ),
			CubicBezier( t, v0.y, v1.y, v2.y, v3.y )
		);

		return point;

	};

	CubicBezierCurve.prototype.copy = function ( source ) {

		Curve.prototype.copy.call( this, source );

		this.v0.copy( source.v0 );
		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );
		this.v3.copy( source.v3 );

		return this;

	};

	CubicBezierCurve.prototype.toJSON = function () {

		const data = Curve.prototype.toJSON.call( this );

		data.v0 = this.v0.toArray();
		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();
		data.v3 = this.v3.toArray();

		return data;

	};

	CubicBezierCurve.prototype.fromJSON = function ( json ) {

		Curve.prototype.fromJSON.call( this, json );

		this.v0.fromArray( json.v0 );
		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );
		this.v3.fromArray( json.v3 );

		return this;

	};

	function CubicBezierCurve3( v0 = new Vector3(), v1 = new Vector3(), v2 = new Vector3(), v3 = new Vector3() ) {

		Curve.call( this );

		this.type = 'CubicBezierCurve3';

		this.v0 = v0;
		this.v1 = v1;
		this.v2 = v2;
		this.v3 = v3;

	}

	CubicBezierCurve3.prototype = Object.create( Curve.prototype );
	CubicBezierCurve3.prototype.constructor = CubicBezierCurve3;

	CubicBezierCurve3.prototype.isCubicBezierCurve3 = true;

	CubicBezierCurve3.prototype.getPoint = function ( t, optionalTarget = new Vector3() ) {

		const point = optionalTarget;

		const v0 = this.v0, v1 = this.v1, v2 = this.v2, v3 = this.v3;

		point.set(
			CubicBezier( t, v0.x, v1.x, v2.x, v3.x ),
			CubicBezier( t, v0.y, v1.y, v2.y, v3.y ),
			CubicBezier( t, v0.z, v1.z, v2.z, v3.z )
		);

		return point;

	};

	CubicBezierCurve3.prototype.copy = function ( source ) {

		Curve.prototype.copy.call( this, source );

		this.v0.copy( source.v0 );
		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );
		this.v3.copy( source.v3 );

		return this;

	};

	CubicBezierCurve3.prototype.toJSON = function () {

		const data = Curve.prototype.toJSON.call( this );

		data.v0 = this.v0.toArray();
		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();
		data.v3 = this.v3.toArray();

		return data;

	};

	CubicBezierCurve3.prototype.fromJSON = function ( json ) {

		Curve.prototype.fromJSON.call( this, json );

		this.v0.fromArray( json.v0 );
		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );
		this.v3.fromArray( json.v3 );

		return this;

	};

	function LineCurve( v1 = new Vector2(), v2 = new Vector2() ) {

		Curve.call( this );

		this.type = 'LineCurve';

		this.v1 = v1;
		this.v2 = v2;

	}

	LineCurve.prototype = Object.create( Curve.prototype );
	LineCurve.prototype.constructor = LineCurve;

	LineCurve.prototype.isLineCurve = true;

	LineCurve.prototype.getPoint = function ( t, optionalTarget = new Vector2() ) {

		const point = optionalTarget;

		if ( t === 1 ) {

			point.copy( this.v2 );

		} else {

			point.copy( this.v2 ).sub( this.v1 );
			point.multiplyScalar( t ).add( this.v1 );

		}

		return point;

	};

	// Line curve is linear, so we can overwrite default getPointAt

	LineCurve.prototype.getPointAt = function ( u, optionalTarget ) {

		return this.getPoint( u, optionalTarget );

	};

	LineCurve.prototype.getTangent = function ( t, optionalTarget ) {

		const tangent = optionalTarget || new Vector2();

		tangent.copy( this.v2 ).sub( this.v1 ).normalize();

		return tangent;

	};

	LineCurve.prototype.copy = function ( source ) {

		Curve.prototype.copy.call( this, source );

		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );

		return this;

	};

	LineCurve.prototype.toJSON = function () {

		const data = Curve.prototype.toJSON.call( this );

		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();

		return data;

	};

	LineCurve.prototype.fromJSON = function ( json ) {

		Curve.prototype.fromJSON.call( this, json );

		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );

		return this;

	};

	function LineCurve3( v1 = new Vector3(), v2 = new Vector3() ) {

		Curve.call( this );

		this.type = 'LineCurve3';

		this.v1 = v1;
		this.v2 = v2;

	}

	LineCurve3.prototype = Object.create( Curve.prototype );
	LineCurve3.prototype.constructor = LineCurve3;

	LineCurve3.prototype.isLineCurve3 = true;

	LineCurve3.prototype.getPoint = function ( t, optionalTarget = new Vector3() ) {

		const point = optionalTarget;

		if ( t === 1 ) {

			point.copy( this.v2 );

		} else {

			point.copy( this.v2 ).sub( this.v1 );
			point.multiplyScalar( t ).add( this.v1 );

		}

		return point;

	};

	// Line curve is linear, so we can overwrite default getPointAt

	LineCurve3.prototype.getPointAt = function ( u, optionalTarget ) {

		return this.getPoint( u, optionalTarget );

	};

	LineCurve3.prototype.copy = function ( source ) {

		Curve.prototype.copy.call( this, source );

		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );

		return this;

	};

	LineCurve3.prototype.toJSON = function () {

		const data = Curve.prototype.toJSON.call( this );

		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();

		return data;

	};

	LineCurve3.prototype.fromJSON = function ( json ) {

		Curve.prototype.fromJSON.call( this, json );

		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );

		return this;

	};

	function QuadraticBezierCurve( v0 = new Vector2(), v1 = new Vector2(), v2 = new Vector2() ) {

		Curve.call( this );

		this.type = 'QuadraticBezierCurve';

		this.v0 = v0;
		this.v1 = v1;
		this.v2 = v2;

	}

	QuadraticBezierCurve.prototype = Object.create( Curve.prototype );
	QuadraticBezierCurve.prototype.constructor = QuadraticBezierCurve;

	QuadraticBezierCurve.prototype.isQuadraticBezierCurve = true;

	QuadraticBezierCurve.prototype.getPoint = function ( t, optionalTarget = new Vector2() ) {

		const point = optionalTarget;

		const v0 = this.v0, v1 = this.v1, v2 = this.v2;

		point.set(
			QuadraticBezier( t, v0.x, v1.x, v2.x ),
			QuadraticBezier( t, v0.y, v1.y, v2.y )
		);

		return point;

	};

	QuadraticBezierCurve.prototype.copy = function ( source ) {

		Curve.prototype.copy.call( this, source );

		this.v0.copy( source.v0 );
		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );

		return this;

	};

	QuadraticBezierCurve.prototype.toJSON = function () {

		const data = Curve.prototype.toJSON.call( this );

		data.v0 = this.v0.toArray();
		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();

		return data;

	};

	QuadraticBezierCurve.prototype.fromJSON = function ( json ) {

		Curve.prototype.fromJSON.call( this, json );

		this.v0.fromArray( json.v0 );
		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );

		return this;

	};

	function QuadraticBezierCurve3( v0 = new Vector3(), v1 = new Vector3(), v2 = new Vector3() ) {

		Curve.call( this );

		this.type = 'QuadraticBezierCurve3';

		this.v0 = v0;
		this.v1 = v1;
		this.v2 = v2;

	}

	QuadraticBezierCurve3.prototype = Object.create( Curve.prototype );
	QuadraticBezierCurve3.prototype.constructor = QuadraticBezierCurve3;

	QuadraticBezierCurve3.prototype.isQuadraticBezierCurve3 = true;

	QuadraticBezierCurve3.prototype.getPoint = function ( t, optionalTarget = new Vector3() ) {

		const point = optionalTarget;

		const v0 = this.v0, v1 = this.v1, v2 = this.v2;

		point.set(
			QuadraticBezier( t, v0.x, v1.x, v2.x ),
			QuadraticBezier( t, v0.y, v1.y, v2.y ),
			QuadraticBezier( t, v0.z, v1.z, v2.z )
		);

		return point;

	};

	QuadraticBezierCurve3.prototype.copy = function ( source ) {

		Curve.prototype.copy.call( this, source );

		this.v0.copy( source.v0 );
		this.v1.copy( source.v1 );
		this.v2.copy( source.v2 );

		return this;

	};

	QuadraticBezierCurve3.prototype.toJSON = function () {

		const data = Curve.prototype.toJSON.call( this );

		data.v0 = this.v0.toArray();
		data.v1 = this.v1.toArray();
		data.v2 = this.v2.toArray();

		return data;

	};

	QuadraticBezierCurve3.prototype.fromJSON = function ( json ) {

		Curve.prototype.fromJSON.call( this, json );

		this.v0.fromArray( json.v0 );
		this.v1.fromArray( json.v1 );
		this.v2.fromArray( json.v2 );

		return this;

	};

	function SplineCurve( points = [] ) {

		Curve.call( this );

		this.type = 'SplineCurve';

		this.points = points;

	}

	SplineCurve.prototype = Object.create( Curve.prototype );
	SplineCurve.prototype.constructor = SplineCurve;

	SplineCurve.prototype.isSplineCurve = true;

	SplineCurve.prototype.getPoint = function ( t, optionalTarget = new Vector2() ) {

		const point = optionalTarget;

		const points = this.points;
		const p = ( points.length - 1 ) * t;

		const intPoint = Math.floor( p );
		const weight = p - intPoint;

		const p0 = points[ intPoint === 0 ? intPoint : intPoint - 1 ];
		const p1 = points[ intPoint ];
		const p2 = points[ intPoint > points.length - 2 ? points.length - 1 : intPoint + 1 ];
		const p3 = points[ intPoint > points.length - 3 ? points.length - 1 : intPoint + 2 ];

		point.set(
			CatmullRom( weight, p0.x, p1.x, p2.x, p3.x ),
			CatmullRom( weight, p0.y, p1.y, p2.y, p3.y )
		);

		return point;

	};

	SplineCurve.prototype.copy = function ( source ) {

		Curve.prototype.copy.call( this, source );

		this.points = [];

		for ( let i = 0, l = source.points.length; i < l; i ++ ) {

			const point = source.points[ i ];

			this.points.push( point.clone() );

		}

		return this;

	};

	SplineCurve.prototype.toJSON = function () {

		const data = Curve.prototype.toJSON.call( this );

		data.points = [];

		for ( let i = 0, l = this.points.length; i < l; i ++ ) {

			const point = this.points[ i ];
			data.points.push( point.toArray() );

		}

		return data;

	};

	SplineCurve.prototype.fromJSON = function ( json ) {

		Curve.prototype.fromJSON.call( this, json );

		this.points = [];

		for ( let i = 0, l = json.points.length; i < l; i ++ ) {

			const point = json.points[ i ];
			this.points.push( new Vector2().fromArray( point ) );

		}

		return this;

	};

	var Curves = /*#__PURE__*/Object.freeze({
		__proto__: null,
		ArcCurve: ArcCurve,
		CatmullRomCurve3: CatmullRomCurve3,
		CubicBezierCurve: CubicBezierCurve,
		CubicBezierCurve3: CubicBezierCurve3,
		EllipseCurve: EllipseCurve,
		LineCurve: LineCurve,
		LineCurve3: LineCurve3,
		QuadraticBezierCurve: QuadraticBezierCurve,
		QuadraticBezierCurve3: QuadraticBezierCurve3,
		SplineCurve: SplineCurve
	});

	/**************************************************************
	 *	Curved Path - a curve path is simply a array of connected
	 *  curves, but retains the api of a curve
	 **************************************************************/

	function CurvePath() {

		Curve.call( this );

		this.type = 'CurvePath';

		this.curves = [];
		this.autoClose = false; // Automatically closes the path

	}

	CurvePath.prototype = Object.assign( Object.create( Curve.prototype ), {

		constructor: CurvePath,

		add: function ( curve ) {

			this.curves.push( curve );

		},

		closePath: function () {

			// Add a line curve if start and end of lines are not connected
			const startPoint = this.curves[ 0 ].getPoint( 0 );
			const endPoint = this.curves[ this.curves.length - 1 ].getPoint( 1 );

			if ( ! startPoint.equals( endPoint ) ) {

				this.curves.push( new LineCurve( endPoint, startPoint ) );

			}

		},

		// To get accurate point with reference to
		// entire path distance at time t,
		// following has to be done:

		// 1. Length of each sub path have to be known
		// 2. Locate and identify type of curve
		// 3. Get t for the curve
		// 4. Return curve.getPointAt(t')

		getPoint: function ( t ) {

			const d = t * this.getLength();
			const curveLengths = this.getCurveLengths();
			let i = 0;

			// To think about boundaries points.

			while ( i < curveLengths.length ) {

				if ( curveLengths[ i ] >= d ) {

					const diff = curveLengths[ i ] - d;
					const curve = this.curves[ i ];

					const segmentLength = curve.getLength();
					const u = segmentLength === 0 ? 0 : 1 - diff / segmentLength;

					return curve.getPointAt( u );

				}

				i ++;

			}

			return null;

			// loop where sum != 0, sum > d , sum+1 <d

		},

		// We cannot use the default THREE.Curve getPoint() with getLength() because in
		// THREE.Curve, getLength() depends on getPoint() but in THREE.CurvePath
		// getPoint() depends on getLength

		getLength: function () {

			const lens = this.getCurveLengths();
			return lens[ lens.length - 1 ];

		},

		// cacheLengths must be recalculated.
		updateArcLengths: function () {

			this.needsUpdate = true;
			this.cacheLengths = null;
			this.getCurveLengths();

		},

		// Compute lengths and cache them
		// We cannot overwrite getLengths() because UtoT mapping uses it.

		getCurveLengths: function () {

			// We use cache values if curves and cache array are same length

			if ( this.cacheLengths && this.cacheLengths.length === this.curves.length ) {

				return this.cacheLengths;

			}

			// Get length of sub-curve
			// Push sums into cached array

			const lengths = [];
			let sums = 0;

			for ( let i = 0, l = this.curves.length; i < l; i ++ ) {

				sums += this.curves[ i ].getLength();
				lengths.push( sums );

			}

			this.cacheLengths = lengths;

			return lengths;

		},

		getSpacedPoints: function ( divisions = 40 ) {

			const points = [];

			for ( let i = 0; i <= divisions; i ++ ) {

				points.push( this.getPoint( i / divisions ) );

			}

			if ( this.autoClose ) {

				points.push( points[ 0 ] );

			}

			return points;

		},

		getPoints: function ( divisions = 12 ) {

			const points = [];
			let last;

			for ( let i = 0, curves = this.curves; i < curves.length; i ++ ) {

				const curve = curves[ i ];
				const resolution = ( curve && curve.isEllipseCurve ) ? divisions * 2
					: ( curve && ( curve.isLineCurve || curve.isLineCurve3 ) ) ? 1
						: ( curve && curve.isSplineCurve ) ? divisions * curve.points.length
							: divisions;

				const pts = curve.getPoints( resolution );

				for ( let j = 0; j < pts.length; j ++ ) {

					const point = pts[ j ];

					if ( last && last.equals( point ) ) continue; // ensures no consecutive points are duplicates

					points.push( point );
					last = point;

				}

			}

			if ( this.autoClose && points.length > 1 && ! points[ points.length - 1 ].equals( points[ 0 ] ) ) {

				points.push( points[ 0 ] );

			}

			return points;

		},

		copy: function ( source ) {

			Curve.prototype.copy.call( this, source );

			this.curves = [];

			for ( let i = 0, l = source.curves.length; i < l; i ++ ) {

				const curve = source.curves[ i ];

				this.curves.push( curve.clone() );

			}

			this.autoClose = source.autoClose;

			return this;

		},

		toJSON: function () {

			const data = Curve.prototype.toJSON.call( this );

			data.autoClose = this.autoClose;
			data.curves = [];

			for ( let i = 0, l = this.curves.length; i < l; i ++ ) {

				const curve = this.curves[ i ];
				data.curves.push( curve.toJSON() );

			}

			return data;

		},

		fromJSON: function ( json ) {

			Curve.prototype.fromJSON.call( this, json );

			this.autoClose = json.autoClose;
			this.curves = [];

			for ( let i = 0, l = json.curves.length; i < l; i ++ ) {

				const curve = json.curves[ i ];
				this.curves.push( new Curves[ curve.type ]().fromJSON( curve ) );

			}

			return this;

		}

	} );

	function Path( points ) {

		CurvePath.call( this );

		this.type = 'Path';

		this.currentPoint = new Vector2();

		if ( points ) {

			this.setFromPoints( points );

		}

	}

	Path.prototype = Object.assign( Object.create( CurvePath.prototype ), {

		constructor: Path,

		setFromPoints: function ( points ) {

			this.moveTo( points[ 0 ].x, points[ 0 ].y );

			for ( let i = 1, l = points.length; i < l; i ++ ) {

				this.lineTo( points[ i ].x, points[ i ].y );

			}

			return this;

		},

		moveTo: function ( x, y ) {

			this.currentPoint.set( x, y ); // TODO consider referencing vectors instead of copying?

			return this;

		},

		lineTo: function ( x, y ) {

			const curve = new LineCurve( this.currentPoint.clone(), new Vector2( x, y ) );
			this.curves.push( curve );

			this.currentPoint.set( x, y );

			return this;

		},

		quadraticCurveTo: function ( aCPx, aCPy, aX, aY ) {

			const curve = new QuadraticBezierCurve(
				this.currentPoint.clone(),
				new Vector2( aCPx, aCPy ),
				new Vector2( aX, aY )
			);

			this.curves.push( curve );

			this.currentPoint.set( aX, aY );

			return this;

		},

		bezierCurveTo: function ( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY ) {

			const curve = new CubicBezierCurve(
				this.currentPoint.clone(),
				new Vector2( aCP1x, aCP1y ),
				new Vector2( aCP2x, aCP2y ),
				new Vector2( aX, aY )
			);

			this.curves.push( curve );

			this.currentPoint.set( aX, aY );

			return this;

		},

		splineThru: function ( pts /*Array of Vector*/ ) {

			const npts = [ this.currentPoint.clone() ].concat( pts );

			const curve = new SplineCurve( npts );
			this.curves.push( curve );

			this.currentPoint.copy( pts[ pts.length - 1 ] );

			return this;

		},

		arc: function ( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {

			const x0 = this.currentPoint.x;
			const y0 = this.currentPoint.y;

			this.absarc( aX + x0, aY + y0, aRadius,
				aStartAngle, aEndAngle, aClockwise );

			return this;

		},

		absarc: function ( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise ) {

			this.absellipse( aX, aY, aRadius, aRadius, aStartAngle, aEndAngle, aClockwise );

			return this;

		},

		ellipse: function ( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation ) {

			const x0 = this.currentPoint.x;
			const y0 = this.currentPoint.y;

			this.absellipse( aX + x0, aY + y0, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation );

			return this;

		},

		absellipse: function ( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation ) {

			const curve = new EllipseCurve( aX, aY, xRadius, yRadius, aStartAngle, aEndAngle, aClockwise, aRotation );

			if ( this.curves.length > 0 ) {

				// if a previous curve is present, attempt to join
				const firstPoint = curve.getPoint( 0 );

				if ( ! firstPoint.equals( this.currentPoint ) ) {

					this.lineTo( firstPoint.x, firstPoint.y );

				}

			}

			this.curves.push( curve );

			const lastPoint = curve.getPoint( 1 );
			this.currentPoint.copy( lastPoint );

			return this;

		},

		copy: function ( source ) {

			CurvePath.prototype.copy.call( this, source );

			this.currentPoint.copy( source.currentPoint );

			return this;

		},

		toJSON: function () {

			const data = CurvePath.prototype.toJSON.call( this );

			data.currentPoint = this.currentPoint.toArray();

			return data;

		},

		fromJSON: function ( json ) {

			CurvePath.prototype.fromJSON.call( this, json );

			this.currentPoint.fromArray( json.currentPoint );

			return this;

		}

	} );

	function Shape( points ) {

		Path.call( this, points );

		this.uuid = MathUtils.generateUUID();

		this.type = 'Shape';

		this.holes = [];

	}

	Shape.prototype = Object.assign( Object.create( Path.prototype ), {

		constructor: Shape,

		getPointsHoles: function ( divisions ) {

			const holesPts = [];

			for ( let i = 0, l = this.holes.length; i < l; i ++ ) {

				holesPts[ i ] = this.holes[ i ].getPoints( divisions );

			}

			return holesPts;

		},

		// get points of shape and holes (keypoints based on segments parameter)

		extractPoints: function ( divisions ) {

			return {

				shape: this.getPoints( divisions ),
				holes: this.getPointsHoles( divisions )

			};

		},

		copy: function ( source ) {

			Path.prototype.copy.call( this, source );

			this.holes = [];

			for ( let i = 0, l = source.holes.length; i < l; i ++ ) {

				const hole = source.holes[ i ];

				this.holes.push( hole.clone() );

			}

			return this;

		},

		toJSON: function () {

			const data = Path.prototype.toJSON.call( this );

			data.uuid = this.uuid;
			data.holes = [];

			for ( let i = 0, l = this.holes.length; i < l; i ++ ) {

				const hole = this.holes[ i ];
				data.holes.push( hole.toJSON() );

			}

			return data;

		},

		fromJSON: function ( json ) {

			Path.prototype.fromJSON.call( this, json );

			this.uuid = json.uuid;
			this.holes = [];

			for ( let i = 0, l = json.holes.length; i < l; i ++ ) {

				const hole = json.holes[ i ];
				this.holes.push( new Path().fromJSON( hole ) );

			}

			return this;

		}

	} );

	function Light( color, intensity = 1 ) {

		Object3D.call( this );

		this.type = 'Light';

		this.color = new Color( color );
		this.intensity = intensity;

	}

	Light.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: Light,

		isLight: true,

		copy: function ( source ) {

			Object3D.prototype.copy.call( this, source );

			this.color.copy( source.color );
			this.intensity = source.intensity;

			return this;

		},

		toJSON: function ( meta ) {

			const data = Object3D.prototype.toJSON.call( this, meta );

			data.object.color = this.color.getHex();
			data.object.intensity = this.intensity;

			if ( this.groundColor !== undefined ) data.object.groundColor = this.groundColor.getHex();

			if ( this.distance !== undefined ) data.object.distance = this.distance;
			if ( this.angle !== undefined ) data.object.angle = this.angle;
			if ( this.decay !== undefined ) data.object.decay = this.decay;
			if ( this.penumbra !== undefined ) data.object.penumbra = this.penumbra;

			if ( this.shadow !== undefined ) data.object.shadow = this.shadow.toJSON();

			return data;

		}

	} );

	function HemisphereLight( skyColor, groundColor, intensity ) {

		Light.call( this, skyColor, intensity );

		this.type = 'HemisphereLight';

		this.position.copy( Object3D.DefaultUp );
		this.updateMatrix();

		this.groundColor = new Color( groundColor );

	}

	HemisphereLight.prototype = Object.assign( Object.create( Light.prototype ), {

		constructor: HemisphereLight,

		isHemisphereLight: true,

		copy: function ( source ) {

			Light.prototype.copy.call( this, source );

			this.groundColor.copy( source.groundColor );

			return this;

		}

	} );

	function LightShadow( camera ) {

		this.camera = camera;

		this.bias = 0;
		this.normalBias = 0;
		this.radius = 1;

		this.mapSize = new Vector2( 512, 512 );

		this.map = null;
		this.mapPass = null;
		this.matrix = new Matrix4();

		this.autoUpdate = true;
		this.needsUpdate = false;

		this._frustum = new Frustum();
		this._frameExtents = new Vector2( 1, 1 );

		this._viewportCount = 1;

		this._viewports = [

			new Vector4( 0, 0, 1, 1 )

		];

	}

	Object.assign( LightShadow.prototype, {

		_projScreenMatrix: new Matrix4(),

		_lightPositionWorld: new Vector3(),

		_lookTarget: new Vector3(),

		getViewportCount: function () {

			return this._viewportCount;

		},

		getFrustum: function () {

			return this._frustum;

		},

		updateMatrices: function ( light ) {

			const shadowCamera = this.camera,
				shadowMatrix = this.matrix,
				projScreenMatrix = this._projScreenMatrix,
				lookTarget = this._lookTarget,
				lightPositionWorld = this._lightPositionWorld;

			lightPositionWorld.setFromMatrixPosition( light.matrixWorld );
			shadowCamera.position.copy( lightPositionWorld );

			lookTarget.setFromMatrixPosition( light.target.matrixWorld );
			shadowCamera.lookAt( lookTarget );
			shadowCamera.updateMatrixWorld();

			projScreenMatrix.multiplyMatrices( shadowCamera.projectionMatrix, shadowCamera.matrixWorldInverse );
			this._frustum.setFromProjectionMatrix( projScreenMatrix );

			shadowMatrix.set(
				0.5, 0.0, 0.0, 0.5,
				0.0, 0.5, 0.0, 0.5,
				0.0, 0.0, 0.5, 0.5,
				0.0, 0.0, 0.0, 1.0
			);

			shadowMatrix.multiply( shadowCamera.projectionMatrix );
			shadowMatrix.multiply( shadowCamera.matrixWorldInverse );

		},

		getViewport: function ( viewportIndex ) {

			return this._viewports[ viewportIndex ];

		},

		getFrameExtents: function () {

			return this._frameExtents;

		},

		copy: function ( source ) {

			this.camera = source.camera.clone();

			this.bias = source.bias;
			this.radius = source.radius;

			this.mapSize.copy( source.mapSize );

			return this;

		},

		clone: function () {

			return new this.constructor().copy( this );

		},

		toJSON: function () {

			const object = {};

			if ( this.bias !== 0 ) object.bias = this.bias;
			if ( this.normalBias !== 0 ) object.normalBias = this.normalBias;
			if ( this.radius !== 1 ) object.radius = this.radius;
			if ( this.mapSize.x !== 512 || this.mapSize.y !== 512 ) object.mapSize = this.mapSize.toArray();

			object.camera = this.camera.toJSON( false ).object;
			delete object.camera.matrix;

			return object;

		}

	} );

	function SpotLightShadow() {

		LightShadow.call( this, new PerspectiveCamera( 50, 1, 0.5, 500 ) );

		this.focus = 1;

	}

	SpotLightShadow.prototype = Object.assign( Object.create( LightShadow.prototype ), {

		constructor: SpotLightShadow,

		isSpotLightShadow: true,

		updateMatrices: function ( light ) {

			const camera = this.camera;

			const fov = MathUtils.RAD2DEG * 2 * light.angle * this.focus;
			const aspect = this.mapSize.width / this.mapSize.height;
			const far = light.distance || camera.far;

			if ( fov !== camera.fov || aspect !== camera.aspect || far !== camera.far ) {

				camera.fov = fov;
				camera.aspect = aspect;
				camera.far = far;
				camera.updateProjectionMatrix();

			}

			LightShadow.prototype.updateMatrices.call( this, light );

		}

	} );

	function SpotLight( color, intensity, distance, angle, penumbra, decay ) {

		Light.call( this, color, intensity );

		this.type = 'SpotLight';

		this.position.copy( Object3D.DefaultUp );
		this.updateMatrix();

		this.target = new Object3D();

		Object.defineProperty( this, 'power', {
			get: function () {

				// intensity = power per solid angle.
				// ref: equation (17) from https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
				return this.intensity * Math.PI;

			},
			set: function ( power ) {

				// intensity = power per solid angle.
				// ref: equation (17) from https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
				this.intensity = power / Math.PI;

			}
		} );

		this.distance = ( distance !== undefined ) ? distance : 0;
		this.angle = ( angle !== undefined ) ? angle : Math.PI / 3;
		this.penumbra = ( penumbra !== undefined ) ? penumbra : 0;
		this.decay = ( decay !== undefined ) ? decay : 1;	// for physically correct lights, should be 2.

		this.shadow = new SpotLightShadow();

	}

	SpotLight.prototype = Object.assign( Object.create( Light.prototype ), {

		constructor: SpotLight,

		isSpotLight: true,

		copy: function ( source ) {

			Light.prototype.copy.call( this, source );

			this.distance = source.distance;
			this.angle = source.angle;
			this.penumbra = source.penumbra;
			this.decay = source.decay;

			this.target = source.target.clone();

			this.shadow = source.shadow.clone();

			return this;

		}

	} );

	function PointLightShadow() {

		LightShadow.call( this, new PerspectiveCamera( 90, 1, 0.5, 500 ) );

		this._frameExtents = new Vector2( 4, 2 );

		this._viewportCount = 6;

		this._viewports = [
			// These viewports map a cube-map onto a 2D texture with the
			// following orientation:
			//
			//  xzXZ
			//   y Y
			//
			// X - Positive x direction
			// x - Negative x direction
			// Y - Positive y direction
			// y - Negative y direction
			// Z - Positive z direction
			// z - Negative z direction

			// positive X
			new Vector4( 2, 1, 1, 1 ),
			// negative X
			new Vector4( 0, 1, 1, 1 ),
			// positive Z
			new Vector4( 3, 1, 1, 1 ),
			// negative Z
			new Vector4( 1, 1, 1, 1 ),
			// positive Y
			new Vector4( 3, 0, 1, 1 ),
			// negative Y
			new Vector4( 1, 0, 1, 1 )
		];

		this._cubeDirections = [
			new Vector3( 1, 0, 0 ), new Vector3( - 1, 0, 0 ), new Vector3( 0, 0, 1 ),
			new Vector3( 0, 0, - 1 ), new Vector3( 0, 1, 0 ), new Vector3( 0, - 1, 0 )
		];

		this._cubeUps = [
			new Vector3( 0, 1, 0 ), new Vector3( 0, 1, 0 ), new Vector3( 0, 1, 0 ),
			new Vector3( 0, 1, 0 ), new Vector3( 0, 0, 1 ),	new Vector3( 0, 0, - 1 )
		];

	}

	PointLightShadow.prototype = Object.assign( Object.create( LightShadow.prototype ), {

		constructor: PointLightShadow,

		isPointLightShadow: true,

		updateMatrices: function ( light, viewportIndex = 0 ) {

			const camera = this.camera,
				shadowMatrix = this.matrix,
				lightPositionWorld = this._lightPositionWorld,
				lookTarget = this._lookTarget,
				projScreenMatrix = this._projScreenMatrix;

			lightPositionWorld.setFromMatrixPosition( light.matrixWorld );
			camera.position.copy( lightPositionWorld );

			lookTarget.copy( camera.position );
			lookTarget.add( this._cubeDirections[ viewportIndex ] );
			camera.up.copy( this._cubeUps[ viewportIndex ] );
			camera.lookAt( lookTarget );
			camera.updateMatrixWorld();

			shadowMatrix.makeTranslation( - lightPositionWorld.x, - lightPositionWorld.y, - lightPositionWorld.z );

			projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
			this._frustum.setFromProjectionMatrix( projScreenMatrix );

		}

	} );

	function PointLight( color, intensity, distance, decay ) {

		Light.call( this, color, intensity );

		this.type = 'PointLight';

		Object.defineProperty( this, 'power', {
			get: function () {

				// intensity = power per solid angle.
				// ref: equation (15) from https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
				return this.intensity * 4 * Math.PI;

			},
			set: function ( power ) {

				// intensity = power per solid angle.
				// ref: equation (15) from https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
				this.intensity = power / ( 4 * Math.PI );

			}
		} );

		this.distance = ( distance !== undefined ) ? distance : 0;
		this.decay = ( decay !== undefined ) ? decay : 1;	// for physically correct lights, should be 2.

		this.shadow = new PointLightShadow();

	}

	PointLight.prototype = Object.assign( Object.create( Light.prototype ), {

		constructor: PointLight,

		isPointLight: true,

		copy: function ( source ) {

			Light.prototype.copy.call( this, source );

			this.distance = source.distance;
			this.decay = source.decay;

			this.shadow = source.shadow.clone();

			return this;

		}

	} );

	function OrthographicCamera( left = - 1, right = 1, top = 1, bottom = - 1, near = 0.1, far = 2000 ) {

		Camera.call( this );

		this.type = 'OrthographicCamera';

		this.zoom = 1;
		this.view = null;

		this.left = left;
		this.right = right;
		this.top = top;
		this.bottom = bottom;

		this.near = near;
		this.far = far;

		this.updateProjectionMatrix();

	}

	OrthographicCamera.prototype = Object.assign( Object.create( Camera.prototype ), {

		constructor: OrthographicCamera,

		isOrthographicCamera: true,

		copy: function ( source, recursive ) {

			Camera.prototype.copy.call( this, source, recursive );

			this.left = source.left;
			this.right = source.right;
			this.top = source.top;
			this.bottom = source.bottom;
			this.near = source.near;
			this.far = source.far;

			this.zoom = source.zoom;
			this.view = source.view === null ? null : Object.assign( {}, source.view );

			return this;

		},

		setViewOffset: function ( fullWidth, fullHeight, x, y, width, height ) {

			if ( this.view === null ) {

				this.view = {
					enabled: true,
					fullWidth: 1,
					fullHeight: 1,
					offsetX: 0,
					offsetY: 0,
					width: 1,
					height: 1
				};

			}

			this.view.enabled = true;
			this.view.fullWidth = fullWidth;
			this.view.fullHeight = fullHeight;
			this.view.offsetX = x;
			this.view.offsetY = y;
			this.view.width = width;
			this.view.height = height;

			this.updateProjectionMatrix();

		},

		clearViewOffset: function () {

			if ( this.view !== null ) {

				this.view.enabled = false;

			}

			this.updateProjectionMatrix();

		},

		updateProjectionMatrix: function () {

			const dx = ( this.right - this.left ) / ( 2 * this.zoom );
			const dy = ( this.top - this.bottom ) / ( 2 * this.zoom );
			const cx = ( this.right + this.left ) / 2;
			const cy = ( this.top + this.bottom ) / 2;

			let left = cx - dx;
			let right = cx + dx;
			let top = cy + dy;
			let bottom = cy - dy;

			if ( this.view !== null && this.view.enabled ) {

				const scaleW = ( this.right - this.left ) / this.view.fullWidth / this.zoom;
				const scaleH = ( this.top - this.bottom ) / this.view.fullHeight / this.zoom;

				left += scaleW * this.view.offsetX;
				right = left + scaleW * this.view.width;
				top -= scaleH * this.view.offsetY;
				bottom = top - scaleH * this.view.height;

			}

			this.projectionMatrix.makeOrthographic( left, right, top, bottom, this.near, this.far );

			this.projectionMatrixInverse.copy( this.projectionMatrix ).invert();

		},

		toJSON: function ( meta ) {

			const data = Object3D.prototype.toJSON.call( this, meta );

			data.object.zoom = this.zoom;
			data.object.left = this.left;
			data.object.right = this.right;
			data.object.top = this.top;
			data.object.bottom = this.bottom;
			data.object.near = this.near;
			data.object.far = this.far;

			if ( this.view !== null ) data.object.view = Object.assign( {}, this.view );

			return data;

		}

	} );

	function DirectionalLightShadow() {

		LightShadow.call( this, new OrthographicCamera( - 5, 5, 5, - 5, 0.5, 500 ) );

	}

	DirectionalLightShadow.prototype = Object.assign( Object.create( LightShadow.prototype ), {

		constructor: DirectionalLightShadow,

		isDirectionalLightShadow: true,

		updateMatrices: function ( light ) {

			LightShadow.prototype.updateMatrices.call( this, light );

		}

	} );

	function DirectionalLight( color, intensity ) {

		Light.call( this, color, intensity );

		this.type = 'DirectionalLight';

		this.position.copy( Object3D.DefaultUp );
		this.updateMatrix();

		this.target = new Object3D();

		this.shadow = new DirectionalLightShadow();

	}

	DirectionalLight.prototype = Object.assign( Object.create( Light.prototype ), {

		constructor: DirectionalLight,

		isDirectionalLight: true,

		copy: function ( source ) {

			Light.prototype.copy.call( this, source );

			this.target = source.target.clone();

			this.shadow = source.shadow.clone();

			return this;

		}

	} );

	function AmbientLight( color, intensity ) {

		Light.call( this, color, intensity );

		this.type = 'AmbientLight';

	}

	AmbientLight.prototype = Object.assign( Object.create( Light.prototype ), {

		constructor: AmbientLight,

		isAmbientLight: true

	} );

	function RectAreaLight( color, intensity, width, height ) {

		Light.call( this, color, intensity );

		this.type = 'RectAreaLight';

		this.width = ( width !== undefined ) ? width : 10;
		this.height = ( height !== undefined ) ? height : 10;

	}

	RectAreaLight.prototype = Object.assign( Object.create( Light.prototype ), {

		constructor: RectAreaLight,

		isRectAreaLight: true,

		copy: function ( source ) {

			Light.prototype.copy.call( this, source );

			this.width = source.width;
			this.height = source.height;

			return this;

		},

		toJSON: function ( meta ) {

			const data = Light.prototype.toJSON.call( this, meta );

			data.object.width = this.width;
			data.object.height = this.height;

			return data;

		}

	} );

	/**
	 * Primary reference:
	 *   https://graphics.stanford.edu/papers/envmap/envmap.pdf
	 *
	 * Secondary reference:
	 *   https://www.ppsloan.org/publications/StupidSH36.pdf
	 */

	// 3-band SH defined by 9 coefficients

	class SphericalHarmonics3 {

		constructor() {

			Object.defineProperty( this, 'isSphericalHarmonics3', { value: true } );

			this.coefficients = [];

			for ( let i = 0; i < 9; i ++ ) {

				this.coefficients.push( new Vector3() );

			}

		}

		set( coefficients ) {

			for ( let i = 0; i < 9; i ++ ) {

				this.coefficients[ i ].copy( coefficients[ i ] );

			}

			return this;

		}

		zero() {

			for ( let i = 0; i < 9; i ++ ) {

				this.coefficients[ i ].set( 0, 0, 0 );

			}

			return this;

		}

		// get the radiance in the direction of the normal
		// target is a Vector3
		getAt( normal, target ) {

			// normal is assumed to be unit length

			const x = normal.x, y = normal.y, z = normal.z;

			const coeff = this.coefficients;

			// band 0
			target.copy( coeff[ 0 ] ).multiplyScalar( 0.282095 );

			// band 1
			target.addScaledVector( coeff[ 1 ], 0.488603 * y );
			target.addScaledVector( coeff[ 2 ], 0.488603 * z );
			target.addScaledVector( coeff[ 3 ], 0.488603 * x );

			// band 2
			target.addScaledVector( coeff[ 4 ], 1.092548 * ( x * y ) );
			target.addScaledVector( coeff[ 5 ], 1.092548 * ( y * z ) );
			target.addScaledVector( coeff[ 6 ], 0.315392 * ( 3.0 * z * z - 1.0 ) );
			target.addScaledVector( coeff[ 7 ], 1.092548 * ( x * z ) );
			target.addScaledVector( coeff[ 8 ], 0.546274 * ( x * x - y * y ) );

			return target;

		}

		// get the irradiance (radiance convolved with cosine lobe) in the direction of the normal
		// target is a Vector3
		// https://graphics.stanford.edu/papers/envmap/envmap.pdf
		getIrradianceAt( normal, target ) {

			// normal is assumed to be unit length

			const x = normal.x, y = normal.y, z = normal.z;

			const coeff = this.coefficients;

			// band 0
			target.copy( coeff[ 0 ] ).multiplyScalar( 0.886227 ); // π * 0.282095

			// band 1
			target.addScaledVector( coeff[ 1 ], 2.0 * 0.511664 * y ); // ( 2 * π / 3 ) * 0.488603
			target.addScaledVector( coeff[ 2 ], 2.0 * 0.511664 * z );
			target.addScaledVector( coeff[ 3 ], 2.0 * 0.511664 * x );

			// band 2
			target.addScaledVector( coeff[ 4 ], 2.0 * 0.429043 * x * y ); // ( π / 4 ) * 1.092548
			target.addScaledVector( coeff[ 5 ], 2.0 * 0.429043 * y * z );
			target.addScaledVector( coeff[ 6 ], 0.743125 * z * z - 0.247708 ); // ( π / 4 ) * 0.315392 * 3
			target.addScaledVector( coeff[ 7 ], 2.0 * 0.429043 * x * z );
			target.addScaledVector( coeff[ 8 ], 0.429043 * ( x * x - y * y ) ); // ( π / 4 ) * 0.546274

			return target;

		}

		add( sh ) {

			for ( let i = 0; i < 9; i ++ ) {

				this.coefficients[ i ].add( sh.coefficients[ i ] );

			}

			return this;

		}

		addScaledSH( sh, s ) {

			for ( let i = 0; i < 9; i ++ ) {

				this.coefficients[ i ].addScaledVector( sh.coefficients[ i ], s );

			}

			return this;

		}

		scale( s ) {

			for ( let i = 0; i < 9; i ++ ) {

				this.coefficients[ i ].multiplyScalar( s );

			}

			return this;

		}

		lerp( sh, alpha ) {

			for ( let i = 0; i < 9; i ++ ) {

				this.coefficients[ i ].lerp( sh.coefficients[ i ], alpha );

			}

			return this;

		}

		equals( sh ) {

			for ( let i = 0; i < 9; i ++ ) {

				if ( ! this.coefficients[ i ].equals( sh.coefficients[ i ] ) ) {

					return false;

				}

			}

			return true;

		}

		copy( sh ) {

			return this.set( sh.coefficients );

		}

		clone() {

			return new this.constructor().copy( this );

		}

		fromArray( array, offset = 0 ) {

			const coefficients = this.coefficients;

			for ( let i = 0; i < 9; i ++ ) {

				coefficients[ i ].fromArray( array, offset + ( i * 3 ) );

			}

			return this;

		}

		toArray( array = [], offset = 0 ) {

			const coefficients = this.coefficients;

			for ( let i = 0; i < 9; i ++ ) {

				coefficients[ i ].toArray( array, offset + ( i * 3 ) );

			}

			return array;

		}

		// evaluate the basis functions
		// shBasis is an Array[ 9 ]
		static getBasisAt( normal, shBasis ) {

			// normal is assumed to be unit length

			const x = normal.x, y = normal.y, z = normal.z;

			// band 0
			shBasis[ 0 ] = 0.282095;

			// band 1
			shBasis[ 1 ] = 0.488603 * y;
			shBasis[ 2 ] = 0.488603 * z;
			shBasis[ 3 ] = 0.488603 * x;

			// band 2
			shBasis[ 4 ] = 1.092548 * x * y;
			shBasis[ 5 ] = 1.092548 * y * z;
			shBasis[ 6 ] = 0.315392 * ( 3 * z * z - 1 );
			shBasis[ 7 ] = 1.092548 * x * z;
			shBasis[ 8 ] = 0.546274 * ( x * x - y * y );

		}

	}

	function LightProbe( sh, intensity ) {

		Light.call( this, undefined, intensity );

		this.type = 'LightProbe';

		this.sh = ( sh !== undefined ) ? sh : new SphericalHarmonics3();

	}

	LightProbe.prototype = Object.assign( Object.create( Light.prototype ), {

		constructor: LightProbe,

		isLightProbe: true,

		copy: function ( source ) {

			Light.prototype.copy.call( this, source );

			this.sh.copy( source.sh );

			return this;

		},

		fromJSON: function ( json ) {

			this.intensity = json.intensity; // TODO: Move this bit to Light.fromJSON();
			this.sh.fromArray( json.sh );

			return this;

		},

		toJSON: function ( meta ) {

			const data = Light.prototype.toJSON.call( this, meta );

			data.object.sh = this.sh.toArray();

			return data;

		}

	} );

	function MaterialLoader( manager ) {

		Loader.call( this, manager );

		this.textures = {};

	}

	MaterialLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: MaterialLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			const scope = this;

			const loader = new FileLoader( scope.manager );
			loader.setPath( scope.path );
			loader.setRequestHeader( scope.requestHeader );
			loader.setWithCredentials( scope.withCredentials );
			loader.load( url, function ( text ) {

				try {

					onLoad( scope.parse( JSON.parse( text ) ) );

				} catch ( e ) {

					if ( onError ) {

						onError( e );

					} else {

						console.error( e );

					}

					scope.manager.itemError( url );

				}

			}, onProgress, onError );

		},

		parse: function ( json ) {

			const textures = this.textures;

			function getTexture( name ) {

				if ( textures[ name ] === undefined ) {

					console.warn( 'THREE.MaterialLoader: Undefined texture', name );

				}

				return textures[ name ];

			}

			const material = new Materials[ json.type ]();

			if ( json.uuid !== undefined ) material.uuid = json.uuid;
			if ( json.name !== undefined ) material.name = json.name;
			if ( json.color !== undefined && material.color !== undefined ) material.color.setHex( json.color );
			if ( json.roughness !== undefined ) material.roughness = json.roughness;
			if ( json.metalness !== undefined ) material.metalness = json.metalness;
			if ( json.sheen !== undefined ) material.sheen = new Color().setHex( json.sheen );
			if ( json.emissive !== undefined && material.emissive !== undefined ) material.emissive.setHex( json.emissive );
			if ( json.specular !== undefined && material.specular !== undefined ) material.specular.setHex( json.specular );
			if ( json.shininess !== undefined ) material.shininess = json.shininess;
			if ( json.clearcoat !== undefined ) material.clearcoat = json.clearcoat;
			if ( json.clearcoatRoughness !== undefined ) material.clearcoatRoughness = json.clearcoatRoughness;
			if ( json.fog !== undefined ) material.fog = json.fog;
			if ( json.flatShading !== undefined ) material.flatShading = json.flatShading;
			if ( json.blending !== undefined ) material.blending = json.blending;
			if ( json.combine !== undefined ) material.combine = json.combine;
			if ( json.side !== undefined ) material.side = json.side;
			if ( json.opacity !== undefined ) material.opacity = json.opacity;
			if ( json.transparent !== undefined ) material.transparent = json.transparent;
			if ( json.alphaTest !== undefined ) material.alphaTest = json.alphaTest;
			if ( json.depthTest !== undefined ) material.depthTest = json.depthTest;
			if ( json.depthWrite !== undefined ) material.depthWrite = json.depthWrite;
			if ( json.colorWrite !== undefined ) material.colorWrite = json.colorWrite;

			if ( json.stencilWrite !== undefined ) material.stencilWrite = json.stencilWrite;
			if ( json.stencilWriteMask !== undefined ) material.stencilWriteMask = json.stencilWriteMask;
			if ( json.stencilFunc !== undefined ) material.stencilFunc = json.stencilFunc;
			if ( json.stencilRef !== undefined ) material.stencilRef = json.stencilRef;
			if ( json.stencilFuncMask !== undefined ) material.stencilFuncMask = json.stencilFuncMask;
			if ( json.stencilFail !== undefined ) material.stencilFail = json.stencilFail;
			if ( json.stencilZFail !== undefined ) material.stencilZFail = json.stencilZFail;
			if ( json.stencilZPass !== undefined ) material.stencilZPass = json.stencilZPass;

			if ( json.wireframe !== undefined ) material.wireframe = json.wireframe;
			if ( json.wireframeLinewidth !== undefined ) material.wireframeLinewidth = json.wireframeLinewidth;
			if ( json.wireframeLinecap !== undefined ) material.wireframeLinecap = json.wireframeLinecap;
			if ( json.wireframeLinejoin !== undefined ) material.wireframeLinejoin = json.wireframeLinejoin;

			if ( json.rotation !== undefined ) material.rotation = json.rotation;

			if ( json.linewidth !== 1 ) material.linewidth = json.linewidth;
			if ( json.dashSize !== undefined ) material.dashSize = json.dashSize;
			if ( json.gapSize !== undefined ) material.gapSize = json.gapSize;
			if ( json.scale !== undefined ) material.scale = json.scale;

			if ( json.polygonOffset !== undefined ) material.polygonOffset = json.polygonOffset;
			if ( json.polygonOffsetFactor !== undefined ) material.polygonOffsetFactor = json.polygonOffsetFactor;
			if ( json.polygonOffsetUnits !== undefined ) material.polygonOffsetUnits = json.polygonOffsetUnits;

			if ( json.skinning !== undefined ) material.skinning = json.skinning;
			if ( json.morphTargets !== undefined ) material.morphTargets = json.morphTargets;
			if ( json.morphNormals !== undefined ) material.morphNormals = json.morphNormals;
			if ( json.dithering !== undefined ) material.dithering = json.dithering;

			if ( json.vertexTangents !== undefined ) material.vertexTangents = json.vertexTangents;

			if ( json.visible !== undefined ) material.visible = json.visible;

			if ( json.toneMapped !== undefined ) material.toneMapped = json.toneMapped;

			if ( json.userData !== undefined ) material.userData = json.userData;

			if ( json.vertexColors !== undefined ) {

				if ( typeof json.vertexColors === 'number' ) {

					material.vertexColors = ( json.vertexColors > 0 ) ? true : false;

				} else {

					material.vertexColors = json.vertexColors;

				}

			}

			// Shader Material

			if ( json.uniforms !== undefined ) {

				for ( const name in json.uniforms ) {

					const uniform = json.uniforms[ name ];

					material.uniforms[ name ] = {};

					switch ( uniform.type ) {

						case 't':
							material.uniforms[ name ].value = getTexture( uniform.value );
							break;

						case 'c':
							material.uniforms[ name ].value = new Color().setHex( uniform.value );
							break;

						case 'v2':
							material.uniforms[ name ].value = new Vector2().fromArray( uniform.value );
							break;

						case 'v3':
							material.uniforms[ name ].value = new Vector3().fromArray( uniform.value );
							break;

						case 'v4':
							material.uniforms[ name ].value = new Vector4().fromArray( uniform.value );
							break;

						case 'm3':
							material.uniforms[ name ].value = new Matrix3().fromArray( uniform.value );
							break;

						case 'm4':
							material.uniforms[ name ].value = new Matrix4().fromArray( uniform.value );
							break;

						default:
							material.uniforms[ name ].value = uniform.value;

					}

				}

			}

			if ( json.defines !== undefined ) material.defines = json.defines;
			if ( json.vertexShader !== undefined ) material.vertexShader = json.vertexShader;
			if ( json.fragmentShader !== undefined ) material.fragmentShader = json.fragmentShader;

			if ( json.extensions !== undefined ) {

				for ( const key in json.extensions ) {

					material.extensions[ key ] = json.extensions[ key ];

				}

			}

			// Deprecated

			if ( json.shading !== undefined ) material.flatShading = json.shading === 1; // THREE.FlatShading

			// for PointsMaterial

			if ( json.size !== undefined ) material.size = json.size;
			if ( json.sizeAttenuation !== undefined ) material.sizeAttenuation = json.sizeAttenuation;

			// maps

			if ( json.map !== undefined ) material.map = getTexture( json.map );
			if ( json.matcap !== undefined ) material.matcap = getTexture( json.matcap );

			if ( json.alphaMap !== undefined ) material.alphaMap = getTexture( json.alphaMap );

			if ( json.bumpMap !== undefined ) material.bumpMap = getTexture( json.bumpMap );
			if ( json.bumpScale !== undefined ) material.bumpScale = json.bumpScale;

			if ( json.normalMap !== undefined ) material.normalMap = getTexture( json.normalMap );
			if ( json.normalMapType !== undefined ) material.normalMapType = json.normalMapType;
			if ( json.normalScale !== undefined ) {

				let normalScale = json.normalScale;

				if ( Array.isArray( normalScale ) === false ) {

					// Blender exporter used to export a scalar. See #7459

					normalScale = [ normalScale, normalScale ];

				}

				material.normalScale = new Vector2().fromArray( normalScale );

			}

			if ( json.displacementMap !== undefined ) material.displacementMap = getTexture( json.displacementMap );
			if ( json.displacementScale !== undefined ) material.displacementScale = json.displacementScale;
			if ( json.displacementBias !== undefined ) material.displacementBias = json.displacementBias;

			if ( json.roughnessMap !== undefined ) material.roughnessMap = getTexture( json.roughnessMap );
			if ( json.metalnessMap !== undefined ) material.metalnessMap = getTexture( json.metalnessMap );

			if ( json.emissiveMap !== undefined ) material.emissiveMap = getTexture( json.emissiveMap );
			if ( json.emissiveIntensity !== undefined ) material.emissiveIntensity = json.emissiveIntensity;

			if ( json.specularMap !== undefined ) material.specularMap = getTexture( json.specularMap );

			if ( json.envMap !== undefined ) material.envMap = getTexture( json.envMap );
			if ( json.envMapIntensity !== undefined ) material.envMapIntensity = json.envMapIntensity;

			if ( json.reflectivity !== undefined ) material.reflectivity = json.reflectivity;
			if ( json.refractionRatio !== undefined ) material.refractionRatio = json.refractionRatio;

			if ( json.lightMap !== undefined ) material.lightMap = getTexture( json.lightMap );
			if ( json.lightMapIntensity !== undefined ) material.lightMapIntensity = json.lightMapIntensity;

			if ( json.aoMap !== undefined ) material.aoMap = getTexture( json.aoMap );
			if ( json.aoMapIntensity !== undefined ) material.aoMapIntensity = json.aoMapIntensity;

			if ( json.gradientMap !== undefined ) material.gradientMap = getTexture( json.gradientMap );

			if ( json.clearcoatMap !== undefined ) material.clearcoatMap = getTexture( json.clearcoatMap );
			if ( json.clearcoatRoughnessMap !== undefined ) material.clearcoatRoughnessMap = getTexture( json.clearcoatRoughnessMap );
			if ( json.clearcoatNormalMap !== undefined ) material.clearcoatNormalMap = getTexture( json.clearcoatNormalMap );
			if ( json.clearcoatNormalScale !== undefined ) material.clearcoatNormalScale = new Vector2().fromArray( json.clearcoatNormalScale );

			if ( json.transmission !== undefined ) material.transmission = json.transmission;
			if ( json.transmissionMap !== undefined ) material.transmissionMap = getTexture( json.transmissionMap );

			return material;

		},

		setTextures: function ( value ) {

			this.textures = value;
			return this;

		}

	} );

	const LoaderUtils = {

		decodeText: function ( array ) {

			if ( typeof TextDecoder !== 'undefined' ) {

				return new TextDecoder().decode( array );

			}

			// Avoid the String.fromCharCode.apply(null, array) shortcut, which
			// throws a "maximum call stack size exceeded" error for large arrays.

			let s = '';

			for ( let i = 0, il = array.length; i < il; i ++ ) {

				// Implicitly assumes little-endian.
				s += String.fromCharCode( array[ i ] );

			}

			try {

				// merges multi-byte utf-8 characters.

				return decodeURIComponent( escape( s ) );

			} catch ( e ) { // see #16358

				return s;

			}

		},

		extractUrlBase: function ( url ) {

			const index = url.lastIndexOf( '/' );

			if ( index === - 1 ) return './';

			return url.substr( 0, index + 1 );

		}

	};

	function InstancedBufferGeometry() {

		BufferGeometry.call( this );

		this.type = 'InstancedBufferGeometry';
		this.instanceCount = Infinity;

	}

	InstancedBufferGeometry.prototype = Object.assign( Object.create( BufferGeometry.prototype ), {

		constructor: InstancedBufferGeometry,

		isInstancedBufferGeometry: true,

		copy: function ( source ) {

			BufferGeometry.prototype.copy.call( this, source );

			this.instanceCount = source.instanceCount;

			return this;

		},

		clone: function () {

			return new this.constructor().copy( this );

		},

		toJSON: function () {

			const data = BufferGeometry.prototype.toJSON.call( this );

			data.instanceCount = this.instanceCount;

			data.isInstancedBufferGeometry = true;

			return data;

		}

	} );

	function InstancedBufferAttribute( array, itemSize, normalized, meshPerAttribute ) {

		if ( typeof ( normalized ) === 'number' ) {

			meshPerAttribute = normalized;

			normalized = false;

			console.error( 'THREE.InstancedBufferAttribute: The constructor now expects normalized as the third argument.' );

		}

		BufferAttribute.call( this, array, itemSize, normalized );

		this.meshPerAttribute = meshPerAttribute || 1;

	}

	InstancedBufferAttribute.prototype = Object.assign( Object.create( BufferAttribute.prototype ), {

		constructor: InstancedBufferAttribute,

		isInstancedBufferAttribute: true,

		copy: function ( source ) {

			BufferAttribute.prototype.copy.call( this, source );

			this.meshPerAttribute = source.meshPerAttribute;

			return this;

		},

		toJSON: function ()	{

			const data = BufferAttribute.prototype.toJSON.call( this );

			data.meshPerAttribute = this.meshPerAttribute;

			data.isInstancedBufferAttribute = true;

			return data;

		}

	} );

	function BufferGeometryLoader( manager ) {

		Loader.call( this, manager );

	}

	BufferGeometryLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: BufferGeometryLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			const scope = this;

			const loader = new FileLoader( scope.manager );
			loader.setPath( scope.path );
			loader.setRequestHeader( scope.requestHeader );
			loader.setWithCredentials( scope.withCredentials );
			loader.load( url, function ( text ) {

				try {

					onLoad( scope.parse( JSON.parse( text ) ) );

				} catch ( e ) {

					if ( onError ) {

						onError( e );

					} else {

						console.error( e );

					}

					scope.manager.itemError( url );

				}

			}, onProgress, onError );

		},

		parse: function ( json ) {

			const interleavedBufferMap = {};
			const arrayBufferMap = {};

			function getInterleavedBuffer( json, uuid ) {

				if ( interleavedBufferMap[ uuid ] !== undefined ) return interleavedBufferMap[ uuid ];

				const interleavedBuffers = json.interleavedBuffers;
				const interleavedBuffer = interleavedBuffers[ uuid ];

				const buffer = getArrayBuffer( json, interleavedBuffer.buffer );

				const array = getTypedArray( interleavedBuffer.type, buffer );
				const ib = new InterleavedBuffer( array, interleavedBuffer.stride );
				ib.uuid = interleavedBuffer.uuid;

				interleavedBufferMap[ uuid ] = ib;

				return ib;

			}

			function getArrayBuffer( json, uuid ) {

				if ( arrayBufferMap[ uuid ] !== undefined ) return arrayBufferMap[ uuid ];

				const arrayBuffers = json.arrayBuffers;
				const arrayBuffer = arrayBuffers[ uuid ];

				const ab = new Uint32Array( arrayBuffer ).buffer;

				arrayBufferMap[ uuid ] = ab;

				return ab;

			}

			const geometry = json.isInstancedBufferGeometry ? new InstancedBufferGeometry() : new BufferGeometry();

			const index = json.data.index;

			if ( index !== undefined ) {

				const typedArray = getTypedArray( index.type, index.array );
				geometry.setIndex( new BufferAttribute( typedArray, 1 ) );

			}

			const attributes = json.data.attributes;

			for ( const key in attributes ) {

				const attribute = attributes[ key ];
				let bufferAttribute;

				if ( attribute.isInterleavedBufferAttribute ) {

					const interleavedBuffer = getInterleavedBuffer( json.data, attribute.data );
					bufferAttribute = new InterleavedBufferAttribute( interleavedBuffer, attribute.itemSize, attribute.offset, attribute.normalized );

				} else {

					const typedArray = getTypedArray( attribute.type, attribute.array );
					const bufferAttributeConstr = attribute.isInstancedBufferAttribute ? InstancedBufferAttribute : BufferAttribute;
					bufferAttribute = new bufferAttributeConstr( typedArray, attribute.itemSize, attribute.normalized );

				}

				if ( attribute.name !== undefined ) bufferAttribute.name = attribute.name;
				geometry.setAttribute( key, bufferAttribute );

			}

			const morphAttributes = json.data.morphAttributes;

			if ( morphAttributes ) {

				for ( const key in morphAttributes ) {

					const attributeArray = morphAttributes[ key ];

					const array = [];

					for ( let i = 0, il = attributeArray.length; i < il; i ++ ) {

						const attribute = attributeArray[ i ];
						let bufferAttribute;

						if ( attribute.isInterleavedBufferAttribute ) {

							const interleavedBuffer = getInterleavedBuffer( json.data, attribute.data );
							bufferAttribute = new InterleavedBufferAttribute( interleavedBuffer, attribute.itemSize, attribute.offset, attribute.normalized );

						} else {

							const typedArray = getTypedArray( attribute.type, attribute.array );
							bufferAttribute = new BufferAttribute( typedArray, attribute.itemSize, attribute.normalized );

						}

						if ( attribute.name !== undefined ) bufferAttribute.name = attribute.name;
						array.push( bufferAttribute );

					}

					geometry.morphAttributes[ key ] = array;

				}

			}

			const morphTargetsRelative = json.data.morphTargetsRelative;

			if ( morphTargetsRelative ) {

				geometry.morphTargetsRelative = true;

			}

			const groups = json.data.groups || json.data.drawcalls || json.data.offsets;

			if ( groups !== undefined ) {

				for ( let i = 0, n = groups.length; i !== n; ++ i ) {

					const group = groups[ i ];

					geometry.addGroup( group.start, group.count, group.materialIndex );

				}

			}

			const boundingSphere = json.data.boundingSphere;

			if ( boundingSphere !== undefined ) {

				const center = new Vector3();

				if ( boundingSphere.center !== undefined ) {

					center.fromArray( boundingSphere.center );

				}

				geometry.boundingSphere = new Sphere( center, boundingSphere.radius );

			}

			if ( json.name ) geometry.name = json.name;
			if ( json.userData ) geometry.userData = json.userData;

			return geometry;

		}

	} );

	class ObjectLoader extends Loader {

		constructor( manager ) {

			super( manager );

		}

		load( url, onLoad, onProgress, onError ) {

			const scope = this;

			const path = ( this.path === '' ) ? LoaderUtils.extractUrlBase( url ) : this.path;
			this.resourcePath = this.resourcePath || path;

			const loader = new FileLoader( this.manager );
			loader.setPath( this.path );
			loader.setRequestHeader( this.requestHeader );
			loader.setWithCredentials( this.withCredentials );
			loader.load( url, function ( text ) {

				let json = null;

				try {

					json = JSON.parse( text );

				} catch ( error ) {

					if ( onError !== undefined ) onError( error );

					console.error( 'THREE:ObjectLoader: Can\'t parse ' + url + '.', error.message );

					return;

				}

				const metadata = json.metadata;

				if ( metadata === undefined || metadata.type === undefined || metadata.type.toLowerCase() === 'geometry' ) {

					console.error( 'THREE.ObjectLoader: Can\'t load ' + url );
					return;

				}

				scope.parse( json, onLoad );

			}, onProgress, onError );

		}

		parse( json, onLoad ) {

			const animations = this.parseAnimations( json.animations );
			const shapes = this.parseShapes( json.shapes );
			const geometries = this.parseGeometries( json.geometries, shapes );

			const images = this.parseImages( json.images, function () {

				if ( onLoad !== undefined ) onLoad( object );

			} );

			const textures = this.parseTextures( json.textures, images );
			const materials = this.parseMaterials( json.materials, textures );

			const object = this.parseObject( json.object, geometries, materials, animations );
			const skeletons = this.parseSkeletons( json.skeletons, object );

			this.bindSkeletons( object, skeletons );

			//

			if ( onLoad !== undefined ) {

				let hasImages = false;

				for ( const uuid in images ) {

					if ( images[ uuid ] instanceof HTMLImageElement ) {

						hasImages = true;
						break;

					}

				}

				if ( hasImages === false ) onLoad( object );

			}

			return object;

		}

		parseShapes( json ) {

			const shapes = {};

			if ( json !== undefined ) {

				for ( let i = 0, l = json.length; i < l; i ++ ) {

					const shape = new Shape().fromJSON( json[ i ] );

					shapes[ shape.uuid ] = shape;

				}

			}

			return shapes;

		}

		parseSkeletons( json, object ) {

			const skeletons = {};
			const bones = {};

			// generate bone lookup table

			object.traverse( function ( child ) {

				if ( child.isBone ) bones[ child.uuid ] = child;

			} );

			// create skeletons

			if ( json !== undefined ) {

				for ( let i = 0, l = json.length; i < l; i ++ ) {

					const skeleton = new Skeleton().fromJSON( json[ i ], bones );

					skeletons[ skeleton.uuid ] = skeleton;

				}

			}

			return skeletons;

		}

		parseGeometries( json, shapes ) {

			const geometries = {};
			let geometryShapes;

			if ( json !== undefined ) {

				const bufferGeometryLoader = new BufferGeometryLoader();

				for ( let i = 0, l = json.length; i < l; i ++ ) {

					let geometry;
					const data = json[ i ];

					switch ( data.type ) {

						case 'PlaneGeometry':
						case 'PlaneBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.width,
								data.height,
								data.widthSegments,
								data.heightSegments
							);

							break;

						case 'BoxGeometry':
						case 'BoxBufferGeometry':
						case 'CubeGeometry': // backwards compatible

							geometry = new Geometries[ data.type ](
								data.width,
								data.height,
								data.depth,
								data.widthSegments,
								data.heightSegments,
								data.depthSegments
							);

							break;

						case 'CircleGeometry':
						case 'CircleBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.radius,
								data.segments,
								data.thetaStart,
								data.thetaLength
							);

							break;

						case 'CylinderGeometry':
						case 'CylinderBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.radiusTop,
								data.radiusBottom,
								data.height,
								data.radialSegments,
								data.heightSegments,
								data.openEnded,
								data.thetaStart,
								data.thetaLength
							);

							break;

						case 'ConeGeometry':
						case 'ConeBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.radius,
								data.height,
								data.radialSegments,
								data.heightSegments,
								data.openEnded,
								data.thetaStart,
								data.thetaLength
							);

							break;

						case 'SphereGeometry':
						case 'SphereBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.radius,
								data.widthSegments,
								data.heightSegments,
								data.phiStart,
								data.phiLength,
								data.thetaStart,
								data.thetaLength
							);

							break;

						case 'DodecahedronGeometry':
						case 'DodecahedronBufferGeometry':
						case 'IcosahedronGeometry':
						case 'IcosahedronBufferGeometry':
						case 'OctahedronGeometry':
						case 'OctahedronBufferGeometry':
						case 'TetrahedronGeometry':
						case 'TetrahedronBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.radius,
								data.detail
							);

							break;

						case 'RingGeometry':
						case 'RingBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.innerRadius,
								data.outerRadius,
								data.thetaSegments,
								data.phiSegments,
								data.thetaStart,
								data.thetaLength
							);

							break;

						case 'TorusGeometry':
						case 'TorusBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.radius,
								data.tube,
								data.radialSegments,
								data.tubularSegments,
								data.arc
							);

							break;

						case 'TorusKnotGeometry':
						case 'TorusKnotBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.radius,
								data.tube,
								data.tubularSegments,
								data.radialSegments,
								data.p,
								data.q
							);

							break;

						case 'TubeGeometry':
						case 'TubeBufferGeometry':

							// This only works for built-in curves (e.g. CatmullRomCurve3).
							// User defined curves or instances of CurvePath will not be deserialized.
							geometry = new Geometries[ data.type ](
								new Curves[ data.path.type ]().fromJSON( data.path ),
								data.tubularSegments,
								data.radius,
								data.radialSegments,
								data.closed
							);

							break;

						case 'LatheGeometry':
						case 'LatheBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.points,
								data.segments,
								data.phiStart,
								data.phiLength
							);

							break;

						case 'PolyhedronGeometry':
						case 'PolyhedronBufferGeometry':

							geometry = new Geometries[ data.type ](
								data.vertices,
								data.indices,
								data.radius,
								data.details
							);

							break;

						case 'ShapeGeometry':
						case 'ShapeBufferGeometry':

							geometryShapes = [];

							for ( let j = 0, jl = data.shapes.length; j < jl; j ++ ) {

								const shape = shapes[ data.shapes[ j ] ];

								geometryShapes.push( shape );

							}

							geometry = new Geometries[ data.type ](
								geometryShapes,
								data.curveSegments
							);

							break;


						case 'ExtrudeGeometry':
						case 'ExtrudeBufferGeometry':

							geometryShapes = [];

							for ( let j = 0, jl = data.shapes.length; j < jl; j ++ ) {

								const shape = shapes[ data.shapes[ j ] ];

								geometryShapes.push( shape );

							}

							const extrudePath = data.options.extrudePath;

							if ( extrudePath !== undefined ) {

								data.options.extrudePath = new Curves[ extrudePath.type ]().fromJSON( extrudePath );

							}

							geometry = new Geometries[ data.type ](
								geometryShapes,
								data.options
							);

							break;

						case 'BufferGeometry':
						case 'InstancedBufferGeometry':

							geometry = bufferGeometryLoader.parse( data );

							break;

						case 'Geometry':

							console.error( 'THREE.ObjectLoader: Loading "Geometry" is not supported anymore.' );

							break;

						default:

							console.warn( 'THREE.ObjectLoader: Unsupported geometry type "' + data.type + '"' );

							continue;

					}

					geometry.uuid = data.uuid;

					if ( data.name !== undefined ) geometry.name = data.name;
					if ( geometry.isBufferGeometry === true && data.userData !== undefined ) geometry.userData = data.userData;

					geometries[ data.uuid ] = geometry;

				}

			}

			return geometries;

		}

		parseMaterials( json, textures ) {

			const cache = {}; // MultiMaterial
			const materials = {};

			if ( json !== undefined ) {

				const loader = new MaterialLoader();
				loader.setTextures( textures );

				for ( let i = 0, l = json.length; i < l; i ++ ) {

					const data = json[ i ];

					if ( data.type === 'MultiMaterial' ) {

						// Deprecated

						const array = [];

						for ( let j = 0; j < data.materials.length; j ++ ) {

							const material = data.materials[ j ];

							if ( cache[ material.uuid ] === undefined ) {

								cache[ material.uuid ] = loader.parse( material );

							}

							array.push( cache[ material.uuid ] );

						}

						materials[ data.uuid ] = array;

					} else {

						if ( cache[ data.uuid ] === undefined ) {

							cache[ data.uuid ] = loader.parse( data );

						}

						materials[ data.uuid ] = cache[ data.uuid ];

					}

				}

			}

			return materials;

		}

		parseAnimations( json ) {

			const animations = {};

			if ( json !== undefined ) {

				for ( let i = 0; i < json.length; i ++ ) {

					const data = json[ i ];

					const clip = AnimationClip.parse( data );

					animations[ clip.uuid ] = clip;

				}

			}

			return animations;

		}

		parseImages( json, onLoad ) {

			const scope = this;
			const images = {};

			let loader;

			function loadImage( url ) {

				scope.manager.itemStart( url );

				return loader.load( url, function () {

					scope.manager.itemEnd( url );

				}, undefined, function () {

					scope.manager.itemError( url );
					scope.manager.itemEnd( url );

				} );

			}

			function deserializeImage( image ) {

				if ( typeof image === 'string' ) {

					const url = image;

					const path = /^(\/\/)|([a-z]+:(\/\/)?)/i.test( url ) ? url : scope.resourcePath + url;

					return loadImage( path );

				} else {

					if ( image.data ) {

						return {
							data: getTypedArray( image.type, image.data ),
							width: image.width,
							height: image.height
						};

					} else {

						return null;

					}

				}

			}

			if ( json !== undefined && json.length > 0 ) {

				const manager = new LoadingManager( onLoad );

				loader = new ImageLoader( manager );
				loader.setCrossOrigin( this.crossOrigin );

				for ( let i = 0, il = json.length; i < il; i ++ ) {

					const image = json[ i ];
					const url = image.url;

					if ( Array.isArray( url ) ) {

						// load array of images e.g CubeTexture

						images[ image.uuid ] = [];

						for ( let j = 0, jl = url.length; j < jl; j ++ ) {

							const currentUrl = url[ j ];

							const deserializedImage = deserializeImage( currentUrl );

							if ( deserializedImage !== null ) {

								if ( deserializedImage instanceof HTMLImageElement ) {

									images[ image.uuid ].push( deserializedImage );

								} else {

									// special case: handle array of data textures for cube textures

									images[ image.uuid ].push( new DataTexture( deserializedImage.data, deserializedImage.width, deserializedImage.height ) );

								}

							}

						}

					} else {

						// load single image

						const deserializedImage = deserializeImage( image.url );

						if ( deserializedImage !== null ) {

							images[ image.uuid ] = deserializedImage;

						}

					}

				}

			}

			return images;

		}

		parseTextures( json, images ) {

			function parseConstant( value, type ) {

				if ( typeof value === 'number' ) return value;

				console.warn( 'THREE.ObjectLoader.parseTexture: Constant should be in numeric form.', value );

				return type[ value ];

			}

			const textures = {};

			if ( json !== undefined ) {

				for ( let i = 0, l = json.length; i < l; i ++ ) {

					const data = json[ i ];

					if ( data.image === undefined ) {

						console.warn( 'THREE.ObjectLoader: No "image" specified for', data.uuid );

					}

					if ( images[ data.image ] === undefined ) {

						console.warn( 'THREE.ObjectLoader: Undefined image', data.image );

					}

					let texture;
					const image = images[ data.image ];

					if ( Array.isArray( image ) ) {

						texture = new CubeTexture( image );

						if ( image.length === 6 ) texture.needsUpdate = true;

					} else {

						if ( image && image.data ) {

							texture = new DataTexture( image.data, image.width, image.height );

						} else {

							texture = new Texture( image );

						}

						if ( image ) texture.needsUpdate = true; // textures can have undefined image data

					}

					texture.uuid = data.uuid;

					if ( data.name !== undefined ) texture.name = data.name;

					if ( data.mapping !== undefined ) texture.mapping = parseConstant( data.mapping, TEXTURE_MAPPING );

					if ( data.offset !== undefined ) texture.offset.fromArray( data.offset );
					if ( data.repeat !== undefined ) texture.repeat.fromArray( data.repeat );
					if ( data.center !== undefined ) texture.center.fromArray( data.center );
					if ( data.rotation !== undefined ) texture.rotation = data.rotation;

					if ( data.wrap !== undefined ) {

						texture.wrapS = parseConstant( data.wrap[ 0 ], TEXTURE_WRAPPING );
						texture.wrapT = parseConstant( data.wrap[ 1 ], TEXTURE_WRAPPING );

					}

					if ( data.format !== undefined ) texture.format = data.format;
					if ( data.type !== undefined ) texture.type = data.type;
					if ( data.encoding !== undefined ) texture.encoding = data.encoding;

					if ( data.minFilter !== undefined ) texture.minFilter = parseConstant( data.minFilter, TEXTURE_FILTER );
					if ( data.magFilter !== undefined ) texture.magFilter = parseConstant( data.magFilter, TEXTURE_FILTER );
					if ( data.anisotropy !== undefined ) texture.anisotropy = data.anisotropy;

					if ( data.flipY !== undefined ) texture.flipY = data.flipY;

					if ( data.premultiplyAlpha !== undefined ) texture.premultiplyAlpha = data.premultiplyAlpha;
					if ( data.unpackAlignment !== undefined ) texture.unpackAlignment = data.unpackAlignment;

					textures[ data.uuid ] = texture;

				}

			}

			return textures;

		}

		parseObject( data, geometries, materials, animations ) {

			let object;

			function getGeometry( name ) {

				if ( geometries[ name ] === undefined ) {

					console.warn( 'THREE.ObjectLoader: Undefined geometry', name );

				}

				return geometries[ name ];

			}

			function getMaterial( name ) {

				if ( name === undefined ) return undefined;

				if ( Array.isArray( name ) ) {

					const array = [];

					for ( let i = 0, l = name.length; i < l; i ++ ) {

						const uuid = name[ i ];

						if ( materials[ uuid ] === undefined ) {

							console.warn( 'THREE.ObjectLoader: Undefined material', uuid );

						}

						array.push( materials[ uuid ] );

					}

					return array;

				}

				if ( materials[ name ] === undefined ) {

					console.warn( 'THREE.ObjectLoader: Undefined material', name );

				}

				return materials[ name ];

			}

			let geometry, material;

			switch ( data.type ) {

				case 'Scene':

					object = new Scene();

					if ( data.background !== undefined ) {

						if ( Number.isInteger( data.background ) ) {

							object.background = new Color( data.background );

						}

					}

					if ( data.fog !== undefined ) {

						if ( data.fog.type === 'Fog' ) {

							object.fog = new Fog( data.fog.color, data.fog.near, data.fog.far );

						} else if ( data.fog.type === 'FogExp2' ) {

							object.fog = new FogExp2( data.fog.color, data.fog.density );

						}

					}

					break;

				case 'PerspectiveCamera':

					object = new PerspectiveCamera( data.fov, data.aspect, data.near, data.far );

					if ( data.focus !== undefined ) object.focus = data.focus;
					if ( data.zoom !== undefined ) object.zoom = data.zoom;
					if ( data.filmGauge !== undefined ) object.filmGauge = data.filmGauge;
					if ( data.filmOffset !== undefined ) object.filmOffset = data.filmOffset;
					if ( data.view !== undefined ) object.view = Object.assign( {}, data.view );

					break;

				case 'OrthographicCamera':

					object = new OrthographicCamera( data.left, data.right, data.top, data.bottom, data.near, data.far );

					if ( data.zoom !== undefined ) object.zoom = data.zoom;
					if ( data.view !== undefined ) object.view = Object.assign( {}, data.view );

					break;

				case 'AmbientLight':

					object = new AmbientLight( data.color, data.intensity );

					break;

				case 'DirectionalLight':

					object = new DirectionalLight( data.color, data.intensity );

					break;

				case 'PointLight':

					object = new PointLight( data.color, data.intensity, data.distance, data.decay );

					break;

				case 'RectAreaLight':

					object = new RectAreaLight( data.color, data.intensity, data.width, data.height );

					break;

				case 'SpotLight':

					object = new SpotLight( data.color, data.intensity, data.distance, data.angle, data.penumbra, data.decay );

					break;

				case 'HemisphereLight':

					object = new HemisphereLight( data.color, data.groundColor, data.intensity );

					break;

				case 'LightProbe':

					object = new LightProbe().fromJSON( data );

					break;

				case 'SkinnedMesh':

					geometry = getGeometry( data.geometry );
				 	material = getMaterial( data.material );

					object = new SkinnedMesh( geometry, material );

					if ( data.bindMode !== undefined ) object.bindMode = data.bindMode;
					if ( data.bindMatrix !== undefined ) object.bindMatrix.fromArray( data.bindMatrix );
					if ( data.skeleton !== undefined ) object.skeleton = data.skeleton;

					break;

				case 'Mesh':

					geometry = getGeometry( data.geometry );
					material = getMaterial( data.material );

					object = new Mesh( geometry, material );

					break;

				case 'InstancedMesh':

					geometry = getGeometry( data.geometry );
					material = getMaterial( data.material );
					const count = data.count;
					const instanceMatrix = data.instanceMatrix;

					object = new InstancedMesh( geometry, material, count );
					object.instanceMatrix = new BufferAttribute( new Float32Array( instanceMatrix.array ), 16 );

					break;

				case 'LOD':

					object = new LOD();

					break;

				case 'Line':

					object = new Line( getGeometry( data.geometry ), getMaterial( data.material ) );

					break;

				case 'LineLoop':

					object = new LineLoop( getGeometry( data.geometry ), getMaterial( data.material ) );

					break;

				case 'LineSegments':

					object = new LineSegments( getGeometry( data.geometry ), getMaterial( data.material ) );

					break;

				case 'PointCloud':
				case 'Points':

					object = new Points( getGeometry( data.geometry ), getMaterial( data.material ) );

					break;

				case 'Sprite':

					object = new Sprite( getMaterial( data.material ) );

					break;

				case 'Group':

					object = new Group();

					break;

				case 'Bone':

					object = new Bone();

					break;

				default:

					object = new Object3D();

			}

			object.uuid = data.uuid;

			if ( data.name !== undefined ) object.name = data.name;

			if ( data.matrix !== undefined ) {

				object.matrix.fromArray( data.matrix );

				if ( data.matrixAutoUpdate !== undefined ) object.matrixAutoUpdate = data.matrixAutoUpdate;
				if ( object.matrixAutoUpdate ) object.matrix.decompose( object.position, object.quaternion, object.scale );

			} else {

				if ( data.position !== undefined ) object.position.fromArray( data.position );
				if ( data.rotation !== undefined ) object.rotation.fromArray( data.rotation );
				if ( data.quaternion !== undefined ) object.quaternion.fromArray( data.quaternion );
				if ( data.scale !== undefined ) object.scale.fromArray( data.scale );

			}

			if ( data.castShadow !== undefined ) object.castShadow = data.castShadow;
			if ( data.receiveShadow !== undefined ) object.receiveShadow = data.receiveShadow;

			if ( data.shadow ) {

				if ( data.shadow.bias !== undefined ) object.shadow.bias = data.shadow.bias;
				if ( data.shadow.normalBias !== undefined ) object.shadow.normalBias = data.shadow.normalBias;
				if ( data.shadow.radius !== undefined ) object.shadow.radius = data.shadow.radius;
				if ( data.shadow.mapSize !== undefined ) object.shadow.mapSize.fromArray( data.shadow.mapSize );
				if ( data.shadow.camera !== undefined ) object.shadow.camera = this.parseObject( data.shadow.camera );

			}

			if ( data.visible !== undefined ) object.visible = data.visible;
			if ( data.frustumCulled !== undefined ) object.frustumCulled = data.frustumCulled;
			if ( data.renderOrder !== undefined ) object.renderOrder = data.renderOrder;
			if ( data.userData !== undefined ) object.userData = data.userData;
			if ( data.layers !== undefined ) object.layers.mask = data.layers;

			if ( data.children !== undefined ) {

				const children = data.children;

				for ( let i = 0; i < children.length; i ++ ) {

					object.add( this.parseObject( children[ i ], geometries, materials, animations ) );

				}

			}

			if ( data.animations !== undefined ) {

				const objectAnimations = data.animations;

				for ( let i = 0; i < objectAnimations.length; i ++ ) {

					const uuid = objectAnimations[ i ];

					object.animations.push( animations[ uuid ] );

				}

			}

			if ( data.type === 'LOD' ) {

				if ( data.autoUpdate !== undefined ) object.autoUpdate = data.autoUpdate;

				const levels = data.levels;

				for ( let l = 0; l < levels.length; l ++ ) {

					const level = levels[ l ];
					const child = object.getObjectByProperty( 'uuid', level.object );

					if ( child !== undefined ) {

						object.addLevel( child, level.distance );

					}

				}

			}

			return object;

		}

		bindSkeletons( object, skeletons ) {

			if ( Object.keys( skeletons ).length === 0 ) return;

			object.traverse( function ( child ) {

				if ( child.isSkinnedMesh === true && child.skeleton !== undefined ) {

					const skeleton = skeletons[ child.skeleton ];

					if ( skeleton === undefined ) {

						console.warn( 'THREE.ObjectLoader: No skeleton found with UUID:', child.skeleton );

					} else {

						child.bind( skeleton, child.bindMatrix );

					}

				}

			} );

		}

		/* DEPRECATED */

		setTexturePath( value ) {

			console.warn( 'THREE.ObjectLoader: .setTexturePath() has been renamed to .setResourcePath().' );
			return this.setResourcePath( value );

		}

	}

	const TEXTURE_MAPPING = {
		UVMapping: UVMapping,
		CubeReflectionMapping: CubeReflectionMapping,
		CubeRefractionMapping: CubeRefractionMapping,
		EquirectangularReflectionMapping: EquirectangularReflectionMapping,
		EquirectangularRefractionMapping: EquirectangularRefractionMapping,
		CubeUVReflectionMapping: CubeUVReflectionMapping,
		CubeUVRefractionMapping: CubeUVRefractionMapping
	};

	const TEXTURE_WRAPPING = {
		RepeatWrapping: RepeatWrapping,
		ClampToEdgeWrapping: ClampToEdgeWrapping,
		MirroredRepeatWrapping: MirroredRepeatWrapping
	};

	const TEXTURE_FILTER = {
		NearestFilter: NearestFilter,
		NearestMipmapNearestFilter: NearestMipmapNearestFilter,
		NearestMipmapLinearFilter: NearestMipmapLinearFilter,
		LinearFilter: LinearFilter,
		LinearMipmapNearestFilter: LinearMipmapNearestFilter,
		LinearMipmapLinearFilter: LinearMipmapLinearFilter
	};

	function ImageBitmapLoader( manager ) {

		if ( typeof createImageBitmap === 'undefined' ) {

			console.warn( 'THREE.ImageBitmapLoader: createImageBitmap() not supported.' );

		}

		if ( typeof fetch === 'undefined' ) {

			console.warn( 'THREE.ImageBitmapLoader: fetch() not supported.' );

		}

		Loader.call( this, manager );

		this.options = { premultiplyAlpha: 'none' };

	}

	ImageBitmapLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: ImageBitmapLoader,

		isImageBitmapLoader: true,

		setOptions: function setOptions( options ) {

			this.options = options;

			return this;

		},

		load: function ( url, onLoad, onProgress, onError ) {

			if ( url === undefined ) url = '';

			if ( this.path !== undefined ) url = this.path + url;

			url = this.manager.resolveURL( url );

			const scope = this;

			const cached = Cache.get( url );

			if ( cached !== undefined ) {

				scope.manager.itemStart( url );

				setTimeout( function () {

					if ( onLoad ) onLoad( cached );

					scope.manager.itemEnd( url );

				}, 0 );

				return cached;

			}

			const fetchOptions = {};
			fetchOptions.credentials = ( this.crossOrigin === 'anonymous' ) ? 'same-origin' : 'include';

			fetch( url, fetchOptions ).then( function ( res ) {

				return res.blob();

			} ).then( function ( blob ) {

				return createImageBitmap( blob, scope.options );

			} ).then( function ( imageBitmap ) {

				Cache.add( url, imageBitmap );

				if ( onLoad ) onLoad( imageBitmap );

				scope.manager.itemEnd( url );

			} ).catch( function ( e ) {

				if ( onError ) onError( e );

				scope.manager.itemError( url );
				scope.manager.itemEnd( url );

			} );

			scope.manager.itemStart( url );

		}

	} );

	function ShapePath() {

		this.type = 'ShapePath';

		this.color = new Color();

		this.subPaths = [];
		this.currentPath = null;

	}

	Object.assign( ShapePath.prototype, {

		moveTo: function ( x, y ) {

			this.currentPath = new Path();
			this.subPaths.push( this.currentPath );
			this.currentPath.moveTo( x, y );

			return this;

		},

		lineTo: function ( x, y ) {

			this.currentPath.lineTo( x, y );

			return this;

		},

		quadraticCurveTo: function ( aCPx, aCPy, aX, aY ) {

			this.currentPath.quadraticCurveTo( aCPx, aCPy, aX, aY );

			return this;

		},

		bezierCurveTo: function ( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY ) {

			this.currentPath.bezierCurveTo( aCP1x, aCP1y, aCP2x, aCP2y, aX, aY );

			return this;

		},

		splineThru: function ( pts ) {

			this.currentPath.splineThru( pts );

			return this;

		},

		toShapes: function ( isCCW, noHoles ) {

			function toShapesNoHoles( inSubpaths ) {

				const shapes = [];

				for ( let i = 0, l = inSubpaths.length; i < l; i ++ ) {

					const tmpPath = inSubpaths[ i ];

					const tmpShape = new Shape();
					tmpShape.curves = tmpPath.curves;

					shapes.push( tmpShape );

				}

				return shapes;

			}

			function isPointInsidePolygon( inPt, inPolygon ) {

				const polyLen = inPolygon.length;

				// inPt on polygon contour => immediate success    or
				// toggling of inside/outside at every single! intersection point of an edge
				//  with the horizontal line through inPt, left of inPt
				//  not counting lowerY endpoints of edges and whole edges on that line
				let inside = false;
				for ( let p = polyLen - 1, q = 0; q < polyLen; p = q ++ ) {

					let edgeLowPt = inPolygon[ p ];
					let edgeHighPt = inPolygon[ q ];

					let edgeDx = edgeHighPt.x - edgeLowPt.x;
					let edgeDy = edgeHighPt.y - edgeLowPt.y;

					if ( Math.abs( edgeDy ) > Number.EPSILON ) {

						// not parallel
						if ( edgeDy < 0 ) {

							edgeLowPt = inPolygon[ q ]; edgeDx = - edgeDx;
							edgeHighPt = inPolygon[ p ]; edgeDy = - edgeDy;

						}

						if ( ( inPt.y < edgeLowPt.y ) || ( inPt.y > edgeHighPt.y ) ) 		continue;

						if ( inPt.y === edgeLowPt.y ) {

							if ( inPt.x === edgeLowPt.x )		return	true;		// inPt is on contour ?
							// continue;				// no intersection or edgeLowPt => doesn't count !!!

						} else {

							const perpEdge = edgeDy * ( inPt.x - edgeLowPt.x ) - edgeDx * ( inPt.y - edgeLowPt.y );
							if ( perpEdge === 0 )				return	true;		// inPt is on contour ?
							if ( perpEdge < 0 ) 				continue;
							inside = ! inside;		// true intersection left of inPt

						}

					} else {

						// parallel or collinear
						if ( inPt.y !== edgeLowPt.y ) 		continue;			// parallel
						// edge lies on the same horizontal line as inPt
						if ( ( ( edgeHighPt.x <= inPt.x ) && ( inPt.x <= edgeLowPt.x ) ) ||
							 ( ( edgeLowPt.x <= inPt.x ) && ( inPt.x <= edgeHighPt.x ) ) )		return	true;	// inPt: Point on contour !
						// continue;

					}

				}

				return	inside;

			}

			const isClockWise = ShapeUtils.isClockWise;

			const subPaths = this.subPaths;
			if ( subPaths.length === 0 ) return [];

			if ( noHoles === true )	return	toShapesNoHoles( subPaths );


			let solid, tmpPath, tmpShape;
			const shapes = [];

			if ( subPaths.length === 1 ) {

				tmpPath = subPaths[ 0 ];
				tmpShape = new Shape();
				tmpShape.curves = tmpPath.curves;
				shapes.push( tmpShape );
				return shapes;

			}

			let holesFirst = ! isClockWise( subPaths[ 0 ].getPoints() );
			holesFirst = isCCW ? ! holesFirst : holesFirst;

			// console.log("Holes first", holesFirst);

			const betterShapeHoles = [];
			const newShapes = [];
			let newShapeHoles = [];
			let mainIdx = 0;
			let tmpPoints;

			newShapes[ mainIdx ] = undefined;
			newShapeHoles[ mainIdx ] = [];

			for ( let i = 0, l = subPaths.length; i < l; i ++ ) {

				tmpPath = subPaths[ i ];
				tmpPoints = tmpPath.getPoints();
				solid = isClockWise( tmpPoints );
				solid = isCCW ? ! solid : solid;

				if ( solid ) {

					if ( ( ! holesFirst ) && ( newShapes[ mainIdx ] ) )	mainIdx ++;

					newShapes[ mainIdx ] = { s: new Shape(), p: tmpPoints };
					newShapes[ mainIdx ].s.curves = tmpPath.curves;

					if ( holesFirst )	mainIdx ++;
					newShapeHoles[ mainIdx ] = [];

					//console.log('cw', i);

				} else {

					newShapeHoles[ mainIdx ].push( { h: tmpPath, p: tmpPoints[ 0 ] } );

					//console.log('ccw', i);

				}

			}

			// only Holes? -> probably all Shapes with wrong orientation
			if ( ! newShapes[ 0 ] )	return	toShapesNoHoles( subPaths );


			if ( newShapes.length > 1 ) {

				let ambiguous = false;
				const toChange = [];

				for ( let sIdx = 0, sLen = newShapes.length; sIdx < sLen; sIdx ++ ) {

					betterShapeHoles[ sIdx ] = [];

				}

				for ( let sIdx = 0, sLen = newShapes.length; sIdx < sLen; sIdx ++ ) {

					const sho = newShapeHoles[ sIdx ];

					for ( let hIdx = 0; hIdx < sho.length; hIdx ++ ) {

						const ho = sho[ hIdx ];
						let hole_unassigned = true;

						for ( let s2Idx = 0; s2Idx < newShapes.length; s2Idx ++ ) {

							if ( isPointInsidePolygon( ho.p, newShapes[ s2Idx ].p ) ) {

								if ( sIdx !== s2Idx )	toChange.push( { froms: sIdx, tos: s2Idx, hole: hIdx } );
								if ( hole_unassigned ) {

									hole_unassigned = false;
									betterShapeHoles[ s2Idx ].push( ho );

								} else {

									ambiguous = true;

								}

							}

						}

						if ( hole_unassigned ) {

							betterShapeHoles[ sIdx ].push( ho );

						}

					}

				}
				// console.log("ambiguous: ", ambiguous);

				if ( toChange.length > 0 ) {

					// console.log("to change: ", toChange);
					if ( ! ambiguous )	newShapeHoles = betterShapeHoles;

				}

			}

			let tmpHoles;

			for ( let i = 0, il = newShapes.length; i < il; i ++ ) {

				tmpShape = newShapes[ i ].s;
				shapes.push( tmpShape );
				tmpHoles = newShapeHoles[ i ];

				for ( let j = 0, jl = tmpHoles.length; j < jl; j ++ ) {

					tmpShape.holes.push( tmpHoles[ j ].h );

				}

			}

			//console.log("shape", shapes);

			return shapes;

		}

	} );

	function Font( data ) {

		this.type = 'Font';

		this.data = data;

	}

	Object.assign( Font.prototype, {

		isFont: true,

		generateShapes: function ( text, size = 100 ) {

			const shapes = [];
			const paths = createPaths( text, size, this.data );

			for ( let p = 0, pl = paths.length; p < pl; p ++ ) {

				Array.prototype.push.apply( shapes, paths[ p ].toShapes() );

			}

			return shapes;

		}

	} );

	function createPaths( text, size, data ) {

		const chars = Array.from ? Array.from( text ) : String( text ).split( '' ); // workaround for IE11, see #13988
		const scale = size / data.resolution;
		const line_height = ( data.boundingBox.yMax - data.boundingBox.yMin + data.underlineThickness ) * scale;

		const paths = [];

		let offsetX = 0, offsetY = 0;

		for ( let i = 0; i < chars.length; i ++ ) {

			const char = chars[ i ];

			if ( char === '\n' ) {

				offsetX = 0;
				offsetY -= line_height;

			} else {

				const ret = createPath( char, scale, offsetX, offsetY, data );
				offsetX += ret.offsetX;
				paths.push( ret.path );

			}

		}

		return paths;

	}

	function createPath( char, scale, offsetX, offsetY, data ) {

		const glyph = data.glyphs[ char ] || data.glyphs[ '?' ];

		if ( ! glyph ) {

			console.error( 'THREE.Font: character "' + char + '" does not exists in font family ' + data.familyName + '.' );

			return;

		}

		const path = new ShapePath();

		let x, y, cpx, cpy, cpx1, cpy1, cpx2, cpy2;

		if ( glyph.o ) {

			const outline = glyph._cachedOutline || ( glyph._cachedOutline = glyph.o.split( ' ' ) );

			for ( let i = 0, l = outline.length; i < l; ) {

				const action = outline[ i ++ ];

				switch ( action ) {

					case 'm': // moveTo

						x = outline[ i ++ ] * scale + offsetX;
						y = outline[ i ++ ] * scale + offsetY;

						path.moveTo( x, y );

						break;

					case 'l': // lineTo

						x = outline[ i ++ ] * scale + offsetX;
						y = outline[ i ++ ] * scale + offsetY;

						path.lineTo( x, y );

						break;

					case 'q': // quadraticCurveTo

						cpx = outline[ i ++ ] * scale + offsetX;
						cpy = outline[ i ++ ] * scale + offsetY;
						cpx1 = outline[ i ++ ] * scale + offsetX;
						cpy1 = outline[ i ++ ] * scale + offsetY;

						path.quadraticCurveTo( cpx1, cpy1, cpx, cpy );

						break;

					case 'b': // bezierCurveTo

						cpx = outline[ i ++ ] * scale + offsetX;
						cpy = outline[ i ++ ] * scale + offsetY;
						cpx1 = outline[ i ++ ] * scale + offsetX;
						cpy1 = outline[ i ++ ] * scale + offsetY;
						cpx2 = outline[ i ++ ] * scale + offsetX;
						cpy2 = outline[ i ++ ] * scale + offsetY;

						path.bezierCurveTo( cpx1, cpy1, cpx2, cpy2, cpx, cpy );

						break;

				}

			}

		}

		return { offsetX: glyph.ha * scale, path: path };

	}

	function FontLoader( manager ) {

		Loader.call( this, manager );

	}

	FontLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: FontLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			const scope = this;

			const loader = new FileLoader( this.manager );
			loader.setPath( this.path );
			loader.setRequestHeader( this.requestHeader );
			loader.setWithCredentials( scope.withCredentials );
			loader.load( url, function ( text ) {

				let json;

				try {

					json = JSON.parse( text );

				} catch ( e ) {

					console.warn( 'THREE.FontLoader: typeface.js support is being deprecated. Use typeface.json instead.' );
					json = JSON.parse( text.substring( 65, text.length - 2 ) );

				}

				const font = scope.parse( json );

				if ( onLoad ) onLoad( font );

			}, onProgress, onError );

		},

		parse: function ( json ) {

			return new Font( json );

		}

	} );

	let _context;

	const AudioContext = {

		getContext: function () {

			if ( _context === undefined ) {

				_context = new ( window.AudioContext || window.webkitAudioContext )();

			}

			return _context;

		},

		setContext: function ( value ) {

			_context = value;

		}

	};

	function AudioLoader( manager ) {

		Loader.call( this, manager );

	}

	AudioLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

		constructor: AudioLoader,

		load: function ( url, onLoad, onProgress, onError ) {

			const scope = this;

			const loader = new FileLoader( scope.manager );
			loader.setResponseType( 'arraybuffer' );
			loader.setPath( scope.path );
			loader.setRequestHeader( scope.requestHeader );
			loader.setWithCredentials( scope.withCredentials );
			loader.load( url, function ( buffer ) {

				try {

					// Create a copy of the buffer. The `decodeAudioData` method
					// detaches the buffer when complete, preventing reuse.
					const bufferCopy = buffer.slice( 0 );

					const context = AudioContext.getContext();
					context.decodeAudioData( bufferCopy, function ( audioBuffer ) {

						onLoad( audioBuffer );

					} );

				} catch ( e ) {

					if ( onError ) {

						onError( e );

					} else {

						console.error( e );

					}

					scope.manager.itemError( url );

				}

			}, onProgress, onError );

		}

	} );

	function HemisphereLightProbe( skyColor, groundColor, intensity ) {

		LightProbe.call( this, undefined, intensity );

		const color1 = new Color().set( skyColor );
		const color2 = new Color().set( groundColor );

		const sky = new Vector3( color1.r, color1.g, color1.b );
		const ground = new Vector3( color2.r, color2.g, color2.b );

		// without extra factor of PI in the shader, should = 1 / Math.sqrt( Math.PI );
		const c0 = Math.sqrt( Math.PI );
		const c1 = c0 * Math.sqrt( 0.75 );

		this.sh.coefficients[ 0 ].copy( sky ).add( ground ).multiplyScalar( c0 );
		this.sh.coefficients[ 1 ].copy( sky ).sub( ground ).multiplyScalar( c1 );

	}

	HemisphereLightProbe.prototype = Object.assign( Object.create( LightProbe.prototype ), {

		constructor: HemisphereLightProbe,

		isHemisphereLightProbe: true,

		copy: function ( source ) { // modifying colors not currently supported

			LightProbe.prototype.copy.call( this, source );

			return this;

		},

		toJSON: function ( meta ) {

			const data = LightProbe.prototype.toJSON.call( this, meta );

			// data.sh = this.sh.toArray(); // todo

			return data;

		}

	} );

	function AmbientLightProbe( color, intensity ) {

		LightProbe.call( this, undefined, intensity );

		const color1 = new Color().set( color );

		// without extra factor of PI in the shader, would be 2 / Math.sqrt( Math.PI );
		this.sh.coefficients[ 0 ].set( color1.r, color1.g, color1.b ).multiplyScalar( 2 * Math.sqrt( Math.PI ) );

	}

	AmbientLightProbe.prototype = Object.assign( Object.create( LightProbe.prototype ), {

		constructor: AmbientLightProbe,

		isAmbientLightProbe: true,

		copy: function ( source ) { // modifying color not currently supported

			LightProbe.prototype.copy.call( this, source );

			return this;

		},

		toJSON: function ( meta ) {

			const data = LightProbe.prototype.toJSON.call( this, meta );

			// data.sh = this.sh.toArray(); // todo

			return data;

		}

	} );

	const _eyeRight = new Matrix4();
	const _eyeLeft = new Matrix4();

	function StereoCamera() {

		this.type = 'StereoCamera';

		this.aspect = 1;

		this.eyeSep = 0.064;

		this.cameraL = new PerspectiveCamera();
		this.cameraL.layers.enable( 1 );
		this.cameraL.matrixAutoUpdate = false;

		this.cameraR = new PerspectiveCamera();
		this.cameraR.layers.enable( 2 );
		this.cameraR.matrixAutoUpdate = false;

		this._cache = {
			focus: null,
			fov: null,
			aspect: null,
			near: null,
			far: null,
			zoom: null,
			eyeSep: null
		};

	}

	Object.assign( StereoCamera.prototype, {

		update: function ( camera ) {

			const cache = this._cache;

			const needsUpdate = cache.focus !== camera.focus || cache.fov !== camera.fov ||
				cache.aspect !== camera.aspect * this.aspect || cache.near !== camera.near ||
				cache.far !== camera.far || cache.zoom !== camera.zoom || cache.eyeSep !== this.eyeSep;

			if ( needsUpdate ) {

				cache.focus = camera.focus;
				cache.fov = camera.fov;
				cache.aspect = camera.aspect * this.aspect;
				cache.near = camera.near;
				cache.far = camera.far;
				cache.zoom = camera.zoom;
				cache.eyeSep = this.eyeSep;

				// Off-axis stereoscopic effect based on
				// http://paulbourke.net/stereographics/stereorender/

				const projectionMatrix = camera.projectionMatrix.clone();
				const eyeSepHalf = cache.eyeSep / 2;
				const eyeSepOnProjection = eyeSepHalf * cache.near / cache.focus;
				const ymax = ( cache.near * Math.tan( MathUtils.DEG2RAD * cache.fov * 0.5 ) ) / cache.zoom;
				let xmin, xmax;

				// translate xOffset

				_eyeLeft.elements[ 12 ] = - eyeSepHalf;
				_eyeRight.elements[ 12 ] = eyeSepHalf;

				// for left eye

				xmin = - ymax * cache.aspect + eyeSepOnProjection;
				xmax = ymax * cache.aspect + eyeSepOnProjection;

				projectionMatrix.elements[ 0 ] = 2 * cache.near / ( xmax - xmin );
				projectionMatrix.elements[ 8 ] = ( xmax + xmin ) / ( xmax - xmin );

				this.cameraL.projectionMatrix.copy( projectionMatrix );

				// for right eye

				xmin = - ymax * cache.aspect - eyeSepOnProjection;
				xmax = ymax * cache.aspect - eyeSepOnProjection;

				projectionMatrix.elements[ 0 ] = 2 * cache.near / ( xmax - xmin );
				projectionMatrix.elements[ 8 ] = ( xmax + xmin ) / ( xmax - xmin );

				this.cameraR.projectionMatrix.copy( projectionMatrix );

			}

			this.cameraL.matrixWorld.copy( camera.matrixWorld ).multiply( _eyeLeft );
			this.cameraR.matrixWorld.copy( camera.matrixWorld ).multiply( _eyeRight );

		}

	} );

	class Clock {

		constructor( autoStart ) {

			this.autoStart = ( autoStart !== undefined ) ? autoStart : true;

			this.startTime = 0;
			this.oldTime = 0;
			this.elapsedTime = 0;

			this.running = false;

		}

		start() {

			this.startTime = now();

			this.oldTime = this.startTime;
			this.elapsedTime = 0;
			this.running = true;

		}

		stop() {

			this.getElapsedTime();
			this.running = false;
			this.autoStart = false;

		}

		getElapsedTime() {

			this.getDelta();
			return this.elapsedTime;

		}

		getDelta() {

			let diff = 0;

			if ( this.autoStart && ! this.running ) {

				this.start();
				return 0;

			}

			if ( this.running ) {

				const newTime = now();

				diff = ( newTime - this.oldTime ) / 1000;
				this.oldTime = newTime;

				this.elapsedTime += diff;

			}

			return diff;

		}

	}

	function now() {

		return ( typeof performance === 'undefined' ? Date : performance ).now(); // see #10732

	}

	const _position$2 = /*@__PURE__*/ new Vector3();
	const _quaternion$3 = /*@__PURE__*/ new Quaternion();
	const _scale$1 = /*@__PURE__*/ new Vector3();
	const _orientation = /*@__PURE__*/ new Vector3();

	class AudioListener extends Object3D {

		constructor() {

			super();

			this.type = 'AudioListener';

			this.context = AudioContext.getContext();

			this.gain = this.context.createGain();
			this.gain.connect( this.context.destination );

			this.filter = null;

			this.timeDelta = 0;

			// private

			this._clock = new Clock();

		}

		getInput() {

			return this.gain;

		}

		removeFilter() {

			if ( this.filter !== null ) {

				this.gain.disconnect( this.filter );
				this.filter.disconnect( this.context.destination );
				this.gain.connect( this.context.destination );
				this.filter = null;

			}

			return this;

		}

		getFilter() {

			return this.filter;

		}

		setFilter( value ) {

			if ( this.filter !== null ) {

				this.gain.disconnect( this.filter );
				this.filter.disconnect( this.context.destination );

			} else {

				this.gain.disconnect( this.context.destination );

			}

			this.filter = value;
			this.gain.connect( this.filter );
			this.filter.connect( this.context.destination );

			return this;

		}

		getMasterVolume() {

			return this.gain.gain.value;

		}

		setMasterVolume( value ) {

			this.gain.gain.setTargetAtTime( value, this.context.currentTime, 0.01 );

			return this;

		}

		updateMatrixWorld( force ) {

			super.updateMatrixWorld( force );

			const listener = this.context.listener;
			const up = this.up;

			this.timeDelta = this._clock.getDelta();

			this.matrixWorld.decompose( _position$2, _quaternion$3, _scale$1 );

			_orientation.set( 0, 0, - 1 ).applyQuaternion( _quaternion$3 );

			if ( listener.positionX ) {

				// code path for Chrome (see #14393)

				const endTime = this.context.currentTime + this.timeDelta;

				listener.positionX.linearRampToValueAtTime( _position$2.x, endTime );
				listener.positionY.linearRampToValueAtTime( _position$2.y, endTime );
				listener.positionZ.linearRampToValueAtTime( _position$2.z, endTime );
				listener.forwardX.linearRampToValueAtTime( _orientation.x, endTime );
				listener.forwardY.linearRampToValueAtTime( _orientation.y, endTime );
				listener.forwardZ.linearRampToValueAtTime( _orientation.z, endTime );
				listener.upX.linearRampToValueAtTime( up.x, endTime );
				listener.upY.linearRampToValueAtTime( up.y, endTime );
				listener.upZ.linearRampToValueAtTime( up.z, endTime );

			} else {

				listener.setPosition( _position$2.x, _position$2.y, _position$2.z );
				listener.setOrientation( _orientation.x, _orientation.y, _orientation.z, up.x, up.y, up.z );

			}

		}

	}

	class Audio extends Object3D {

		constructor( listener ) {

			super();

			this.type = 'Audio';

			this.listener = listener;
			this.context = listener.context;

			this.gain = this.context.createGain();
			this.gain.connect( listener.getInput() );

			this.autoplay = false;

			this.buffer = null;
			this.detune = 0;
			this.loop = false;
			this.loopStart = 0;
			this.loopEnd = 0;
			this.offset = 0;
			this.duration = undefined;
			this.playbackRate = 1;
			this.isPlaying = false;
			this.hasPlaybackControl = true;
			this.source = null;
			this.sourceType = 'empty';

			this._startedAt = 0;
			this._progress = 0;
			this._connected = false;

			this.filters = [];

		}

		getOutput() {

			return this.gain;

		}

		setNodeSource( audioNode ) {

			this.hasPlaybackControl = false;
			this.sourceType = 'audioNode';
			this.source = audioNode;
			this.connect();

			return this;

		}

		setMediaElementSource( mediaElement ) {

			this.hasPlaybackControl = false;
			this.sourceType = 'mediaNode';
			this.source = this.context.createMediaElementSource( mediaElement );
			this.connect();

			return this;

		}

		setMediaStreamSource( mediaStream ) {

			this.hasPlaybackControl = false;
			this.sourceType = 'mediaStreamNode';
			this.source = this.context.createMediaStreamSource( mediaStream );
			this.connect();

			return this;

		}

		setBuffer( audioBuffer ) {

			this.buffer = audioBuffer;
			this.sourceType = 'buffer';

			if ( this.autoplay ) this.play();

			return this;

		}

		play( delay = 0 ) {

			if ( this.isPlaying === true ) {

				console.warn( 'THREE.Audio: Audio is already playing.' );
				return;

			}

			if ( this.hasPlaybackControl === false ) {

				console.warn( 'THREE.Audio: this Audio has no playback control.' );
				return;

			}

			this._startedAt = this.context.currentTime + delay;

			const source = this.context.createBufferSource();
			source.buffer = this.buffer;
			source.loop = this.loop;
			source.loopStart = this.loopStart;
			source.loopEnd = this.loopEnd;
			source.onended = this.onEnded.bind( this );
			source.start( this._startedAt, this._progress + this.offset, this.duration );

			this.isPlaying = true;

			this.source = source;

			this.setDetune( this.detune );
			this.setPlaybackRate( this.playbackRate );

			return this.connect();

		}

		pause() {

			if ( this.hasPlaybackControl === false ) {

				console.warn( 'THREE.Audio: this Audio has no playback control.' );
				return;

			}

			if ( this.isPlaying === true ) {

				// update current progress

				this._progress += Math.max( this.context.currentTime - this._startedAt, 0 ) * this.playbackRate;

				if ( this.loop === true ) {

					// ensure _progress does not exceed duration with looped audios

					this._progress = this._progress % ( this.duration || this.buffer.duration );

				}

				this.source.stop();
				this.source.onended = null;

				this.isPlaying = false;

			}

			return this;

		}

		stop() {

			if ( this.hasPlaybackControl === false ) {

				console.warn( 'THREE.Audio: this Audio has no playback control.' );
				return;

			}

			this._progress = 0;

			this.source.stop();
			this.source.onended = null;
			this.isPlaying = false;

			return this;

		}

		connect() {

			if ( this.filters.length > 0 ) {

				this.source.connect( this.filters[ 0 ] );

				for ( let i = 1, l = this.filters.length; i < l; i ++ ) {

					this.filters[ i - 1 ].connect( this.filters[ i ] );

				}

				this.filters[ this.filters.length - 1 ].connect( this.getOutput() );

			} else {

				this.source.connect( this.getOutput() );

			}

			this._connected = true;

			return this;

		}

		disconnect() {

			if ( this.filters.length > 0 ) {

				this.source.disconnect( this.filters[ 0 ] );

				for ( let i = 1, l = this.filters.length; i < l; i ++ ) {

					this.filters[ i - 1 ].disconnect( this.filters[ i ] );

				}

				this.filters[ this.filters.length - 1 ].disconnect( this.getOutput() );

			} else {

				this.source.disconnect( this.getOutput() );

			}

			this._connected = false;

			return this;

		}

		getFilters() {

			return this.filters;

		}

		setFilters( value ) {

			if ( ! value ) value = [];

			if ( this._connected === true ) {

				this.disconnect();
				this.filters = value.slice();
				this.connect();

			} else {

				this.filters = value.slice();

			}

			return this;

		}

		setDetune( value ) {

			this.detune = value;

			if ( this.source.detune === undefined ) return; // only set detune when available

			if ( this.isPlaying === true ) {

				this.source.detune.setTargetAtTime( this.detune, this.context.currentTime, 0.01 );

			}

			return this;

		}

		getDetune() {

			return this.detune;

		}

		getFilter() {

			return this.getFilters()[ 0 ];

		}

		setFilter( filter ) {

			return this.setFilters( filter ? [ filter ] : [] );

		}

		setPlaybackRate( value ) {

			if ( this.hasPlaybackControl === false ) {

				console.warn( 'THREE.Audio: this Audio has no playback control.' );
				return;

			}

			this.playbackRate = value;

			if ( this.isPlaying === true ) {

				this.source.playbackRate.setTargetAtTime( this.playbackRate, this.context.currentTime, 0.01 );

			}

			return this;

		}

		getPlaybackRate() {

			return this.playbackRate;

		}

		onEnded() {

			this.isPlaying = false;

		}

		getLoop() {

			if ( this.hasPlaybackControl === false ) {

				console.warn( 'THREE.Audio: this Audio has no playback control.' );
				return false;

			}

			return this.loop;

		}

		setLoop( value ) {

			if ( this.hasPlaybackControl === false ) {

				console.warn( 'THREE.Audio: this Audio has no playback control.' );
				return;

			}

			this.loop = value;

			if ( this.isPlaying === true ) {

				this.source.loop = this.loop;

			}

			return this;

		}

		setLoopStart( value ) {

			this.loopStart = value;

			return this;

		}

		setLoopEnd( value ) {

			this.loopEnd = value;

			return this;

		}

		getVolume() {

			return this.gain.gain.value;

		}

		setVolume( value ) {

			this.gain.gain.setTargetAtTime( value, this.context.currentTime, 0.01 );

			return this;

		}

	}

	const _position$3 = /*@__PURE__*/ new Vector3();
	const _quaternion$4 = /*@__PURE__*/ new Quaternion();
	const _scale$2 = /*@__PURE__*/ new Vector3();
	const _orientation$1 = /*@__PURE__*/ new Vector3();

	class PositionalAudio extends Audio {

		constructor( listener ) {

			super( listener );

			this.panner = this.context.createPanner();
			this.panner.panningModel = 'HRTF';
			this.panner.connect( this.gain );

		}

		getOutput() {

			return this.panner;

		}

		getRefDistance() {

			return this.panner.refDistance;

		}

		setRefDistance( value ) {

			this.panner.refDistance = value;

			return this;

		}

		getRolloffFactor() {

			return this.panner.rolloffFactor;

		}

		setRolloffFactor( value ) {

			this.panner.rolloffFactor = value;

			return this;

		}

		getDistanceModel() {

			return this.panner.distanceModel;

		}

		setDistanceModel( value ) {

			this.panner.distanceModel = value;

			return this;

		}

		getMaxDistance() {

			return this.panner.maxDistance;

		}

		setMaxDistance( value ) {

			this.panner.maxDistance = value;

			return this;

		}

		setDirectionalCone( coneInnerAngle, coneOuterAngle, coneOuterGain ) {

			this.panner.coneInnerAngle = coneInnerAngle;
			this.panner.coneOuterAngle = coneOuterAngle;
			this.panner.coneOuterGain = coneOuterGain;

			return this;

		}

		updateMatrixWorld( force ) {

			super.updateMatrixWorld( force );

			if ( this.hasPlaybackControl === true && this.isPlaying === false ) return;

			this.matrixWorld.decompose( _position$3, _quaternion$4, _scale$2 );

			_orientation$1.set( 0, 0, 1 ).applyQuaternion( _quaternion$4 );

			const panner = this.panner;

			if ( panner.positionX ) {

				// code path for Chrome and Firefox (see #14393)

				const endTime = this.context.currentTime + this.listener.timeDelta;

				panner.positionX.linearRampToValueAtTime( _position$3.x, endTime );
				panner.positionY.linearRampToValueAtTime( _position$3.y, endTime );
				panner.positionZ.linearRampToValueAtTime( _position$3.z, endTime );
				panner.orientationX.linearRampToValueAtTime( _orientation$1.x, endTime );
				panner.orientationY.linearRampToValueAtTime( _orientation$1.y, endTime );
				panner.orientationZ.linearRampToValueAtTime( _orientation$1.z, endTime );

			} else {

				panner.setPosition( _position$3.x, _position$3.y, _position$3.z );
				panner.setOrientation( _orientation$1.x, _orientation$1.y, _orientation$1.z );

			}

		}

	}

	class AudioAnalyser {

		constructor( audio, fftSize = 2048 ) {

			this.analyser = audio.context.createAnalyser();
			this.analyser.fftSize = fftSize;

			this.data = new Uint8Array( this.analyser.frequencyBinCount );

			audio.getOutput().connect( this.analyser );

		}


		getFrequencyData() {

			this.analyser.getByteFrequencyData( this.data );

			return this.data;

		}

		getAverageFrequency() {

			let value = 0;
			const data = this.getFrequencyData();

			for ( let i = 0; i < data.length; i ++ ) {

				value += data[ i ];

			}

			return value / data.length;

		}

	}

	function PropertyMixer( binding, typeName, valueSize ) {

		this.binding = binding;
		this.valueSize = valueSize;

		let mixFunction,
			mixFunctionAdditive,
			setIdentity;

		// buffer layout: [ incoming | accu0 | accu1 | orig | addAccu | (optional work) ]
		//
		// interpolators can use .buffer as their .result
		// the data then goes to 'incoming'
		//
		// 'accu0' and 'accu1' are used frame-interleaved for
		// the cumulative result and are compared to detect
		// changes
		//
		// 'orig' stores the original state of the property
		//
		// 'add' is used for additive cumulative results
		//
		// 'work' is optional and is only present for quaternion types. It is used
		// to store intermediate quaternion multiplication results

		switch ( typeName ) {

			case 'quaternion':
				mixFunction = this._slerp;
				mixFunctionAdditive = this._slerpAdditive;
				setIdentity = this._setAdditiveIdentityQuaternion;

				this.buffer = new Float64Array( valueSize * 6 );
				this._workIndex = 5;
				break;

			case 'string':
			case 'bool':
				mixFunction = this._select;

				// Use the regular mix function and for additive on these types,
				// additive is not relevant for non-numeric types
				mixFunctionAdditive = this._select;

				setIdentity = this._setAdditiveIdentityOther;

				this.buffer = new Array( valueSize * 5 );
				break;

			default:
				mixFunction = this._lerp;
				mixFunctionAdditive = this._lerpAdditive;
				setIdentity = this._setAdditiveIdentityNumeric;

				this.buffer = new Float64Array( valueSize * 5 );

		}

		this._mixBufferRegion = mixFunction;
		this._mixBufferRegionAdditive = mixFunctionAdditive;
		this._setIdentity = setIdentity;
		this._origIndex = 3;
		this._addIndex = 4;

		this.cumulativeWeight = 0;
		this.cumulativeWeightAdditive = 0;

		this.useCount = 0;
		this.referenceCount = 0;

	}

	Object.assign( PropertyMixer.prototype, {

		// accumulate data in the 'incoming' region into 'accu<i>'
		accumulate: function ( accuIndex, weight ) {

			// note: happily accumulating nothing when weight = 0, the caller knows
			// the weight and shouldn't have made the call in the first place

			const buffer = this.buffer,
				stride = this.valueSize,
				offset = accuIndex * stride + stride;

			let currentWeight = this.cumulativeWeight;

			if ( currentWeight === 0 ) {

				// accuN := incoming * weight

				for ( let i = 0; i !== stride; ++ i ) {

					buffer[ offset + i ] = buffer[ i ];

				}

				currentWeight = weight;

			} else {

				// accuN := accuN + incoming * weight

				currentWeight += weight;
				const mix = weight / currentWeight;
				this._mixBufferRegion( buffer, offset, 0, mix, stride );

			}

			this.cumulativeWeight = currentWeight;

		},

		// accumulate data in the 'incoming' region into 'add'
		accumulateAdditive: function ( weight ) {

			const buffer = this.buffer,
				stride = this.valueSize,
				offset = stride * this._addIndex;

			if ( this.cumulativeWeightAdditive === 0 ) {

				// add = identity

				this._setIdentity();

			}

			// add := add + incoming * weight

			this._mixBufferRegionAdditive( buffer, offset, 0, weight, stride );
			this.cumulativeWeightAdditive += weight;

		},

		// apply the state of 'accu<i>' to the binding when accus differ
		apply: function ( accuIndex ) {

			const stride = this.valueSize,
				buffer = this.buffer,
				offset = accuIndex * stride + stride,

				weight = this.cumulativeWeight,
				weightAdditive = this.cumulativeWeightAdditive,

				binding = this.binding;

			this.cumulativeWeight = 0;
			this.cumulativeWeightAdditive = 0;

			if ( weight < 1 ) {

				// accuN := accuN + original * ( 1 - cumulativeWeight )

				const originalValueOffset = stride * this._origIndex;

				this._mixBufferRegion(
					buffer, offset, originalValueOffset, 1 - weight, stride );

			}

			if ( weightAdditive > 0 ) {

				// accuN := accuN + additive accuN

				this._mixBufferRegionAdditive( buffer, offset, this._addIndex * stride, 1, stride );

			}

			for ( let i = stride, e = stride + stride; i !== e; ++ i ) {

				if ( buffer[ i ] !== buffer[ i + stride ] ) {

					// value has changed -> update scene graph

					binding.setValue( buffer, offset );
					break;

				}

			}

		},

		// remember the state of the bound property and copy it to both accus
		saveOriginalState: function () {

			const binding = this.binding;

			const buffer = this.buffer,
				stride = this.valueSize,

				originalValueOffset = stride * this._origIndex;

			binding.getValue( buffer, originalValueOffset );

			// accu[0..1] := orig -- initially detect changes against the original
			for ( let i = stride, e = originalValueOffset; i !== e; ++ i ) {

				buffer[ i ] = buffer[ originalValueOffset + ( i % stride ) ];

			}

			// Add to identity for additive
			this._setIdentity();

			this.cumulativeWeight = 0;
			this.cumulativeWeightAdditive = 0;

		},

		// apply the state previously taken via 'saveOriginalState' to the binding
		restoreOriginalState: function () {

			const originalValueOffset = this.valueSize * 3;
			this.binding.setValue( this.buffer, originalValueOffset );

		},

		_setAdditiveIdentityNumeric: function () {

			const startIndex = this._addIndex * this.valueSize;
			const endIndex = startIndex + this.valueSize;

			for ( let i = startIndex; i < endIndex; i ++ ) {

				this.buffer[ i ] = 0;

			}

		},

		_setAdditiveIdentityQuaternion: function () {

			this._setAdditiveIdentityNumeric();
			this.buffer[ this._addIndex * this.valueSize + 3 ] = 1;

		},

		_setAdditiveIdentityOther: function () {

			const startIndex = this._origIndex * this.valueSize;
			const targetIndex = this._addIndex * this.valueSize;

			for ( let i = 0; i < this.valueSize; i ++ ) {

				this.buffer[ targetIndex + i ] = this.buffer[ startIndex + i ];

			}

		},


		// mix functions

		_select: function ( buffer, dstOffset, srcOffset, t, stride ) {

			if ( t >= 0.5 ) {

				for ( let i = 0; i !== stride; ++ i ) {

					buffer[ dstOffset + i ] = buffer[ srcOffset + i ];

				}

			}

		},

		_slerp: function ( buffer, dstOffset, srcOffset, t ) {

			Quaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, srcOffset, t );

		},

		_slerpAdditive: function ( buffer, dstOffset, srcOffset, t, stride ) {

			const workOffset = this._workIndex * stride;

			// Store result in intermediate buffer offset
			Quaternion.multiplyQuaternionsFlat( buffer, workOffset, buffer, dstOffset, buffer, srcOffset );

			// Slerp to the intermediate result
			Quaternion.slerpFlat( buffer, dstOffset, buffer, dstOffset, buffer, workOffset, t );

		},

		_lerp: function ( buffer, dstOffset, srcOffset, t, stride ) {

			const s = 1 - t;

			for ( let i = 0; i !== stride; ++ i ) {

				const j = dstOffset + i;

				buffer[ j ] = buffer[ j ] * s + buffer[ srcOffset + i ] * t;

			}

		},

		_lerpAdditive: function ( buffer, dstOffset, srcOffset, t, stride ) {

			for ( let i = 0; i !== stride; ++ i ) {

				const j = dstOffset + i;

				buffer[ j ] = buffer[ j ] + buffer[ srcOffset + i ] * t;

			}

		}

	} );

	// Characters [].:/ are reserved for track binding syntax.
	const _RESERVED_CHARS_RE = '\\[\\]\\.:\\/';
	const _reservedRe = new RegExp( '[' + _RESERVED_CHARS_RE + ']', 'g' );

	// Attempts to allow node names from any language. ES5's `\w` regexp matches
	// only latin characters, and the unicode \p{L} is not yet supported. So
	// instead, we exclude reserved characters and match everything else.
	const _wordChar = '[^' + _RESERVED_CHARS_RE + ']';
	const _wordCharOrDot = '[^' + _RESERVED_CHARS_RE.replace( '\\.', '' ) + ']';

	// Parent directories, delimited by '/' or ':'. Currently unused, but must
	// be matched to parse the rest of the track name.
	const _directoryRe = /((?:WC+[\/:])*)/.source.replace( 'WC', _wordChar );

	// Target node. May contain word characters (a-zA-Z0-9_) and '.' or '-'.
	const _nodeRe = /(WCOD+)?/.source.replace( 'WCOD', _wordCharOrDot );

	// Object on target node, and accessor. May not contain reserved
	// characters. Accessor may contain any character except closing bracket.
	const _objectRe = /(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace( 'WC', _wordChar );

	// Property and accessor. May not contain reserved characters. Accessor may
	// contain any non-bracket characters.
	const _propertyRe = /\.(WC+)(?:\[(.+)\])?/.source.replace( 'WC', _wordChar );

	const _trackRe = new RegExp( ''
		+ '^'
		+ _directoryRe
		+ _nodeRe
		+ _objectRe
		+ _propertyRe
		+ '$'
	);

	const _supportedObjectNames = [ 'material', 'materials', 'bones' ];

	function Composite( targetGroup, path, optionalParsedPath ) {

		const parsedPath = optionalParsedPath || PropertyBinding.parseTrackName( path );

		this._targetGroup = targetGroup;
		this._bindings = targetGroup.subscribe_( path, parsedPath );

	}

	Object.assign( Composite.prototype, {

		getValue: function ( array, offset ) {

			this.bind(); // bind all binding

			const firstValidIndex = this._targetGroup.nCachedObjects_,
				binding = this._bindings[ firstValidIndex ];

			// and only call .getValue on the first
			if ( binding !== undefined ) binding.getValue( array, offset );

		},

		setValue: function ( array, offset ) {

			const bindings = this._bindings;

			for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {

				bindings[ i ].setValue( array, offset );

			}

		},

		bind: function () {

			const bindings = this._bindings;

			for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {

				bindings[ i ].bind();

			}

		},

		unbind: function () {

			const bindings = this._bindings;

			for ( let i = this._targetGroup.nCachedObjects_, n = bindings.length; i !== n; ++ i ) {

				bindings[ i ].unbind();

			}

		}

	} );


	function PropertyBinding( rootNode, path, parsedPath ) {

		this.path = path;
		this.parsedPath = parsedPath || PropertyBinding.parseTrackName( path );

		this.node = PropertyBinding.findNode( rootNode, this.parsedPath.nodeName ) || rootNode;

		this.rootNode = rootNode;

	}

	Object.assign( PropertyBinding, {

		Composite: Composite,

		create: function ( root, path, parsedPath ) {

			if ( ! ( root && root.isAnimationObjectGroup ) ) {

				return new PropertyBinding( root, path, parsedPath );

			} else {

				return new PropertyBinding.Composite( root, path, parsedPath );

			}

		},

		/**
		 * Replaces spaces with underscores and removes unsupported characters from
		 * node names, to ensure compatibility with parseTrackName().
		 *
		 * @param {string} name Node name to be sanitized.
		 * @return {string}
		 */
		sanitizeNodeName: function ( name ) {

			return name.replace( /\s/g, '_' ).replace( _reservedRe, '' );

		},

		parseTrackName: function ( trackName ) {

			const matches = _trackRe.exec( trackName );

			if ( ! matches ) {

				throw new Error( 'PropertyBinding: Cannot parse trackName: ' + trackName );

			}

			const results = {
				// directoryName: matches[ 1 ], // (tschw) currently unused
				nodeName: matches[ 2 ],
				objectName: matches[ 3 ],
				objectIndex: matches[ 4 ],
				propertyName: matches[ 5 ], // required
				propertyIndex: matches[ 6 ]
			};

			const lastDot = results.nodeName && results.nodeName.lastIndexOf( '.' );

			if ( lastDot !== undefined && lastDot !== - 1 ) {

				const objectName = results.nodeName.substring( lastDot + 1 );

				// Object names must be checked against an allowlist. Otherwise, there
				// is no way to parse 'foo.bar.baz': 'baz' must be a property, but
				// 'bar' could be the objectName, or part of a nodeName (which can
				// include '.' characters).
				if ( _supportedObjectNames.indexOf( objectName ) !== - 1 ) {

					results.nodeName = results.nodeName.substring( 0, lastDot );
					results.objectName = objectName;

				}

			}

			if ( results.propertyName === null || results.propertyName.length === 0 ) {

				throw new Error( 'PropertyBinding: can not parse propertyName from trackName: ' + trackName );

			}

			return results;

		},

		findNode: function ( root, nodeName ) {

			if ( ! nodeName || nodeName === '' || nodeName === '.' || nodeName === - 1 || nodeName === root.name || nodeName === root.uuid ) {

				return root;

			}

			// search into skeleton bones.
			if ( root.skeleton ) {

				const bone = root.skeleton.getBoneByName( nodeName );

				if ( bone !== undefined ) {

					return bone;

				}

			}

			// search into node subtree.
			if ( root.children ) {

				const searchNodeSubtree = function ( children ) {

					for ( let i = 0; i < children.length; i ++ ) {

						const childNode = children[ i ];

						if ( childNode.name === nodeName || childNode.uuid === nodeName ) {

							return childNode;

						}

						const result = searchNodeSubtree( childNode.children );

						if ( result ) return result;

					}

					return null;

				};

				const subTreeNode = searchNodeSubtree( root.children );

				if ( subTreeNode ) {

					return subTreeNode;

				}

			}

			return null;

		}

	} );

	Object.assign( PropertyBinding.prototype, { // prototype, continued

		// these are used to "bind" a nonexistent property
		_getValue_unavailable: function () {},
		_setValue_unavailable: function () {},

		BindingType: {
			Direct: 0,
			EntireArray: 1,
			ArrayElement: 2,
			HasFromToArray: 3
		},

		Versioning: {
			None: 0,
			NeedsUpdate: 1,
			MatrixWorldNeedsUpdate: 2
		},

		GetterByBindingType: [

			function getValue_direct( buffer, offset ) {

				buffer[ offset ] = this.node[ this.propertyName ];

			},

			function getValue_array( buffer, offset ) {

				const source = this.resolvedProperty;

				for ( let i = 0, n = source.length; i !== n; ++ i ) {

					buffer[ offset ++ ] = source[ i ];

				}

			},

			function getValue_arrayElement( buffer, offset ) {

				buffer[ offset ] = this.resolvedProperty[ this.propertyIndex ];

			},

			function getValue_toArray( buffer, offset ) {

				this.resolvedProperty.toArray( buffer, offset );

			}

		],

		SetterByBindingTypeAndVersioning: [

			[
				// Direct

				function setValue_direct( buffer, offset ) {

					this.targetObject[ this.propertyName ] = buffer[ offset ];

				},

				function setValue_direct_setNeedsUpdate( buffer, offset ) {

					this.targetObject[ this.propertyName ] = buffer[ offset ];
					this.targetObject.needsUpdate = true;

				},

				function setValue_direct_setMatrixWorldNeedsUpdate( buffer, offset ) {

					this.targetObject[ this.propertyName ] = buffer[ offset ];
					this.targetObject.matrixWorldNeedsUpdate = true;

				}

			], [

				// EntireArray

				function setValue_array( buffer, offset ) {

					const dest = this.resolvedProperty;

					for ( let i = 0, n = dest.length; i !== n; ++ i ) {

						dest[ i ] = buffer[ offset ++ ];

					}

				},

				function setValue_array_setNeedsUpdate( buffer, offset ) {

					const dest = this.resolvedProperty;

					for ( let i = 0, n = dest.length; i !== n; ++ i ) {

						dest[ i ] = buffer[ offset ++ ];

					}

					this.targetObject.needsUpdate = true;

				},

				function setValue_array_setMatrixWorldNeedsUpdate( buffer, offset ) {

					const dest = this.resolvedProperty;

					for ( let i = 0, n = dest.length; i !== n; ++ i ) {

						dest[ i ] = buffer[ offset ++ ];

					}

					this.targetObject.matrixWorldNeedsUpdate = true;

				}

			], [

				// ArrayElement

				function setValue_arrayElement( buffer, offset ) {

					this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];

				},

				function setValue_arrayElement_setNeedsUpdate( buffer, offset ) {

					this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
					this.targetObject.needsUpdate = true;

				},

				function setValue_arrayElement_setMatrixWorldNeedsUpdate( buffer, offset ) {

					this.resolvedProperty[ this.propertyIndex ] = buffer[ offset ];
					this.targetObject.matrixWorldNeedsUpdate = true;

				}

			], [

				// HasToFromArray

				function setValue_fromArray( buffer, offset ) {

					this.resolvedProperty.fromArray( buffer, offset );

				},

				function setValue_fromArray_setNeedsUpdate( buffer, offset ) {

					this.resolvedProperty.fromArray( buffer, offset );
					this.targetObject.needsUpdate = true;

				},

				function setValue_fromArray_setMatrixWorldNeedsUpdate( buffer, offset ) {

					this.resolvedProperty.fromArray( buffer, offset );
					this.targetObject.matrixWorldNeedsUpdate = true;

				}

			]

		],

		getValue: function getValue_unbound( targetArray, offset ) {

			this.bind();
			this.getValue( targetArray, offset );

			// Note: This class uses a State pattern on a per-method basis:
			// 'bind' sets 'this.getValue' / 'setValue' and shadows the
			// prototype version of these methods with one that represents
			// the bound state. When the property is not found, the methods
			// become no-ops.

		},

		setValue: function getValue_unbound( sourceArray, offset ) {

			this.bind();
			this.setValue( sourceArray, offset );

		},

		// create getter / setter pair for a property in the scene graph
		bind: function () {

			let targetObject = this.node;
			const parsedPath = this.parsedPath;

			const objectName = parsedPath.objectName;
			const propertyName = parsedPath.propertyName;
			let propertyIndex = parsedPath.propertyIndex;

			if ( ! targetObject ) {

				targetObject = PropertyBinding.findNode( this.rootNode, parsedPath.nodeName ) || this.rootNode;

				this.node = targetObject;

			}

			// set fail state so we can just 'return' on error
			this.getValue = this._getValue_unavailable;
			this.setValue = this._setValue_unavailable;

			// ensure there is a value node
			if ( ! targetObject ) {

				console.error( 'THREE.PropertyBinding: Trying to update node for track: ' + this.path + ' but it wasn\'t found.' );
				return;

			}

			if ( objectName ) {

				let objectIndex = parsedPath.objectIndex;

				// special cases were we need to reach deeper into the hierarchy to get the face materials....
				switch ( objectName ) {

					case 'materials':

						if ( ! targetObject.material ) {

							console.error( 'THREE.PropertyBinding: Can not bind to material as node does not have a material.', this );
							return;

						}

						if ( ! targetObject.material.materials ) {

							console.error( 'THREE.PropertyBinding: Can not bind to material.materials as node.material does not have a materials array.', this );
							return;

						}

						targetObject = targetObject.material.materials;

						break;

					case 'bones':

						if ( ! targetObject.skeleton ) {

							console.error( 'THREE.PropertyBinding: Can not bind to bones as node does not have a skeleton.', this );
							return;

						}

						// potential future optimization: skip this if propertyIndex is already an integer
						// and convert the integer string to a true integer.

						targetObject = targetObject.skeleton.bones;

						// support resolving morphTarget names into indices.
						for ( let i = 0; i < targetObject.length; i ++ ) {

							if ( targetObject[ i ].name === objectIndex ) {

								objectIndex = i;
								break;

							}

						}

						break;

					default:

						if ( targetObject[ objectName ] === undefined ) {

							console.error( 'THREE.PropertyBinding: Can not bind to objectName of node undefined.', this );
							return;

						}

						targetObject = targetObject[ objectName ];

				}


				if ( objectIndex !== undefined ) {

					if ( targetObject[ objectIndex ] === undefined ) {

						console.error( 'THREE.PropertyBinding: Trying to bind to objectIndex of objectName, but is undefined.', this, targetObject );
						return;

					}

					targetObject = targetObject[ objectIndex ];

				}

			}

			// resolve property
			const nodeProperty = targetObject[ propertyName ];

			if ( nodeProperty === undefined ) {

				const nodeName = parsedPath.nodeName;

				console.error( 'THREE.PropertyBinding: Trying to update property for track: ' + nodeName +
					'.' + propertyName + ' but it wasn\'t found.', targetObject );
				return;

			}

			// determine versioning scheme
			let versioning = this.Versioning.None;

			this.targetObject = targetObject;

			if ( targetObject.needsUpdate !== undefined ) { // material

				versioning = this.Versioning.NeedsUpdate;

			} else if ( targetObject.matrixWorldNeedsUpdate !== undefined ) { // node transform

				versioning = this.Versioning.MatrixWorldNeedsUpdate;

			}

			// determine how the property gets bound
			let bindingType = this.BindingType.Direct;

			if ( propertyIndex !== undefined ) {

				// access a sub element of the property array (only primitives are supported right now)

				if ( propertyName === 'morphTargetInfluences' ) {

					// potential optimization, skip this if propertyIndex is already an integer, and convert the integer string to a true integer.

					// support resolving morphTarget names into indices.
					if ( ! targetObject.geometry ) {

						console.error( 'THREE.PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.', this );
						return;

					}

					if ( targetObject.geometry.isBufferGeometry ) {

						if ( ! targetObject.geometry.morphAttributes ) {

							console.error( 'THREE.PropertyBinding: Can not bind to morphTargetInfluences because node does not have a geometry.morphAttributes.', this );
							return;

						}

						if ( targetObject.morphTargetDictionary[ propertyIndex ] !== undefined ) {

							propertyIndex = targetObject.morphTargetDictionary[ propertyIndex ];

						}


					} else {

						console.error( 'THREE.PropertyBinding: Can not bind to morphTargetInfluences on THREE.Geometry. Use THREE.BufferGeometry instead.', this );
						return;

					}

				}

				bindingType = this.BindingType.ArrayElement;

				this.resolvedProperty = nodeProperty;
				this.propertyIndex = propertyIndex;

			} else if ( nodeProperty.fromArray !== undefined && nodeProperty.toArray !== undefined ) {

				// must use copy for Object3D.Euler/Quaternion

				bindingType = this.BindingType.HasFromToArray;

				this.resolvedProperty = nodeProperty;

			} else if ( Array.isArray( nodeProperty ) ) {

				bindingType = this.BindingType.EntireArray;

				this.resolvedProperty = nodeProperty;

			} else {

				this.propertyName = propertyName;

			}

			// select getter / setter
			this.getValue = this.GetterByBindingType[ bindingType ];
			this.setValue = this.SetterByBindingTypeAndVersioning[ bindingType ][ versioning ];

		},

		unbind: function () {

			this.node = null;

			// back to the prototype version of getValue / setValue
			// note: avoiding to mutate the shape of 'this' via 'delete'
			this.getValue = this._getValue_unbound;
			this.setValue = this._setValue_unbound;

		}

	} );

	// DECLARE ALIAS AFTER assign prototype
	Object.assign( PropertyBinding.prototype, {

		// initial state of these methods that calls 'bind'
		_getValue_unbound: PropertyBinding.prototype.getValue,
		_setValue_unbound: PropertyBinding.prototype.setValue,

	} );

	/**
	 *
	 * A group of objects that receives a shared animation state.
	 *
	 * Usage:
	 *
	 *  - Add objects you would otherwise pass as 'root' to the
	 *    constructor or the .clipAction method of AnimationMixer.
	 *
	 *  - Instead pass this object as 'root'.
	 *
	 *  - You can also add and remove objects later when the mixer
	 *    is running.
	 *
	 * Note:
	 *
	 *    Objects of this class appear as one object to the mixer,
	 *    so cache control of the individual objects must be done
	 *    on the group.
	 *
	 * Limitation:
	 *
	 *  - The animated properties must be compatible among the
	 *    all objects in the group.
	 *
	 *  - A single property can either be controlled through a
	 *    target group or directly, but not both.
	 */

	function AnimationObjectGroup() {

		this.uuid = MathUtils.generateUUID();

		// cached objects followed by the active ones
		this._objects = Array.prototype.slice.call( arguments );

		this.nCachedObjects_ = 0; // threshold
		// note: read by PropertyBinding.Composite

		const indices = {};
		this._indicesByUUID = indices; // for bookkeeping

		for ( let i = 0, n = arguments.length; i !== n; ++ i ) {

			indices[ arguments[ i ].uuid ] = i;

		}

		this._paths = []; // inside: string
		this._parsedPaths = []; // inside: { we don't care, here }
		this._bindings = []; // inside: Array< PropertyBinding >
		this._bindingsIndicesByPath = {}; // inside: indices in these arrays

		const scope = this;

		this.stats = {

			objects: {
				get total() {

					return scope._objects.length;

				},
				get inUse() {

					return this.total - scope.nCachedObjects_;

				}
			},
			get bindingsPerObject() {

				return scope._bindings.length;

			}

		};

	}

	Object.assign( AnimationObjectGroup.prototype, {

		isAnimationObjectGroup: true,

		add: function () {

			const objects = this._objects,
				indicesByUUID = this._indicesByUUID,
				paths = this._paths,
				parsedPaths = this._parsedPaths,
				bindings = this._bindings,
				nBindings = bindings.length;

			let knownObject = undefined,
				nObjects = objects.length,
				nCachedObjects = this.nCachedObjects_;

			for ( let i = 0, n = arguments.length; i !== n; ++ i ) {

				const object = arguments[ i ],
					uuid = object.uuid;
				let index = indicesByUUID[ uuid ];

				if ( index === undefined ) {

					// unknown object -> add it to the ACTIVE region

					index = nObjects ++;
					indicesByUUID[ uuid ] = index;
					objects.push( object );

					// accounting is done, now do the same for all bindings

					for ( let j = 0, m = nBindings; j !== m; ++ j ) {

						bindings[ j ].push( new PropertyBinding( object, paths[ j ], parsedPaths[ j ] ) );

					}

				} else if ( index < nCachedObjects ) {

					knownObject = objects[ index ];

					// move existing object to the ACTIVE region

					const firstActiveIndex = -- nCachedObjects,
						lastCachedObject = objects[ firstActiveIndex ];

					indicesByUUID[ lastCachedObject.uuid ] = index;
					objects[ index ] = lastCachedObject;

					indicesByUUID[ uuid ] = firstActiveIndex;
					objects[ firstActiveIndex ] = object;

					// accounting is done, now do the same for all bindings

					for ( let j = 0, m = nBindings; j !== m; ++ j ) {

						const bindingsForPath = bindings[ j ],
							lastCached = bindingsForPath[ firstActiveIndex ];

						let binding = bindingsForPath[ index ];

						bindingsForPath[ index ] = lastCached;

						if ( binding === undefined ) {

							// since we do not bother to create new bindings
							// for objects that are cached, the binding may
							// or may not exist

							binding = new PropertyBinding( object, paths[ j ], parsedPaths[ j ] );

						}

						bindingsForPath[ firstActiveIndex ] = binding;

					}

				} else if ( objects[ index ] !== knownObject ) {

					console.error( 'THREE.AnimationObjectGroup: Different objects with the same UUID ' +
						'detected. Clean the caches or recreate your infrastructure when reloading scenes.' );

				} // else the object is already where we want it to be

			} // for arguments

			this.nCachedObjects_ = nCachedObjects;

		},

		remove: function () {

			const objects = this._objects,
				indicesByUUID = this._indicesByUUID,
				bindings = this._bindings,
				nBindings = bindings.length;

			let nCachedObjects = this.nCachedObjects_;

			for ( let i = 0, n = arguments.length; i !== n; ++ i ) {

				const object = arguments[ i ],
					uuid = object.uuid,
					index = indicesByUUID[ uuid ];

				if ( index !== undefined && index >= nCachedObjects ) {

					// move existing object into the CACHED region

					const lastCachedIndex = nCachedObjects ++,
						firstActiveObject = objects[ lastCachedIndex ];

					indicesByUUID[ firstActiveObject.uuid ] = index;
					objects[ index ] = firstActiveObject;

					indicesByUUID[ uuid ] = lastCachedIndex;
					objects[ lastCachedIndex ] = object;

					// accounting is done, now do the same for all bindings

					for ( let j = 0, m = nBindings; j !== m; ++ j ) {

						const bindingsForPath = bindings[ j ],
							firstActive = bindingsForPath[ lastCachedIndex ],
							binding = bindingsForPath[ index ];

						bindingsForPath[ index ] = firstActive;
						bindingsForPath[ lastCachedIndex ] = binding;

					}

				}

			} // for arguments

			this.nCachedObjects_ = nCachedObjects;

		},

		// remove & forget
		uncache: function () {

			const objects = this._objects,
				indicesByUUID = this._indicesByUUID,
				bindings = this._bindings,
				nBindings = bindings.length;

			let nCachedObjects = this.nCachedObjects_,
				nObjects = objects.length;

			for ( let i = 0, n = arguments.length; i !== n; ++ i ) {

				const object = arguments[ i ],
					uuid = object.uuid,
					index = indicesByUUID[ uuid ];

				if ( index !== undefined ) {

					delete indicesByUUID[ uuid ];

					if ( index < nCachedObjects ) {

						// object is cached, shrink the CACHED region

						const firstActiveIndex = -- nCachedObjects,
							lastCachedObject = objects[ firstActiveIndex ],
							lastIndex = -- nObjects,
							lastObject = objects[ lastIndex ];

						// last cached object takes this object's place
						indicesByUUID[ lastCachedObject.uuid ] = index;
						objects[ index ] = lastCachedObject;

						// last object goes to the activated slot and pop
						indicesByUUID[ lastObject.uuid ] = firstActiveIndex;
						objects[ firstActiveIndex ] = lastObject;
						objects.pop();

						// accounting is done, now do the same for all bindings

						for ( let j = 0, m = nBindings; j !== m; ++ j ) {

							const bindingsForPath = bindings[ j ],
								lastCached = bindingsForPath[ firstActiveIndex ],
								last = bindingsForPath[ lastIndex ];

							bindingsForPath[ index ] = lastCached;
							bindingsForPath[ firstActiveIndex ] = last;
							bindingsForPath.pop();

						}

					} else {

						// object is active, just swap with the last and pop

						const lastIndex = -- nObjects,
							lastObject = objects[ lastIndex ];

						if ( lastIndex > 0 ) {

							indicesByUUID[ lastObject.uuid ] = index;

						}

						objects[ index ] = lastObject;
						objects.pop();

						// accounting is done, now do the same for all bindings

						for ( let j = 0, m = nBindings; j !== m; ++ j ) {

							const bindingsForPath = bindings[ j ];

							bindingsForPath[ index ] = bindingsForPath[ lastIndex ];
							bindingsForPath.pop();

						}

					} // cached or active

				} // if object is known

			} // for arguments

			this.nCachedObjects_ = nCachedObjects;

		},

		// Internal interface used by befriended PropertyBinding.Composite:

		subscribe_: function ( path, parsedPath ) {

			// returns an array of bindings for the given path that is changed
			// according to the contained objects in the group

			const indicesByPath = this._bindingsIndicesByPath;
			let index = indicesByPath[ path ];
			const bindings = this._bindings;

			if ( index !== undefined ) return bindings[ index ];

			const paths = this._paths,
				parsedPaths = this._parsedPaths,
				objects = this._objects,
				nObjects = objects.length,
				nCachedObjects = this.nCachedObjects_,
				bindingsForPath = new Array( nObjects );

			index = bindings.length;

			indicesByPath[ path ] = index;

			paths.push( path );
			parsedPaths.push( parsedPath );
			bindings.push( bindingsForPath );

			for ( let i = nCachedObjects, n = objects.length; i !== n; ++ i ) {

				const object = objects[ i ];
				bindingsForPath[ i ] = new PropertyBinding( object, path, parsedPath );

			}

			return bindingsForPath;

		},

		unsubscribe_: function ( path ) {

			// tells the group to forget about a property path and no longer
			// update the array previously obtained with 'subscribe_'

			const indicesByPath = this._bindingsIndicesByPath,
				index = indicesByPath[ path ];

			if ( index !== undefined ) {

				const paths = this._paths,
					parsedPaths = this._parsedPaths,
					bindings = this._bindings,
					lastBindingsIndex = bindings.length - 1,
					lastBindings = bindings[ lastBindingsIndex ],
					lastBindingsPath = path[ lastBindingsIndex ];

				indicesByPath[ lastBindingsPath ] = index;

				bindings[ index ] = lastBindings;
				bindings.pop();

				parsedPaths[ index ] = parsedPaths[ lastBindingsIndex ];
				parsedPaths.pop();

				paths[ index ] = paths[ lastBindingsIndex ];
				paths.pop();

			}

		}

	} );

	class AnimationAction {

		constructor( mixer, clip, localRoot = null, blendMode = clip.blendMode ) {

			this._mixer = mixer;
			this._clip = clip;
			this._localRoot = localRoot;
			this.blendMode = blendMode;

			const tracks = clip.tracks,
				nTracks = tracks.length,
				interpolants = new Array( nTracks );

			const interpolantSettings = {
				endingStart: ZeroCurvatureEnding,
				endingEnd: ZeroCurvatureEnding
			};

			for ( let i = 0; i !== nTracks; ++ i ) {

				const interpolant = tracks[ i ].createInterpolant( null );
				interpolants[ i ] = interpolant;
				interpolant.settings = interpolantSettings;

			}

			this._interpolantSettings = interpolantSettings;

			this._interpolants = interpolants; // bound by the mixer

			// inside: PropertyMixer (managed by the mixer)
			this._propertyBindings = new Array( nTracks );

			this._cacheIndex = null; // for the memory manager
			this._byClipCacheIndex = null; // for the memory manager

			this._timeScaleInterpolant = null;
			this._weightInterpolant = null;

			this.loop = LoopRepeat;
			this._loopCount = - 1;

			// global mixer time when the action is to be started
			// it's set back to 'null' upon start of the action
			this._startTime = null;

			// scaled local time of the action
			// gets clamped or wrapped to 0..clip.duration according to loop
			this.time = 0;

			this.timeScale = 1;
			this._effectiveTimeScale = 1;

			this.weight = 1;
			this._effectiveWeight = 1;

			this.repetitions = Infinity; // no. of repetitions when looping

			this.paused = false; // true -> zero effective time scale
			this.enabled = true; // false -> zero effective weight

			this.clampWhenFinished = false;// keep feeding the last frame?

			this.zeroSlopeAtStart = true;// for smooth interpolation w/o separate
			this.zeroSlopeAtEnd = true;// clips for start, loop and end

		}

		// State & Scheduling

		play() {

			this._mixer._activateAction( this );

			return this;

		}

		stop() {

			this._mixer._deactivateAction( this );

			return this.reset();

		}

		reset() {

			this.paused = false;
			this.enabled = true;

			this.time = 0; // restart clip
			this._loopCount = - 1;// forget previous loops
			this._startTime = null;// forget scheduling

			return this.stopFading().stopWarping();

		}

		isRunning() {

			return this.enabled && ! this.paused && this.timeScale !== 0 &&
				this._startTime === null && this._mixer._isActiveAction( this );

		}

		// return true when play has been called
		isScheduled() {

			return this._mixer._isActiveAction( this );

		}

		startAt( time ) {

			this._startTime = time;

			return this;

		}

		setLoop( mode, repetitions ) {

			this.loop = mode;
			this.repetitions = repetitions;

			return this;

		}

		// Weight

		// set the weight stopping any scheduled fading
		// although .enabled = false yields an effective weight of zero, this
		// method does *not* change .enabled, because it would be confusing
		setEffectiveWeight( weight ) {

			this.weight = weight;

			// note: same logic as when updated at runtime
			this._effectiveWeight = this.enabled ? weight : 0;

			return this.stopFading();

		}

		// return the weight considering fading and .enabled
		getEffectiveWeight() {

			return this._effectiveWeight;

		}

		fadeIn( duration ) {

			return this._scheduleFading( duration, 0, 1 );

		}

		fadeOut( duration ) {

			return this._scheduleFading( duration, 1, 0 );

		}

		crossFadeFrom( fadeOutAction, duration, warp ) {

			fadeOutAction.fadeOut( duration );
			this.fadeIn( duration );

			if ( warp ) {

				const fadeInDuration = this._clip.duration,
					fadeOutDuration = fadeOutAction._clip.duration,

					startEndRatio = fadeOutDuration / fadeInDuration,
					endStartRatio = fadeInDuration / fadeOutDuration;

				fadeOutAction.warp( 1.0, startEndRatio, duration );
				this.warp( endStartRatio, 1.0, duration );

			}

			return this;

		}

		crossFadeTo( fadeInAction, duration, warp ) {

			return fadeInAction.crossFadeFrom( this, duration, warp );

		}

		stopFading() {

			const weightInterpolant = this._weightInterpolant;

			if ( weightInterpolant !== null ) {

				this._weightInterpolant = null;
				this._mixer._takeBackControlInterpolant( weightInterpolant );

			}

			return this;

		}

		// Time Scale Control

		// set the time scale stopping any scheduled warping
		// although .paused = true yields an effective time scale of zero, this
		// method does *not* change .paused, because it would be confusing
		setEffectiveTimeScale( timeScale ) {

			this.timeScale = timeScale;
			this._effectiveTimeScale = this.paused ? 0 : timeScale;

			return this.stopWarping();

		}

		// return the time scale considering warping and .paused
		getEffectiveTimeScale() {

			return this._effectiveTimeScale;

		}

		setDuration( duration ) {

			this.timeScale = this._clip.duration / duration;

			return this.stopWarping();

		}

		syncWith( action ) {

			this.time = action.time;
			this.timeScale = action.timeScale;

			return this.stopWarping();

		}

		halt( duration ) {

			return this.warp( this._effectiveTimeScale, 0, duration );

		}

		warp( startTimeScale, endTimeScale, duration ) {

			const mixer = this._mixer,
				now = mixer.time,
				timeScale = this.timeScale;

			let interpolant = this._timeScaleInterpolant;

			if ( interpolant === null ) {

				interpolant = mixer._lendControlInterpolant();
				this._timeScaleInterpolant = interpolant;

			}

			const times = interpolant.parameterPositions,
				values = interpolant.sampleValues;

			times[ 0 ] = now;
			times[ 1 ] = now + duration;

			values[ 0 ] = startTimeScale / timeScale;
			values[ 1 ] = endTimeScale / timeScale;

			return this;

		}

		stopWarping() {

			const timeScaleInterpolant = this._timeScaleInterpolant;

			if ( timeScaleInterpolant !== null ) {

				this._timeScaleInterpolant = null;
				this._mixer._takeBackControlInterpolant( timeScaleInterpolant );

			}

			return this;

		}

		// Object Accessors

		getMixer() {

			return this._mixer;

		}

		getClip() {

			return this._clip;

		}

		getRoot() {

			return this._localRoot || this._mixer._root;

		}

		// Interna

		_update( time, deltaTime, timeDirection, accuIndex ) {

			// called by the mixer

			if ( ! this.enabled ) {

				// call ._updateWeight() to update ._effectiveWeight

				this._updateWeight( time );
				return;

			}

			const startTime = this._startTime;

			if ( startTime !== null ) {

				// check for scheduled start of action

				const timeRunning = ( time - startTime ) * timeDirection;
				if ( timeRunning < 0 || timeDirection === 0 ) {

					return; // yet to come / don't decide when delta = 0

				}

				// start

				this._startTime = null; // unschedule
				deltaTime = timeDirection * timeRunning;

			}

			// apply time scale and advance time

			deltaTime *= this._updateTimeScale( time );
			const clipTime = this._updateTime( deltaTime );

			// note: _updateTime may disable the action resulting in
			// an effective weight of 0

			const weight = this._updateWeight( time );

			if ( weight > 0 ) {

				const interpolants = this._interpolants;
				const propertyMixers = this._propertyBindings;

				switch ( this.blendMode ) {

					case AdditiveAnimationBlendMode:

						for ( let j = 0, m = interpolants.length; j !== m; ++ j ) {

							interpolants[ j ].evaluate( clipTime );
							propertyMixers[ j ].accumulateAdditive( weight );

						}

						break;

					case NormalAnimationBlendMode:
					default:

						for ( let j = 0, m = interpolants.length; j !== m; ++ j ) {

							interpolants[ j ].evaluate( clipTime );
							propertyMixers[ j ].accumulate( accuIndex, weight );

						}

				}

			}

		}

		_updateWeight( time ) {

			let weight = 0;

			if ( this.enabled ) {

				weight = this.weight;
				const interpolant = this._weightInterpolant;

				if ( interpolant !== null ) {

					const interpolantValue = interpolant.evaluate( time )[ 0 ];

					weight *= interpolantValue;

					if ( time > interpolant.parameterPositions[ 1 ] ) {

						this.stopFading();

						if ( interpolantValue === 0 ) {

							// faded out, disable
							this.enabled = false;

						}

					}

				}

			}

			this._effectiveWeight = weight;
			return weight;

		}

		_updateTimeScale( time ) {

			let timeScale = 0;

			if ( ! this.paused ) {

				timeScale = this.timeScale;

				const interpolant = this._timeScaleInterpolant;

				if ( interpolant !== null ) {

					const interpolantValue = interpolant.evaluate( time )[ 0 ];

					timeScale *= interpolantValue;

					if ( time > interpolant.parameterPositions[ 1 ] ) {

						this.stopWarping();

						if ( timeScale === 0 ) {

							// motion has halted, pause
							this.paused = true;

						} else {

							// warp done - apply final time scale
							this.timeScale = timeScale;

						}

					}

				}

			}

			this._effectiveTimeScale = timeScale;
			return timeScale;

		}

		_updateTime( deltaTime ) {

			const duration = this._clip.duration;
			const loop = this.loop;

			let time = this.time + deltaTime;
			let loopCount = this._loopCount;

			const pingPong = ( loop === LoopPingPong );

			if ( deltaTime === 0 ) {

				if ( loopCount === - 1 ) return time;

				return ( pingPong && ( loopCount & 1 ) === 1 ) ? duration - time : time;

			}

			if ( loop === LoopOnce ) {

				if ( loopCount === - 1 ) {

					// just started

					this._loopCount = 0;
					this._setEndings( true, true, false );

				}

				handle_stop: {

					if ( time >= duration ) {

						time = duration;

					} else if ( time < 0 ) {

						time = 0;

					} else {

						this.time = time;

						break handle_stop;

					}

					if ( this.clampWhenFinished ) this.paused = true;
					else this.enabled = false;

					this.time = time;

					this._mixer.dispatchEvent( {
						type: 'finished', action: this,
						direction: deltaTime < 0 ? - 1 : 1
					} );

				}

			} else { // repetitive Repeat or PingPong

				if ( loopCount === - 1 ) {

					// just started

					if ( deltaTime >= 0 ) {

						loopCount = 0;

						this._setEndings( true, this.repetitions === 0, pingPong );

					} else {

						// when looping in reverse direction, the initial
						// transition through zero counts as a repetition,
						// so leave loopCount at -1

						this._setEndings( this.repetitions === 0, true, pingPong );

					}

				}

				if ( time >= duration || time < 0 ) {

					// wrap around

					const loopDelta = Math.floor( time / duration ); // signed
					time -= duration * loopDelta;

					loopCount += Math.abs( loopDelta );

					const pending = this.repetitions - loopCount;

					if ( pending <= 0 ) {

						// have to stop (switch state, clamp time, fire event)

						if ( this.clampWhenFinished ) this.paused = true;
						else this.enabled = false;

						time = deltaTime > 0 ? duration : 0;

						this.time = time;

						this._mixer.dispatchEvent( {
							type: 'finished', action: this,
							direction: deltaTime > 0 ? 1 : - 1
						} );

					} else {

						// keep running

						if ( pending === 1 ) {

							// entering the last round

							const atStart = deltaTime < 0;
							this._setEndings( atStart, ! atStart, pingPong );

						} else {

							this._setEndings( false, false, pingPong );

						}

						this._loopCount = loopCount;

						this.time = time;

						this._mixer.dispatchEvent( {
							type: 'loop', action: this, loopDelta: loopDelta
						} );

					}

				} else {

					this.time = time;

				}

				if ( pingPong && ( loopCount & 1 ) === 1 ) {

					// invert time for the "pong round"

					return duration - time;

				}

			}

			return time;

		}

		_setEndings( atStart, atEnd, pingPong ) {

			const settings = this._interpolantSettings;

			if ( pingPong ) {

				settings.endingStart = ZeroSlopeEnding;
				settings.endingEnd = ZeroSlopeEnding;

			} else {

				// assuming for LoopOnce atStart == atEnd == true

				if ( atStart ) {

					settings.endingStart = this.zeroSlopeAtStart ? ZeroSlopeEnding : ZeroCurvatureEnding;

				} else {

					settings.endingStart = WrapAroundEnding;

				}

				if ( atEnd ) {

					settings.endingEnd = this.zeroSlopeAtEnd ? ZeroSlopeEnding : ZeroCurvatureEnding;

				} else {

					settings.endingEnd 	 = WrapAroundEnding;

				}

			}

		}

		_scheduleFading( duration, weightNow, weightThen ) {

			const mixer = this._mixer, now = mixer.time;
			let interpolant = this._weightInterpolant;

			if ( interpolant === null ) {

				interpolant = mixer._lendControlInterpolant();
				this._weightInterpolant = interpolant;

			}

			const times = interpolant.parameterPositions,
				values = interpolant.sampleValues;

			times[ 0 ] = now;
			values[ 0 ] = weightNow;
			times[ 1 ] = now + duration;
			values[ 1 ] = weightThen;

			return this;

		}

	}

	function AnimationMixer( root ) {

		this._root = root;
		this._initMemoryManager();
		this._accuIndex = 0;

		this.time = 0;

		this.timeScale = 1.0;

	}

	AnimationMixer.prototype = Object.assign( Object.create( EventDispatcher$1.prototype ), {

		constructor: AnimationMixer,

		_bindAction: function ( action, prototypeAction ) {

			const root = action._localRoot || this._root,
				tracks = action._clip.tracks,
				nTracks = tracks.length,
				bindings = action._propertyBindings,
				interpolants = action._interpolants,
				rootUuid = root.uuid,
				bindingsByRoot = this._bindingsByRootAndName;

			let bindingsByName = bindingsByRoot[ rootUuid ];

			if ( bindingsByName === undefined ) {

				bindingsByName = {};
				bindingsByRoot[ rootUuid ] = bindingsByName;

			}

			for ( let i = 0; i !== nTracks; ++ i ) {

				const track = tracks[ i ],
					trackName = track.name;

				let binding = bindingsByName[ trackName ];

				if ( binding !== undefined ) {

					bindings[ i ] = binding;

				} else {

					binding = bindings[ i ];

					if ( binding !== undefined ) {

						// existing binding, make sure the cache knows

						if ( binding._cacheIndex === null ) {

							++ binding.referenceCount;
							this._addInactiveBinding( binding, rootUuid, trackName );

						}

						continue;

					}

					const path = prototypeAction && prototypeAction.
						_propertyBindings[ i ].binding.parsedPath;

					binding = new PropertyMixer(
						PropertyBinding.create( root, trackName, path ),
						track.ValueTypeName, track.getValueSize() );

					++ binding.referenceCount;
					this._addInactiveBinding( binding, rootUuid, trackName );

					bindings[ i ] = binding;

				}

				interpolants[ i ].resultBuffer = binding.buffer;

			}

		},

		_activateAction: function ( action ) {

			if ( ! this._isActiveAction( action ) ) {

				if ( action._cacheIndex === null ) {

					// this action has been forgotten by the cache, but the user
					// appears to be still using it -> rebind

					const rootUuid = ( action._localRoot || this._root ).uuid,
						clipUuid = action._clip.uuid,
						actionsForClip = this._actionsByClip[ clipUuid ];

					this._bindAction( action,
						actionsForClip && actionsForClip.knownActions[ 0 ] );

					this._addInactiveAction( action, clipUuid, rootUuid );

				}

				const bindings = action._propertyBindings;

				// increment reference counts / sort out state
				for ( let i = 0, n = bindings.length; i !== n; ++ i ) {

					const binding = bindings[ i ];

					if ( binding.useCount ++ === 0 ) {

						this._lendBinding( binding );
						binding.saveOriginalState();

					}

				}

				this._lendAction( action );

			}

		},

		_deactivateAction: function ( action ) {

			if ( this._isActiveAction( action ) ) {

				const bindings = action._propertyBindings;

				// decrement reference counts / sort out state
				for ( let i = 0, n = bindings.length; i !== n; ++ i ) {

					const binding = bindings[ i ];

					if ( -- binding.useCount === 0 ) {

						binding.restoreOriginalState();
						this._takeBackBinding( binding );

					}

				}

				this._takeBackAction( action );

			}

		},

		// Memory manager

		_initMemoryManager: function () {

			this._actions = []; // 'nActiveActions' followed by inactive ones
			this._nActiveActions = 0;

			this._actionsByClip = {};
			// inside:
			// {
			// 	knownActions: Array< AnimationAction > - used as prototypes
			// 	actionByRoot: AnimationAction - lookup
			// }


			this._bindings = []; // 'nActiveBindings' followed by inactive ones
			this._nActiveBindings = 0;

			this._bindingsByRootAndName = {}; // inside: Map< name, PropertyMixer >


			this._controlInterpolants = []; // same game as above
			this._nActiveControlInterpolants = 0;

			const scope = this;

			this.stats = {

				actions: {
					get total() {

						return scope._actions.length;

					},
					get inUse() {

						return scope._nActiveActions;

					}
				},
				bindings: {
					get total() {

						return scope._bindings.length;

					},
					get inUse() {

						return scope._nActiveBindings;

					}
				},
				controlInterpolants: {
					get total() {

						return scope._controlInterpolants.length;

					},
					get inUse() {

						return scope._nActiveControlInterpolants;

					}
				}

			};

		},

		// Memory management for AnimationAction objects

		_isActiveAction: function ( action ) {

			const index = action._cacheIndex;
			return index !== null && index < this._nActiveActions;

		},

		_addInactiveAction: function ( action, clipUuid, rootUuid ) {

			const actions = this._actions,
				actionsByClip = this._actionsByClip;

			let actionsForClip = actionsByClip[ clipUuid ];

			if ( actionsForClip === undefined ) {

				actionsForClip = {

					knownActions: [ action ],
					actionByRoot: {}

				};

				action._byClipCacheIndex = 0;

				actionsByClip[ clipUuid ] = actionsForClip;

			} else {

				const knownActions = actionsForClip.knownActions;

				action._byClipCacheIndex = knownActions.length;
				knownActions.push( action );

			}

			action._cacheIndex = actions.length;
			actions.push( action );

			actionsForClip.actionByRoot[ rootUuid ] = action;

		},

		_removeInactiveAction: function ( action ) {

			const actions = this._actions,
				lastInactiveAction = actions[ actions.length - 1 ],
				cacheIndex = action._cacheIndex;

			lastInactiveAction._cacheIndex = cacheIndex;
			actions[ cacheIndex ] = lastInactiveAction;
			actions.pop();

			action._cacheIndex = null;


			const clipUuid = action._clip.uuid,
				actionsByClip = this._actionsByClip,
				actionsForClip = actionsByClip[ clipUuid ],
				knownActionsForClip = actionsForClip.knownActions,

				lastKnownAction =
					knownActionsForClip[ knownActionsForClip.length - 1 ],

				byClipCacheIndex = action._byClipCacheIndex;

			lastKnownAction._byClipCacheIndex = byClipCacheIndex;
			knownActionsForClip[ byClipCacheIndex ] = lastKnownAction;
			knownActionsForClip.pop();

			action._byClipCacheIndex = null;


			const actionByRoot = actionsForClip.actionByRoot,
				rootUuid = ( action._localRoot || this._root ).uuid;

			delete actionByRoot[ rootUuid ];

			if ( knownActionsForClip.length === 0 ) {

				delete actionsByClip[ clipUuid ];

			}

			this._removeInactiveBindingsForAction( action );

		},

		_removeInactiveBindingsForAction: function ( action ) {

			const bindings = action._propertyBindings;

			for ( let i = 0, n = bindings.length; i !== n; ++ i ) {

				const binding = bindings[ i ];

				if ( -- binding.referenceCount === 0 ) {

					this._removeInactiveBinding( binding );

				}

			}

		},

		_lendAction: function ( action ) {

			// [ active actions |  inactive actions  ]
			// [  active actions >| inactive actions ]
			//                 s        a
			//                  <-swap->
			//                 a        s

			const actions = this._actions,
				prevIndex = action._cacheIndex,

				lastActiveIndex = this._nActiveActions ++,

				firstInactiveAction = actions[ lastActiveIndex ];

			action._cacheIndex = lastActiveIndex;
			actions[ lastActiveIndex ] = action;

			firstInactiveAction._cacheIndex = prevIndex;
			actions[ prevIndex ] = firstInactiveAction;

		},

		_takeBackAction: function ( action ) {

			// [  active actions  | inactive actions ]
			// [ active actions |< inactive actions  ]
			//        a        s
			//         <-swap->
			//        s        a

			const actions = this._actions,
				prevIndex = action._cacheIndex,

				firstInactiveIndex = -- this._nActiveActions,

				lastActiveAction = actions[ firstInactiveIndex ];

			action._cacheIndex = firstInactiveIndex;
			actions[ firstInactiveIndex ] = action;

			lastActiveAction._cacheIndex = prevIndex;
			actions[ prevIndex ] = lastActiveAction;

		},

		// Memory management for PropertyMixer objects

		_addInactiveBinding: function ( binding, rootUuid, trackName ) {

			const bindingsByRoot = this._bindingsByRootAndName,
				bindings = this._bindings;

			let bindingByName = bindingsByRoot[ rootUuid ];

			if ( bindingByName === undefined ) {

				bindingByName = {};
				bindingsByRoot[ rootUuid ] = bindingByName;

			}

			bindingByName[ trackName ] = binding;

			binding._cacheIndex = bindings.length;
			bindings.push( binding );

		},

		_removeInactiveBinding: function ( binding ) {

			const bindings = this._bindings,
				propBinding = binding.binding,
				rootUuid = propBinding.rootNode.uuid,
				trackName = propBinding.path,
				bindingsByRoot = this._bindingsByRootAndName,
				bindingByName = bindingsByRoot[ rootUuid ],

				lastInactiveBinding = bindings[ bindings.length - 1 ],
				cacheIndex = binding._cacheIndex;

			lastInactiveBinding._cacheIndex = cacheIndex;
			bindings[ cacheIndex ] = lastInactiveBinding;
			bindings.pop();

			delete bindingByName[ trackName ];

			if ( Object.keys( bindingByName ).length === 0 ) {

				delete bindingsByRoot[ rootUuid ];

			}

		},

		_lendBinding: function ( binding ) {

			const bindings = this._bindings,
				prevIndex = binding._cacheIndex,

				lastActiveIndex = this._nActiveBindings ++,

				firstInactiveBinding = bindings[ lastActiveIndex ];

			binding._cacheIndex = lastActiveIndex;
			bindings[ lastActiveIndex ] = binding;

			firstInactiveBinding._cacheIndex = prevIndex;
			bindings[ prevIndex ] = firstInactiveBinding;

		},

		_takeBackBinding: function ( binding ) {

			const bindings = this._bindings,
				prevIndex = binding._cacheIndex,

				firstInactiveIndex = -- this._nActiveBindings,

				lastActiveBinding = bindings[ firstInactiveIndex ];

			binding._cacheIndex = firstInactiveIndex;
			bindings[ firstInactiveIndex ] = binding;

			lastActiveBinding._cacheIndex = prevIndex;
			bindings[ prevIndex ] = lastActiveBinding;

		},


		// Memory management of Interpolants for weight and time scale

		_lendControlInterpolant: function () {

			const interpolants = this._controlInterpolants,
				lastActiveIndex = this._nActiveControlInterpolants ++;

			let interpolant = interpolants[ lastActiveIndex ];

			if ( interpolant === undefined ) {

				interpolant = new LinearInterpolant(
					new Float32Array( 2 ), new Float32Array( 2 ),
					1, this._controlInterpolantsResultBuffer );

				interpolant.__cacheIndex = lastActiveIndex;
				interpolants[ lastActiveIndex ] = interpolant;

			}

			return interpolant;

		},

		_takeBackControlInterpolant: function ( interpolant ) {

			const interpolants = this._controlInterpolants,
				prevIndex = interpolant.__cacheIndex,

				firstInactiveIndex = -- this._nActiveControlInterpolants,

				lastActiveInterpolant = interpolants[ firstInactiveIndex ];

			interpolant.__cacheIndex = firstInactiveIndex;
			interpolants[ firstInactiveIndex ] = interpolant;

			lastActiveInterpolant.__cacheIndex = prevIndex;
			interpolants[ prevIndex ] = lastActiveInterpolant;

		},

		_controlInterpolantsResultBuffer: new Float32Array( 1 ),

		// return an action for a clip optionally using a custom root target
		// object (this method allocates a lot of dynamic memory in case a
		// previously unknown clip/root combination is specified)
		clipAction: function ( clip, optionalRoot, blendMode ) {

			const root = optionalRoot || this._root,
				rootUuid = root.uuid;

			let clipObject = typeof clip === 'string' ? AnimationClip.findByName( root, clip ) : clip;

			const clipUuid = clipObject !== null ? clipObject.uuid : clip;

			const actionsForClip = this._actionsByClip[ clipUuid ];
			let prototypeAction = null;

			if ( blendMode === undefined ) {

				if ( clipObject !== null ) {

					blendMode = clipObject.blendMode;

				} else {

					blendMode = NormalAnimationBlendMode;

				}

			}

			if ( actionsForClip !== undefined ) {

				const existingAction = actionsForClip.actionByRoot[ rootUuid ];

				if ( existingAction !== undefined && existingAction.blendMode === blendMode ) {

					return existingAction;

				}

				// we know the clip, so we don't have to parse all
				// the bindings again but can just copy
				prototypeAction = actionsForClip.knownActions[ 0 ];

				// also, take the clip from the prototype action
				if ( clipObject === null )
					clipObject = prototypeAction._clip;

			}

			// clip must be known when specified via string
			if ( clipObject === null ) return null;

			// allocate all resources required to run it
			const newAction = new AnimationAction( this, clipObject, optionalRoot, blendMode );

			this._bindAction( newAction, prototypeAction );

			// and make the action known to the memory manager
			this._addInactiveAction( newAction, clipUuid, rootUuid );

			return newAction;

		},

		// get an existing action
		existingAction: function ( clip, optionalRoot ) {

			const root = optionalRoot || this._root,
				rootUuid = root.uuid,

				clipObject = typeof clip === 'string' ?
					AnimationClip.findByName( root, clip ) : clip,

				clipUuid = clipObject ? clipObject.uuid : clip,

				actionsForClip = this._actionsByClip[ clipUuid ];

			if ( actionsForClip !== undefined ) {

				return actionsForClip.actionByRoot[ rootUuid ] || null;

			}

			return null;

		},

		// deactivates all previously scheduled actions
		stopAllAction: function () {

			const actions = this._actions,
				nActions = this._nActiveActions;

			for ( let i = nActions - 1; i >= 0; -- i ) {

				actions[ i ].stop();

			}

			return this;

		},

		// advance the time and update apply the animation
		update: function ( deltaTime ) {

			deltaTime *= this.timeScale;

			const actions = this._actions,
				nActions = this._nActiveActions,

				time = this.time += deltaTime,
				timeDirection = Math.sign( deltaTime ),

				accuIndex = this._accuIndex ^= 1;

			// run active actions

			for ( let i = 0; i !== nActions; ++ i ) {

				const action = actions[ i ];

				action._update( time, deltaTime, timeDirection, accuIndex );

			}

			// update scene graph

			const bindings = this._bindings,
				nBindings = this._nActiveBindings;

			for ( let i = 0; i !== nBindings; ++ i ) {

				bindings[ i ].apply( accuIndex );

			}

			return this;

		},

		// Allows you to seek to a specific time in an animation.
		setTime: function ( timeInSeconds ) {

			this.time = 0; // Zero out time attribute for AnimationMixer object;
			for ( let i = 0; i < this._actions.length; i ++ ) {

				this._actions[ i ].time = 0; // Zero out time attribute for all associated AnimationAction objects.

			}

			return this.update( timeInSeconds ); // Update used to set exact time. Returns "this" AnimationMixer object.

		},

		// return this mixer's root target object
		getRoot: function () {

			return this._root;

		},

		// free all resources specific to a particular clip
		uncacheClip: function ( clip ) {

			const actions = this._actions,
				clipUuid = clip.uuid,
				actionsByClip = this._actionsByClip,
				actionsForClip = actionsByClip[ clipUuid ];

			if ( actionsForClip !== undefined ) {

				// note: just calling _removeInactiveAction would mess up the
				// iteration state and also require updating the state we can
				// just throw away

				const actionsToRemove = actionsForClip.knownActions;

				for ( let i = 0, n = actionsToRemove.length; i !== n; ++ i ) {

					const action = actionsToRemove[ i ];

					this._deactivateAction( action );

					const cacheIndex = action._cacheIndex,
						lastInactiveAction = actions[ actions.length - 1 ];

					action._cacheIndex = null;
					action._byClipCacheIndex = null;

					lastInactiveAction._cacheIndex = cacheIndex;
					actions[ cacheIndex ] = lastInactiveAction;
					actions.pop();

					this._removeInactiveBindingsForAction( action );

				}

				delete actionsByClip[ clipUuid ];

			}

		},

		// free all resources specific to a particular root target object
		uncacheRoot: function ( root ) {

			const rootUuid = root.uuid,
				actionsByClip = this._actionsByClip;

			for ( const clipUuid in actionsByClip ) {

				const actionByRoot = actionsByClip[ clipUuid ].actionByRoot,
					action = actionByRoot[ rootUuid ];

				if ( action !== undefined ) {

					this._deactivateAction( action );
					this._removeInactiveAction( action );

				}

			}

			const bindingsByRoot = this._bindingsByRootAndName,
				bindingByName = bindingsByRoot[ rootUuid ];

			if ( bindingByName !== undefined ) {

				for ( const trackName in bindingByName ) {

					const binding = bindingByName[ trackName ];
					binding.restoreOriginalState();
					this._removeInactiveBinding( binding );

				}

			}

		},

		// remove a targeted clip from the cache
		uncacheAction: function ( clip, optionalRoot ) {

			const action = this.existingAction( clip, optionalRoot );

			if ( action !== null ) {

				this._deactivateAction( action );
				this._removeInactiveAction( action );

			}

		}

	} );

	class Uniform {

		constructor( value ) {

			if ( typeof value === 'string' ) {

				console.warn( 'THREE.Uniform: Type parameter is no longer needed.' );
				value = arguments[ 1 ];

			}

			this.value = value;

		}

		clone() {

			return new Uniform( this.value.clone === undefined ? this.value : this.value.clone() );

		}

	}

	function InstancedInterleavedBuffer( array, stride, meshPerAttribute ) {

		InterleavedBuffer.call( this, array, stride );

		this.meshPerAttribute = meshPerAttribute || 1;

	}

	InstancedInterleavedBuffer.prototype = Object.assign( Object.create( InterleavedBuffer.prototype ), {

		constructor: InstancedInterleavedBuffer,

		isInstancedInterleavedBuffer: true,

		copy: function ( source ) {

			InterleavedBuffer.prototype.copy.call( this, source );

			this.meshPerAttribute = source.meshPerAttribute;

			return this;

		},

		clone: function ( data ) {

			const ib = InterleavedBuffer.prototype.clone.call( this, data );

			ib.meshPerAttribute = this.meshPerAttribute;

			return ib;

		},

		toJSON: function ( data ) {

			const json = InterleavedBuffer.prototype.toJSON.call( this, data );

			json.isInstancedInterleavedBuffer = true;
			json.meshPerAttribute = this.meshPerAttribute;

			return json;

		}

	} );

	function GLBufferAttribute( buffer, type, itemSize, elementSize, count ) {

		this.buffer = buffer;
		this.type = type;
		this.itemSize = itemSize;
		this.elementSize = elementSize;
		this.count = count;

		this.version = 0;

	}

	Object.defineProperty( GLBufferAttribute.prototype, 'needsUpdate', {

		set: function ( value ) {

			if ( value === true ) this.version ++;

		}

	} );

	Object.assign( GLBufferAttribute.prototype, {

		isGLBufferAttribute: true,

		setBuffer: function ( buffer ) {

			this.buffer = buffer;

			return this;

		},

		setType: function ( type, elementSize ) {

			this.type = type;
			this.elementSize = elementSize;

			return this;

		},

		setItemSize: function ( itemSize ) {

			this.itemSize = itemSize;

			return this;

		},

		setCount: function ( count ) {

			this.count = count;

			return this;

		},

	} );

	function Raycaster( origin, direction, near, far ) {

		this.ray = new Ray( origin, direction );
		// direction is assumed to be normalized (for accurate distance calculations)

		this.near = near || 0;
		this.far = far || Infinity;
		this.camera = null;
		this.layers = new Layers();

		this.params = {
			Mesh: {},
			Line: { threshold: 1 },
			LOD: {},
			Points: { threshold: 1 },
			Sprite: {}
		};

		Object.defineProperties( this.params, {
			PointCloud: {
				get: function () {

					console.warn( 'THREE.Raycaster: params.PointCloud has been renamed to params.Points.' );
					return this.Points;

				}
			}
		} );

	}

	function ascSort( a, b ) {

		return a.distance - b.distance;

	}

	function intersectObject( object, raycaster, intersects, recursive ) {

		if ( object.layers.test( raycaster.layers ) ) {

			object.raycast( raycaster, intersects );

		}

		if ( recursive === true ) {

			const children = object.children;

			for ( let i = 0, l = children.length; i < l; i ++ ) {

				intersectObject( children[ i ], raycaster, intersects, true );

			}

		}

	}

	Object.assign( Raycaster.prototype, {

		set: function ( origin, direction ) {

			// direction is assumed to be normalized (for accurate distance calculations)

			this.ray.set( origin, direction );

		},

		setFromCamera: function ( coords, camera ) {

			if ( camera && camera.isPerspectiveCamera ) {

				this.ray.origin.setFromMatrixPosition( camera.matrixWorld );
				this.ray.direction.set( coords.x, coords.y, 0.5 ).unproject( camera ).sub( this.ray.origin ).normalize();
				this.camera = camera;

			} else if ( camera && camera.isOrthographicCamera ) {

				this.ray.origin.set( coords.x, coords.y, ( camera.near + camera.far ) / ( camera.near - camera.far ) ).unproject( camera ); // set origin in plane of camera
				this.ray.direction.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );
				this.camera = camera;

			} else {

				console.error( 'THREE.Raycaster: Unsupported camera type: ' + camera.type );

			}

		},

		intersectObject: function ( object, recursive, optionalTarget ) {

			const intersects = optionalTarget || [];

			intersectObject( object, this, intersects, recursive );

			intersects.sort( ascSort );

			return intersects;

		},

		intersectObjects: function ( objects, recursive, optionalTarget ) {

			const intersects = optionalTarget || [];

			if ( Array.isArray( objects ) === false ) {

				console.warn( 'THREE.Raycaster.intersectObjects: objects is not an Array.' );
				return intersects;

			}

			for ( let i = 0, l = objects.length; i < l; i ++ ) {

				intersectObject( objects[ i ], this, intersects, recursive );

			}

			intersects.sort( ascSort );

			return intersects;

		}

	} );

	/**
	 * Ref: https://en.wikipedia.org/wiki/Spherical_coordinate_system
	 *
	 * The polar angle (phi) is measured from the positive y-axis. The positive y-axis is up.
	 * The azimuthal angle (theta) is measured from the positive z-axis.
	 */

	class Spherical {

		constructor( radius = 1, phi = 0, theta = 0 ) {

			this.radius = radius;
			this.phi = phi; // polar angle
			this.theta = theta; // azimuthal angle

			return this;

		}

		set( radius, phi, theta ) {

			this.radius = radius;
			this.phi = phi;
			this.theta = theta;

			return this;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( other ) {

			this.radius = other.radius;
			this.phi = other.phi;
			this.theta = other.theta;

			return this;

		}

		// restrict phi to be betwee EPS and PI-EPS
		makeSafe() {

			const EPS = 0.000001;
			this.phi = Math.max( EPS, Math.min( Math.PI - EPS, this.phi ) );

			return this;

		}

		setFromVector3( v ) {

			return this.setFromCartesianCoords( v.x, v.y, v.z );

		}

		setFromCartesianCoords( x, y, z ) {

			this.radius = Math.sqrt( x * x + y * y + z * z );

			if ( this.radius === 0 ) {

				this.theta = 0;
				this.phi = 0;

			} else {

				this.theta = Math.atan2( x, z );
				this.phi = Math.acos( MathUtils.clamp( y / this.radius, - 1, 1 ) );

			}

			return this;

		}

	}

	/**
	 * Ref: https://en.wikipedia.org/wiki/Cylindrical_coordinate_system
	 */

	class Cylindrical {

		constructor( radius, theta, y ) {

			this.radius = ( radius !== undefined ) ? radius : 1.0; // distance from the origin to a point in the x-z plane
			this.theta = ( theta !== undefined ) ? theta : 0; // counterclockwise angle in the x-z plane measured in radians from the positive z-axis
			this.y = ( y !== undefined ) ? y : 0; // height above the x-z plane

			return this;

		}

		set( radius, theta, y ) {

			this.radius = radius;
			this.theta = theta;
			this.y = y;

			return this;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( other ) {

			this.radius = other.radius;
			this.theta = other.theta;
			this.y = other.y;

			return this;

		}

		setFromVector3( v ) {

			return this.setFromCartesianCoords( v.x, v.y, v.z );

		}

		setFromCartesianCoords( x, y, z ) {

			this.radius = Math.sqrt( x * x + z * z );
			this.theta = Math.atan2( x, z );
			this.y = y;

			return this;

		}

	}

	const _vector$8 = /*@__PURE__*/ new Vector2();

	class Box2 {

		constructor( min, max ) {

			Object.defineProperty( this, 'isBox2', { value: true } );

			this.min = ( min !== undefined ) ? min : new Vector2( + Infinity, + Infinity );
			this.max = ( max !== undefined ) ? max : new Vector2( - Infinity, - Infinity );

		}

		set( min, max ) {

			this.min.copy( min );
			this.max.copy( max );

			return this;

		}

		setFromPoints( points ) {

			this.makeEmpty();

			for ( let i = 0, il = points.length; i < il; i ++ ) {

				this.expandByPoint( points[ i ] );

			}

			return this;

		}

		setFromCenterAndSize( center, size ) {

			const halfSize = _vector$8.copy( size ).multiplyScalar( 0.5 );
			this.min.copy( center ).sub( halfSize );
			this.max.copy( center ).add( halfSize );

			return this;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( box ) {

			this.min.copy( box.min );
			this.max.copy( box.max );

			return this;

		}

		makeEmpty() {

			this.min.x = this.min.y = + Infinity;
			this.max.x = this.max.y = - Infinity;

			return this;

		}

		isEmpty() {

			// this is a more robust check for empty than ( volume <= 0 ) because volume can get positive with two negative axes

			return ( this.max.x < this.min.x ) || ( this.max.y < this.min.y );

		}

		getCenter( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Box2: .getCenter() target is now required' );
				target = new Vector2();

			}

			return this.isEmpty() ? target.set( 0, 0 ) : target.addVectors( this.min, this.max ).multiplyScalar( 0.5 );

		}

		getSize( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Box2: .getSize() target is now required' );
				target = new Vector2();

			}

			return this.isEmpty() ? target.set( 0, 0 ) : target.subVectors( this.max, this.min );

		}

		expandByPoint( point ) {

			this.min.min( point );
			this.max.max( point );

			return this;

		}

		expandByVector( vector ) {

			this.min.sub( vector );
			this.max.add( vector );

			return this;

		}

		expandByScalar( scalar ) {

			this.min.addScalar( - scalar );
			this.max.addScalar( scalar );

			return this;

		}

		containsPoint( point ) {

			return point.x < this.min.x || point.x > this.max.x ||
				point.y < this.min.y || point.y > this.max.y ? false : true;

		}

		containsBox( box ) {

			return this.min.x <= box.min.x && box.max.x <= this.max.x &&
				this.min.y <= box.min.y && box.max.y <= this.max.y;

		}

		getParameter( point, target ) {

			// This can potentially have a divide by zero if the box
			// has a size dimension of 0.

			if ( target === undefined ) {

				console.warn( 'THREE.Box2: .getParameter() target is now required' );
				target = new Vector2();

			}

			return target.set(
				( point.x - this.min.x ) / ( this.max.x - this.min.x ),
				( point.y - this.min.y ) / ( this.max.y - this.min.y )
			);

		}

		intersectsBox( box ) {

			// using 4 splitting planes to rule out intersections

			return box.max.x < this.min.x || box.min.x > this.max.x ||
				box.max.y < this.min.y || box.min.y > this.max.y ? false : true;

		}

		clampPoint( point, target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Box2: .clampPoint() target is now required' );
				target = new Vector2();

			}

			return target.copy( point ).clamp( this.min, this.max );

		}

		distanceToPoint( point ) {

			const clampedPoint = _vector$8.copy( point ).clamp( this.min, this.max );
			return clampedPoint.sub( point ).length();

		}

		intersect( box ) {

			this.min.max( box.min );
			this.max.min( box.max );

			return this;

		}

		union( box ) {

			this.min.min( box.min );
			this.max.max( box.max );

			return this;

		}

		translate( offset ) {

			this.min.add( offset );
			this.max.add( offset );

			return this;

		}

		equals( box ) {

			return box.min.equals( this.min ) && box.max.equals( this.max );

		}

	}

	const _startP = /*@__PURE__*/ new Vector3();
	const _startEnd = /*@__PURE__*/ new Vector3();

	class Line3 {

		constructor( start, end ) {

			this.start = ( start !== undefined ) ? start : new Vector3();
			this.end = ( end !== undefined ) ? end : new Vector3();

		}

		set( start, end ) {

			this.start.copy( start );
			this.end.copy( end );

			return this;

		}

		clone() {

			return new this.constructor().copy( this );

		}

		copy( line ) {

			this.start.copy( line.start );
			this.end.copy( line.end );

			return this;

		}

		getCenter( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Line3: .getCenter() target is now required' );
				target = new Vector3();

			}

			return target.addVectors( this.start, this.end ).multiplyScalar( 0.5 );

		}

		delta( target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Line3: .delta() target is now required' );
				target = new Vector3();

			}

			return target.subVectors( this.end, this.start );

		}

		distanceSq() {

			return this.start.distanceToSquared( this.end );

		}

		distance() {

			return this.start.distanceTo( this.end );

		}

		at( t, target ) {

			if ( target === undefined ) {

				console.warn( 'THREE.Line3: .at() target is now required' );
				target = new Vector3();

			}

			return this.delta( target ).multiplyScalar( t ).add( this.start );

		}

		closestPointToPointParameter( point, clampToLine ) {

			_startP.subVectors( point, this.start );
			_startEnd.subVectors( this.end, this.start );

			const startEnd2 = _startEnd.dot( _startEnd );
			const startEnd_startP = _startEnd.dot( _startP );

			let t = startEnd_startP / startEnd2;

			if ( clampToLine ) {

				t = MathUtils.clamp( t, 0, 1 );

			}

			return t;

		}

		closestPointToPoint( point, clampToLine, target ) {

			const t = this.closestPointToPointParameter( point, clampToLine );

			if ( target === undefined ) {

				console.warn( 'THREE.Line3: .closestPointToPoint() target is now required' );
				target = new Vector3();

			}

			return this.delta( target ).multiplyScalar( t ).add( this.start );

		}

		applyMatrix4( matrix ) {

			this.start.applyMatrix4( matrix );
			this.end.applyMatrix4( matrix );

			return this;

		}

		equals( line ) {

			return line.start.equals( this.start ) && line.end.equals( this.end );

		}

	}

	function ImmediateRenderObject( material ) {

		Object3D.call( this );

		this.material = material;
		this.render = function ( /* renderCallback */ ) {};

		this.hasPositions = false;
		this.hasNormals = false;
		this.hasColors = false;
		this.hasUvs = false;

		this.positionArray = null;
		this.normalArray = null;
		this.colorArray = null;
		this.uvArray = null;

		this.count = 0;

	}

	ImmediateRenderObject.prototype = Object.create( Object3D.prototype );
	ImmediateRenderObject.prototype.constructor = ImmediateRenderObject;

	ImmediateRenderObject.prototype.isImmediateRenderObject = true;

	const _vector$9 = /*@__PURE__*/ new Vector3();

	class SpotLightHelper extends Object3D {

		constructor( light, color ) {

			super();
			this.light = light;
			this.light.updateMatrixWorld();

			this.matrix = light.matrixWorld;
			this.matrixAutoUpdate = false;

			this.color = color;

			const geometry = new BufferGeometry();

			const positions = [
				0, 0, 0, 	0, 0, 1,
				0, 0, 0, 	1, 0, 1,
				0, 0, 0,	- 1, 0, 1,
				0, 0, 0, 	0, 1, 1,
				0, 0, 0, 	0, - 1, 1
			];

			for ( let i = 0, j = 1, l = 32; i < l; i ++, j ++ ) {

				const p1 = ( i / l ) * Math.PI * 2;
				const p2 = ( j / l ) * Math.PI * 2;

				positions.push(
					Math.cos( p1 ), Math.sin( p1 ), 1,
					Math.cos( p2 ), Math.sin( p2 ), 1
				);

			}

			geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );

			const material = new LineBasicMaterial( { fog: false, toneMapped: false } );

			this.cone = new LineSegments( geometry, material );
			this.add( this.cone );

			this.update();

		}

		dispose() {

			this.cone.geometry.dispose();
			this.cone.material.dispose();

		}

		update() {

			this.light.updateMatrixWorld();

			const coneLength = this.light.distance ? this.light.distance : 1000;
			const coneWidth = coneLength * Math.tan( this.light.angle );

			this.cone.scale.set( coneWidth, coneWidth, coneLength );

			_vector$9.setFromMatrixPosition( this.light.target.matrixWorld );

			this.cone.lookAt( _vector$9 );

			if ( this.color !== undefined ) {

				this.cone.material.color.set( this.color );

			} else {

				this.cone.material.color.copy( this.light.color );

			}

		}

	}

	const _vector$a = /*@__PURE__*/ new Vector3();
	const _boneMatrix = /*@__PURE__*/ new Matrix4();
	const _matrixWorldInv = /*@__PURE__*/ new Matrix4();


	class SkeletonHelper extends LineSegments {

		constructor( object ) {

			const bones = getBoneList( object );

			const geometry = new BufferGeometry();

			const vertices = [];
			const colors = [];

			const color1 = new Color( 0, 0, 1 );
			const color2 = new Color( 0, 1, 0 );

			for ( let i = 0; i < bones.length; i ++ ) {

				const bone = bones[ i ];

				if ( bone.parent && bone.parent.isBone ) {

					vertices.push( 0, 0, 0 );
					vertices.push( 0, 0, 0 );
					colors.push( color1.r, color1.g, color1.b );
					colors.push( color2.r, color2.g, color2.b );

				}

			}

			geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );

			const material = new LineBasicMaterial( { vertexColors: true, depthTest: false, depthWrite: false, toneMapped: false, transparent: true } );

			super( geometry, material );

			this.type = 'SkeletonHelper';
			this.isSkeletonHelper = true;

			this.root = object;
			this.bones = bones;

			this.matrix = object.matrixWorld;
			this.matrixAutoUpdate = false;

		}

		updateMatrixWorld( force ) {

			const bones = this.bones;

			const geometry = this.geometry;
			const position = geometry.getAttribute( 'position' );

			_matrixWorldInv.copy( this.root.matrixWorld ).invert();

			for ( let i = 0, j = 0; i < bones.length; i ++ ) {

				const bone = bones[ i ];

				if ( bone.parent && bone.parent.isBone ) {

					_boneMatrix.multiplyMatrices( _matrixWorldInv, bone.matrixWorld );
					_vector$a.setFromMatrixPosition( _boneMatrix );
					position.setXYZ( j, _vector$a.x, _vector$a.y, _vector$a.z );

					_boneMatrix.multiplyMatrices( _matrixWorldInv, bone.parent.matrixWorld );
					_vector$a.setFromMatrixPosition( _boneMatrix );
					position.setXYZ( j + 1, _vector$a.x, _vector$a.y, _vector$a.z );

					j += 2;

				}

			}

			geometry.getAttribute( 'position' ).needsUpdate = true;

			super.updateMatrixWorld( force );

		}

	}


	function getBoneList( object ) {

		const boneList = [];

		if ( object && object.isBone ) {

			boneList.push( object );

		}

		for ( let i = 0; i < object.children.length; i ++ ) {

			boneList.push.apply( boneList, getBoneList( object.children[ i ] ) );

		}

		return boneList;

	}

	class PointLightHelper extends Mesh {

		constructor( light, sphereSize, color ) {

			const geometry = new SphereBufferGeometry( sphereSize, 4, 2 );
			const material = new MeshBasicMaterial( { wireframe: true, fog: false, toneMapped: false } );

			super( geometry, material );

			this.light = light;
			this.light.updateMatrixWorld();

			this.color = color;

			this.type = 'PointLightHelper';

			this.matrix = this.light.matrixWorld;
			this.matrixAutoUpdate = false;

			this.update();


			/*
		// TODO: delete this comment?
		const distanceGeometry = new THREE.IcosahedronBufferGeometry( 1, 2 );
		const distanceMaterial = new THREE.MeshBasicMaterial( { color: hexColor, fog: false, wireframe: true, opacity: 0.1, transparent: true } );

		this.lightSphere = new THREE.Mesh( bulbGeometry, bulbMaterial );
		this.lightDistance = new THREE.Mesh( distanceGeometry, distanceMaterial );

		const d = light.distance;

		if ( d === 0.0 ) {

			this.lightDistance.visible = false;

		} else {

			this.lightDistance.scale.set( d, d, d );

		}

		this.add( this.lightDistance );
		*/

		}

		dispose() {

			this.geometry.dispose();
			this.material.dispose();

		}

		update() {

			if ( this.color !== undefined ) {

				this.material.color.set( this.color );

			} else {

				this.material.color.copy( this.light.color );

			}

			/*
			const d = this.light.distance;

			if ( d === 0.0 ) {

				this.lightDistance.visible = false;

			} else {

				this.lightDistance.visible = true;
				this.lightDistance.scale.set( d, d, d );

			}
			*/

		}

	}

	const _vector$b = /*@__PURE__*/ new Vector3();
	const _color1 = /*@__PURE__*/ new Color();
	const _color2 = /*@__PURE__*/ new Color();

	class HemisphereLightHelper extends Object3D {

		constructor( light, size, color ) {

			super();
			this.light = light;
			this.light.updateMatrixWorld();

			this.matrix = light.matrixWorld;
			this.matrixAutoUpdate = false;

			this.color = color;

			const geometry = new OctahedronBufferGeometry( size );
			geometry.rotateY( Math.PI * 0.5 );

			this.material = new MeshBasicMaterial( { wireframe: true, fog: false, toneMapped: false } );
			if ( this.color === undefined ) this.material.vertexColors = true;

			const position = geometry.getAttribute( 'position' );
			const colors = new Float32Array( position.count * 3 );

			geometry.setAttribute( 'color', new BufferAttribute( colors, 3 ) );

			this.add( new Mesh( geometry, this.material ) );

			this.update();

		}

		dispose() {

			this.children[ 0 ].geometry.dispose();
			this.children[ 0 ].material.dispose();

		}

		update() {

			const mesh = this.children[ 0 ];

			if ( this.color !== undefined ) {

				this.material.color.set( this.color );

			} else {

				const colors = mesh.geometry.getAttribute( 'color' );

				_color1.copy( this.light.color );
				_color2.copy( this.light.groundColor );

				for ( let i = 0, l = colors.count; i < l; i ++ ) {

					const color = ( i < ( l / 2 ) ) ? _color1 : _color2;

					colors.setXYZ( i, color.r, color.g, color.b );

				}

				colors.needsUpdate = true;

			}

			mesh.lookAt( _vector$b.setFromMatrixPosition( this.light.matrixWorld ).negate() );

		}

	}

	class GridHelper extends LineSegments {

		constructor( size = 10, divisions = 10, color1 = 0x444444, color2 = 0x888888 ) {

			color1 = new Color( color1 );
			color2 = new Color( color2 );

			const center = divisions / 2;
			const step = size / divisions;
			const halfSize = size / 2;

			const vertices = [], colors = [];

			for ( let i = 0, j = 0, k = - halfSize; i <= divisions; i ++, k += step ) {

				vertices.push( - halfSize, 0, k, halfSize, 0, k );
				vertices.push( k, 0, - halfSize, k, 0, halfSize );

				const color = i === center ? color1 : color2;

				color.toArray( colors, j ); j += 3;
				color.toArray( colors, j ); j += 3;
				color.toArray( colors, j ); j += 3;
				color.toArray( colors, j ); j += 3;

			}

			const geometry = new BufferGeometry();
			geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );

			const material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } );

			super( geometry, material );

			this.type = 'GridHelper';

		}

	}

	class PolarGridHelper extends LineSegments {

		constructor( radius = 10, radials = 16, circles = 8, divisions = 64, color1 = 0x444444, color2 = 0x888888 ) {

			color1 = new Color( color1 );
			color2 = new Color( color2 );

			const vertices = [];
			const colors = [];

			// create the radials

			for ( let i = 0; i <= radials; i ++ ) {

				const v = ( i / radials ) * ( Math.PI * 2 );

				const x = Math.sin( v ) * radius;
				const z = Math.cos( v ) * radius;

				vertices.push( 0, 0, 0 );
				vertices.push( x, 0, z );

				const color = ( i & 1 ) ? color1 : color2;

				colors.push( color.r, color.g, color.b );
				colors.push( color.r, color.g, color.b );

			}

			// create the circles

			for ( let i = 0; i <= circles; i ++ ) {

				const color = ( i & 1 ) ? color1 : color2;

				const r = radius - ( radius / circles * i );

				for ( let j = 0; j < divisions; j ++ ) {

					// first vertex

					let v = ( j / divisions ) * ( Math.PI * 2 );

					let x = Math.sin( v ) * r;
					let z = Math.cos( v ) * r;

					vertices.push( x, 0, z );
					colors.push( color.r, color.g, color.b );

					// second vertex

					v = ( ( j + 1 ) / divisions ) * ( Math.PI * 2 );

					x = Math.sin( v ) * r;
					z = Math.cos( v ) * r;

					vertices.push( x, 0, z );
					colors.push( color.r, color.g, color.b );

				}

			}

			const geometry = new BufferGeometry();
			geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );

			const material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } );

			super( geometry, material );

			this.type = 'PolarGridHelper';

		}

	}

	const _v1$6 = /*@__PURE__*/ new Vector3();
	const _v2$3 = /*@__PURE__*/ new Vector3();
	const _v3$1 = /*@__PURE__*/ new Vector3();

	class DirectionalLightHelper extends Object3D {

		constructor( light, size, color ) {

			super();
			this.light = light;
			this.light.updateMatrixWorld();

			this.matrix = light.matrixWorld;
			this.matrixAutoUpdate = false;

			this.color = color;

			if ( size === undefined ) size = 1;

			let geometry = new BufferGeometry();
			geometry.setAttribute( 'position', new Float32BufferAttribute( [
				- size, size, 0,
				size, size, 0,
				size, - size, 0,
				- size, - size, 0,
				- size, size, 0
			], 3 ) );

			const material = new LineBasicMaterial( { fog: false, toneMapped: false } );

			this.lightPlane = new Line( geometry, material );
			this.add( this.lightPlane );

			geometry = new BufferGeometry();
			geometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 0, 0, 1 ], 3 ) );

			this.targetLine = new Line( geometry, material );
			this.add( this.targetLine );

			this.update();

		}

		dispose() {

			this.lightPlane.geometry.dispose();
			this.lightPlane.material.dispose();
			this.targetLine.geometry.dispose();
			this.targetLine.material.dispose();

		}

		update() {

			_v1$6.setFromMatrixPosition( this.light.matrixWorld );
			_v2$3.setFromMatrixPosition( this.light.target.matrixWorld );
			_v3$1.subVectors( _v2$3, _v1$6 );

			this.lightPlane.lookAt( _v2$3 );

			if ( this.color !== undefined ) {

				this.lightPlane.material.color.set( this.color );
				this.targetLine.material.color.set( this.color );

			} else {

				this.lightPlane.material.color.copy( this.light.color );
				this.targetLine.material.color.copy( this.light.color );

			}

			this.targetLine.lookAt( _v2$3 );
			this.targetLine.scale.z = _v3$1.length();

		}

	}

	const _vector$c = /*@__PURE__*/ new Vector3();
	const _camera = /*@__PURE__*/ new Camera();

	/**
	 *	- shows frustum, line of sight and up of the camera
	 *	- suitable for fast updates
	 * 	- based on frustum visualization in lightgl.js shadowmap example
	 *		http://evanw.github.com/lightgl.js/tests/shadowmap.html
	 */

	class CameraHelper extends LineSegments {

		constructor( camera ) {

			const geometry = new BufferGeometry();
			const material = new LineBasicMaterial( { color: 0xffffff, vertexColors: true, toneMapped: false } );

			const vertices = [];
			const colors = [];

			const pointMap = {};

			// colors

			const colorFrustum = new Color( 0xffaa00 );
			const colorCone = new Color( 0xff0000 );
			const colorUp = new Color( 0x00aaff );
			const colorTarget = new Color( 0xffffff );
			const colorCross = new Color( 0x333333 );

			// near

			addLine( 'n1', 'n2', colorFrustum );
			addLine( 'n2', 'n4', colorFrustum );
			addLine( 'n4', 'n3', colorFrustum );
			addLine( 'n3', 'n1', colorFrustum );

			// far

			addLine( 'f1', 'f2', colorFrustum );
			addLine( 'f2', 'f4', colorFrustum );
			addLine( 'f4', 'f3', colorFrustum );
			addLine( 'f3', 'f1', colorFrustum );

			// sides

			addLine( 'n1', 'f1', colorFrustum );
			addLine( 'n2', 'f2', colorFrustum );
			addLine( 'n3', 'f3', colorFrustum );
			addLine( 'n4', 'f4', colorFrustum );

			// cone

			addLine( 'p', 'n1', colorCone );
			addLine( 'p', 'n2', colorCone );
			addLine( 'p', 'n3', colorCone );
			addLine( 'p', 'n4', colorCone );

			// up

			addLine( 'u1', 'u2', colorUp );
			addLine( 'u2', 'u3', colorUp );
			addLine( 'u3', 'u1', colorUp );

			// target

			addLine( 'c', 't', colorTarget );
			addLine( 'p', 'c', colorCross );

			// cross

			addLine( 'cn1', 'cn2', colorCross );
			addLine( 'cn3', 'cn4', colorCross );

			addLine( 'cf1', 'cf2', colorCross );
			addLine( 'cf3', 'cf4', colorCross );

			function addLine( a, b, color ) {

				addPoint( a, color );
				addPoint( b, color );

			}

			function addPoint( id, color ) {

				vertices.push( 0, 0, 0 );
				colors.push( color.r, color.g, color.b );

				if ( pointMap[ id ] === undefined ) {

					pointMap[ id ] = [];

				}

				pointMap[ id ].push( ( vertices.length / 3 ) - 1 );

			}

			geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );

			super( geometry, material );

			this.type = 'CameraHelper';

			this.camera = camera;
			if ( this.camera.updateProjectionMatrix ) this.camera.updateProjectionMatrix();

			this.matrix = camera.matrixWorld;
			this.matrixAutoUpdate = false;

			this.pointMap = pointMap;

			this.update();

		}

		update() {

			const geometry = this.geometry;
			const pointMap = this.pointMap;

			const w = 1, h = 1;

			// we need just camera projection matrix inverse
			// world matrix must be identity

			_camera.projectionMatrixInverse.copy( this.camera.projectionMatrixInverse );

			// center / target

			setPoint( 'c', pointMap, geometry, _camera, 0, 0, - 1 );
			setPoint( 't', pointMap, geometry, _camera, 0, 0, 1 );

			// near

			setPoint( 'n1', pointMap, geometry, _camera, - w, - h, - 1 );
			setPoint( 'n2', pointMap, geometry, _camera, w, - h, - 1 );
			setPoint( 'n3', pointMap, geometry, _camera, - w, h, - 1 );
			setPoint( 'n4', pointMap, geometry, _camera, w, h, - 1 );

			// far

			setPoint( 'f1', pointMap, geometry, _camera, - w, - h, 1 );
			setPoint( 'f2', pointMap, geometry, _camera, w, - h, 1 );
			setPoint( 'f3', pointMap, geometry, _camera, - w, h, 1 );
			setPoint( 'f4', pointMap, geometry, _camera, w, h, 1 );

			// up

			setPoint( 'u1', pointMap, geometry, _camera, w * 0.7, h * 1.1, - 1 );
			setPoint( 'u2', pointMap, geometry, _camera, - w * 0.7, h * 1.1, - 1 );
			setPoint( 'u3', pointMap, geometry, _camera, 0, h * 2, - 1 );

			// cross

			setPoint( 'cf1', pointMap, geometry, _camera, - w, 0, 1 );
			setPoint( 'cf2', pointMap, geometry, _camera, w, 0, 1 );
			setPoint( 'cf3', pointMap, geometry, _camera, 0, - h, 1 );
			setPoint( 'cf4', pointMap, geometry, _camera, 0, h, 1 );

			setPoint( 'cn1', pointMap, geometry, _camera, - w, 0, - 1 );
			setPoint( 'cn2', pointMap, geometry, _camera, w, 0, - 1 );
			setPoint( 'cn3', pointMap, geometry, _camera, 0, - h, - 1 );
			setPoint( 'cn4', pointMap, geometry, _camera, 0, h, - 1 );

			geometry.getAttribute( 'position' ).needsUpdate = true;

		}

	}


	function setPoint( point, pointMap, geometry, camera, x, y, z ) {

		_vector$c.set( x, y, z ).unproject( camera );

		const points = pointMap[ point ];

		if ( points !== undefined ) {

			const position = geometry.getAttribute( 'position' );

			for ( let i = 0, l = points.length; i < l; i ++ ) {

				position.setXYZ( points[ i ], _vector$c.x, _vector$c.y, _vector$c.z );

			}

		}

	}

	const _box$3 = /*@__PURE__*/ new Box3();

	class BoxHelper extends LineSegments {

		constructor( object, color = 0xffff00 ) {

			const indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] );
			const positions = new Float32Array( 8 * 3 );

			const geometry = new BufferGeometry();
			geometry.setIndex( new BufferAttribute( indices, 1 ) );
			geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );

			super( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );

			this.object = object;
			this.type = 'BoxHelper';

			this.matrixAutoUpdate = false;

			this.update();

		}

		update( object ) {

			if ( object !== undefined ) {

				console.warn( 'THREE.BoxHelper: .update() has no longer arguments.' );

			}

			if ( this.object !== undefined ) {

				_box$3.setFromObject( this.object );

			}

			if ( _box$3.isEmpty() ) return;

			const min = _box$3.min;
			const max = _box$3.max;

			/*
				5____4
			1/___0/|
			| 6__|_7
			2/___3/

			0: max.x, max.y, max.z
			1: min.x, max.y, max.z
			2: min.x, min.y, max.z
			3: max.x, min.y, max.z
			4: max.x, max.y, min.z
			5: min.x, max.y, min.z
			6: min.x, min.y, min.z
			7: max.x, min.y, min.z
			*/

			const position = this.geometry.attributes.position;
			const array = position.array;

			array[ 0 ] = max.x; array[ 1 ] = max.y; array[ 2 ] = max.z;
			array[ 3 ] = min.x; array[ 4 ] = max.y; array[ 5 ] = max.z;
			array[ 6 ] = min.x; array[ 7 ] = min.y; array[ 8 ] = max.z;
			array[ 9 ] = max.x; array[ 10 ] = min.y; array[ 11 ] = max.z;
			array[ 12 ] = max.x; array[ 13 ] = max.y; array[ 14 ] = min.z;
			array[ 15 ] = min.x; array[ 16 ] = max.y; array[ 17 ] = min.z;
			array[ 18 ] = min.x; array[ 19 ] = min.y; array[ 20 ] = min.z;
			array[ 21 ] = max.x; array[ 22 ] = min.y; array[ 23 ] = min.z;

			position.needsUpdate = true;

			this.geometry.computeBoundingSphere();


		}

		setFromObject( object ) {

			this.object = object;
			this.update();

			return this;

		}

		copy( source ) {

			LineSegments.prototype.copy.call( this, source );

			this.object = source.object;

			return this;

		}

	}

	class Box3Helper extends LineSegments {

		constructor( box, color = 0xffff00 ) {

			const indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] );

			const positions = [ 1, 1, 1, - 1, 1, 1, - 1, - 1, 1, 1, - 1, 1, 1, 1, - 1, - 1, 1, - 1, - 1, - 1, - 1, 1, - 1, - 1 ];

			const geometry = new BufferGeometry();

			geometry.setIndex( new BufferAttribute( indices, 1 ) );

			geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );

			super( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );

			this.box = box;

			this.type = 'Box3Helper';

			this.geometry.computeBoundingSphere();

		}

		updateMatrixWorld( force ) {

			const box = this.box;

			if ( box.isEmpty() ) return;

			box.getCenter( this.position );

			box.getSize( this.scale );

			this.scale.multiplyScalar( 0.5 );

			super.updateMatrixWorld( force );

		}

	}

	class PlaneHelper extends Line {

		constructor( plane, size = 1, hex = 0xffff00 ) {

			const color = hex;

			const positions = [ 1, - 1, 1, - 1, 1, 1, - 1, - 1, 1, 1, 1, 1, - 1, 1, 1, - 1, - 1, 1, 1, - 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0 ];

			const geometry = new BufferGeometry();
			geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
			geometry.computeBoundingSphere();

			super( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );

			this.type = 'PlaneHelper';

			this.plane = plane;

			this.size = size;

			const positions2 = [ 1, 1, 1, - 1, 1, 1, - 1, - 1, 1, 1, 1, 1, - 1, - 1, 1, 1, - 1, 1 ];

			const geometry2 = new BufferGeometry();
			geometry2.setAttribute( 'position', new Float32BufferAttribute( positions2, 3 ) );
			geometry2.computeBoundingSphere();

			this.add( new Mesh( geometry2, new MeshBasicMaterial( { color: color, opacity: 0.2, transparent: true, depthWrite: false, toneMapped: false } ) ) );

		}

		updateMatrixWorld( force ) {

			let scale = - this.plane.constant;

			if ( Math.abs( scale ) < 1e-8 ) scale = 1e-8; // sign does not matter

			this.scale.set( 0.5 * this.size, 0.5 * this.size, scale );

			this.children[ 0 ].material.side = ( scale < 0 ) ? BackSide : FrontSide; // renderer flips side when determinant < 0; flipping not wanted here

			this.lookAt( this.plane.normal );

			super.updateMatrixWorld( force );

		}

	}

	const _axis = /*@__PURE__*/ new Vector3();
	let _lineGeometry, _coneGeometry;

	class ArrowHelper extends Object3D {

		constructor( dir, origin, length, color, headLength, headWidth ) {

			super();
			// dir is assumed to be normalized

			this.type = 'ArrowHelper';

			if ( dir === undefined ) dir = new Vector3( 0, 0, 1 );
			if ( origin === undefined ) origin = new Vector3( 0, 0, 0 );
			if ( length === undefined ) length = 1;
			if ( color === undefined ) color = 0xffff00;
			if ( headLength === undefined ) headLength = 0.2 * length;
			if ( headWidth === undefined ) headWidth = 0.2 * headLength;

			if ( _lineGeometry === undefined ) {

				_lineGeometry = new BufferGeometry();
				_lineGeometry.setAttribute( 'position', new Float32BufferAttribute( [ 0, 0, 0, 0, 1, 0 ], 3 ) );

				_coneGeometry = new CylinderBufferGeometry( 0, 0.5, 1, 5, 1 );
				_coneGeometry.translate( 0, - 0.5, 0 );

			}

			this.position.copy( origin );

			this.line = new Line( _lineGeometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );
			this.line.matrixAutoUpdate = false;
			this.add( this.line );

			this.cone = new Mesh( _coneGeometry, new MeshBasicMaterial( { color: color, toneMapped: false } ) );
			this.cone.matrixAutoUpdate = false;
			this.add( this.cone );

			this.setDirection( dir );
			this.setLength( length, headLength, headWidth );

		}

		setDirection( dir ) {

			// dir is assumed to be normalized

			if ( dir.y > 0.99999 ) {

				this.quaternion.set( 0, 0, 0, 1 );

			} else if ( dir.y < - 0.99999 ) {

				this.quaternion.set( 1, 0, 0, 0 );

			} else {

				_axis.set( dir.z, 0, - dir.x ).normalize();

				const radians = Math.acos( dir.y );

				this.quaternion.setFromAxisAngle( _axis, radians );

			}

		}

		setLength( length, headLength, headWidth ) {

			if ( headLength === undefined ) headLength = 0.2 * length;
			if ( headWidth === undefined ) headWidth = 0.2 * headLength;

			this.line.scale.set( 1, Math.max( 0.0001, length - headLength ), 1 ); // see #17458
			this.line.updateMatrix();

			this.cone.scale.set( headWidth, headLength, headWidth );
			this.cone.position.y = length;
			this.cone.updateMatrix();

		}

		setColor( color ) {

			this.line.material.color.set( color );
			this.cone.material.color.set( color );

		}

		copy( source ) {

			super.copy( source, false );

			this.line.copy( source.line );
			this.cone.copy( source.cone );

			return this;

		}

	}

	class AxesHelper extends LineSegments {

		constructor( size = 1 ) {

			const vertices = [
				0, 0, 0,	size, 0, 0,
				0, 0, 0,	0, size, 0,
				0, 0, 0,	0, 0, size
			];

			const colors = [
				1, 0, 0,	1, 0.6, 0,
				0, 1, 0,	0.6, 1, 0,
				0, 0, 1,	0, 0.6, 1
			];

			const geometry = new BufferGeometry();
			geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
			geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );

			const material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } );

			super( geometry, material );

			this.type = 'AxesHelper';

		}

	}

	const _floatView = new Float32Array( 1 );
	const _int32View = new Int32Array( _floatView.buffer );

	const DataUtils = {

		// Converts float32 to float16 (stored as uint16 value).

		toHalfFloat: function ( val ) {

			// Source: http://gamedev.stackexchange.com/questions/17326/conversion-of-a-number-from-single-precision-floating-point-representation-to-a/17410#17410

			/* This method is faster than the OpenEXR implementation (very often
			* used, eg. in Ogre), with the additional benefit of rounding, inspired
			* by James Tursa?s half-precision code. */

			_floatView[ 0 ] = val;
			const x = _int32View[ 0 ];

			let bits = ( x >> 16 ) & 0x8000; /* Get the sign */
			let m = ( x >> 12 ) & 0x07ff; /* Keep one extra bit for rounding */
			const e = ( x >> 23 ) & 0xff; /* Using int is faster here */

			/* If zero, or denormal, or exponent underflows too much for a denormal
				* half, return signed zero. */
			if ( e < 103 ) return bits;

			/* If NaN, return NaN. If Inf or exponent overflow, return Inf. */
			if ( e > 142 ) {

				bits |= 0x7c00;
				/* If exponent was 0xff and one mantissa bit was set, it means NaN,
							* not Inf, so make sure we set one mantissa bit too. */
				bits |= ( ( e == 255 ) ? 0 : 1 ) && ( x & 0x007fffff );
				return bits;

			}

			/* If exponent underflows but not too much, return a denormal */
			if ( e < 113 ) {

				m |= 0x0800;
				/* Extra rounding may overflow and set mantissa to 0 and exponent
					* to 1, which is OK. */
				bits |= ( m >> ( 114 - e ) ) + ( ( m >> ( 113 - e ) ) & 1 );
				return bits;

			}

			bits |= ( ( e - 112 ) << 10 ) | ( m >> 1 );
			/* Extra rounding. An overflow will set mantissa to 0 and increment
				* the exponent, which is OK. */
			bits += m & 1;
			return bits;

		}

	};

	const LOD_MIN = 4;
	const LOD_MAX = 8;
	const SIZE_MAX = Math.pow( 2, LOD_MAX );

	// The standard deviations (radians) associated with the extra mips. These are
	// chosen to approximate a Trowbridge-Reitz distribution function times the
	// geometric shadowing function. These sigma values squared must match the
	// variance #defines in cube_uv_reflection_fragment.glsl.js.
	const EXTRA_LOD_SIGMA = [ 0.125, 0.215, 0.35, 0.446, 0.526, 0.582 ];

	const TOTAL_LODS = LOD_MAX - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length;

	// The maximum length of the blur for loop. Smaller sigmas will use fewer
	// samples and exit early, but not recompile the shader.
	const MAX_SAMPLES = 20;

	const ENCODINGS = {
		[ LinearEncoding ]: 0,
		[ sRGBEncoding ]: 1,
		[ RGBEEncoding ]: 2,
		[ RGBM7Encoding ]: 3,
		[ RGBM16Encoding ]: 4,
		[ RGBDEncoding ]: 5,
		[ GammaEncoding ]: 6
	};

	const _flatCamera = /*@__PURE__*/ new OrthographicCamera();
	const { _lodPlanes, _sizeLods, _sigmas } = /*@__PURE__*/ _createPlanes();
	const _clearColor = /*@__PURE__*/ new Color();
	let _oldTarget = null;

	// Golden Ratio
	const PHI = ( 1 + Math.sqrt( 5 ) ) / 2;
	const INV_PHI = 1 / PHI;

	// Vertices of a dodecahedron (except the opposites, which represent the
	// same axis), used as axis directions evenly spread on a sphere.
	const _axisDirections = [
		/*@__PURE__*/ new Vector3( 1, 1, 1 ),
		/*@__PURE__*/ new Vector3( - 1, 1, 1 ),
		/*@__PURE__*/ new Vector3( 1, 1, - 1 ),
		/*@__PURE__*/ new Vector3( - 1, 1, - 1 ),
		/*@__PURE__*/ new Vector3( 0, PHI, INV_PHI ),
		/*@__PURE__*/ new Vector3( 0, PHI, - INV_PHI ),
		/*@__PURE__*/ new Vector3( INV_PHI, 0, PHI ),
		/*@__PURE__*/ new Vector3( - INV_PHI, 0, PHI ),
		/*@__PURE__*/ new Vector3( PHI, INV_PHI, 0 ),
		/*@__PURE__*/ new Vector3( - PHI, INV_PHI, 0 ) ];

	/**
	 * This class generates a Prefiltered, Mipmapped Radiance Environment Map
	 * (PMREM) from a cubeMap environment texture. This allows different levels of
	 * blur to be quickly accessed based on material roughness. It is packed into a
	 * special CubeUV format that allows us to perform custom interpolation so that
	 * we can support nonlinear formats such as RGBE. Unlike a traditional mipmap
	 * chain, it only goes down to the LOD_MIN level (above), and then creates extra
	 * even more filtered 'mips' at the same LOD_MIN resolution, associated with
	 * higher roughness levels. In this way we maintain resolution to smoothly
	 * interpolate diffuse lighting while limiting sampling computation.
	 */

	class PMREMGenerator {

		constructor( renderer ) {

			this._renderer = renderer;
			this._pingPongRenderTarget = null;

			this._blurMaterial = _getBlurShader( MAX_SAMPLES );
			this._equirectShader = null;
			this._cubemapShader = null;

			this._compileMaterial( this._blurMaterial );

		}

		/**
		 * Generates a PMREM from a supplied Scene, which can be faster than using an
		 * image if networking bandwidth is low. Optional sigma specifies a blur radius
		 * in radians to be applied to the scene before PMREM generation. Optional near
		 * and far planes ensure the scene is rendered in its entirety (the cubeCamera
		 * is placed at the origin).
		 */
		fromScene( scene, sigma = 0, near = 0.1, far = 100 ) {

			_oldTarget = this._renderer.getRenderTarget();
			const cubeUVRenderTarget = this._allocateTargets();

			this._sceneToCubeUV( scene, near, far, cubeUVRenderTarget );
			if ( sigma > 0 ) {

				this._blur( cubeUVRenderTarget, 0, 0, sigma );

			}

			this._applyPMREM( cubeUVRenderTarget );
			this._cleanup( cubeUVRenderTarget );

			return cubeUVRenderTarget;

		}

		/**
		 * Generates a PMREM from an equirectangular texture, which can be either LDR
		 * (RGBFormat) or HDR (RGBEFormat). The ideal input image size is 1k (1024 x 512),
		 * as this matches best with the 256 x 256 cubemap output.
		 */
		fromEquirectangular( equirectangular ) {

			return this._fromTexture( equirectangular );

		}

		/**
		 * Generates a PMREM from an cubemap texture, which can be either LDR
		 * (RGBFormat) or HDR (RGBEFormat). The ideal input cube size is 256 x 256,
		 * as this matches best with the 256 x 256 cubemap output.
		 */
		fromCubemap( cubemap ) {

			return this._fromTexture( cubemap );

		}

		/**
		 * Pre-compiles the cubemap shader. You can get faster start-up by invoking this method during
		 * your texture's network fetch for increased concurrency.
		 */
		compileCubemapShader() {

			if ( this._cubemapShader === null ) {

				this._cubemapShader = _getCubemapShader();
				this._compileMaterial( this._cubemapShader );

			}

		}

		/**
		 * Pre-compiles the equirectangular shader. You can get faster start-up by invoking this method during
		 * your texture's network fetch for increased concurrency.
		 */
		compileEquirectangularShader() {

			if ( this._equirectShader === null ) {

				this._equirectShader = _getEquirectShader();
				this._compileMaterial( this._equirectShader );

			}

		}

		/**
		 * Disposes of the PMREMGenerator's internal memory. Note that PMREMGenerator is a static class,
		 * so you should not need more than one PMREMGenerator object. If you do, calling dispose() on
		 * one of them will cause any others to also become unusable.
		 */
		dispose() {

			this._blurMaterial.dispose();

			if ( this._cubemapShader !== null ) this._cubemapShader.dispose();
			if ( this._equirectShader !== null ) this._equirectShader.dispose();

			for ( let i = 0; i < _lodPlanes.length; i ++ ) {

				_lodPlanes[ i ].dispose();

			}

		}

		// private interface

		_cleanup( outputTarget ) {

			this._pingPongRenderTarget.dispose();
			this._renderer.setRenderTarget( _oldTarget );
			outputTarget.scissorTest = false;
			_setViewport( outputTarget, 0, 0, outputTarget.width, outputTarget.height );

		}

		_fromTexture( texture ) {

			_oldTarget = this._renderer.getRenderTarget();
			const cubeUVRenderTarget = this._allocateTargets( texture );
			this._textureToCubeUV( texture, cubeUVRenderTarget );
			this._applyPMREM( cubeUVRenderTarget );
			this._cleanup( cubeUVRenderTarget );

			return cubeUVRenderTarget;

		}

		_allocateTargets( texture ) { // warning: null texture is valid

			const params = {
				magFilter: NearestFilter,
				minFilter: NearestFilter,
				generateMipmaps: false,
				type: UnsignedByteType,
				format: RGBEFormat,
				encoding: _isLDR( texture ) ? texture.encoding : RGBEEncoding,
				depthBuffer: false
			};

			const cubeUVRenderTarget = _createRenderTarget( params );
			cubeUVRenderTarget.depthBuffer = texture ? false : true;
			this._pingPongRenderTarget = _createRenderTarget( params );
			return cubeUVRenderTarget;

		}

		_compileMaterial( material ) {

			const tmpMesh = new Mesh( _lodPlanes[ 0 ], material );
			this._renderer.compile( tmpMesh, _flatCamera );

		}

		_sceneToCubeUV( scene, near, far, cubeUVRenderTarget ) {

			const fov = 90;
			const aspect = 1;
			const cubeCamera = new PerspectiveCamera( fov, aspect, near, far );
			const upSign = [ 1, - 1, 1, 1, 1, 1 ];
			const forwardSign = [ 1, 1, 1, - 1, - 1, - 1 ];
			const renderer = this._renderer;

			const outputEncoding = renderer.outputEncoding;
			const toneMapping = renderer.toneMapping;
			renderer.getClearColor( _clearColor );
			const clearAlpha = renderer.getClearAlpha();

			renderer.toneMapping = NoToneMapping;
			renderer.outputEncoding = LinearEncoding;

			let background = scene.background;
			if ( background && background.isColor ) {

				background.convertSRGBToLinear();
				// Convert linear to RGBE
				const maxComponent = Math.max( background.r, background.g, background.b );
				const fExp = Math.min( Math.max( Math.ceil( Math.log2( maxComponent ) ), - 128.0 ), 127.0 );
				background = background.multiplyScalar( Math.pow( 2.0, - fExp ) );
				const alpha = ( fExp + 128.0 ) / 255.0;
				renderer.setClearColor( background, alpha );
				scene.background = null;

			}

			for ( let i = 0; i < 6; i ++ ) {

				const col = i % 3;
				if ( col == 0 ) {

					cubeCamera.up.set( 0, upSign[ i ], 0 );
					cubeCamera.lookAt( forwardSign[ i ], 0, 0 );

				} else if ( col == 1 ) {

					cubeCamera.up.set( 0, 0, upSign[ i ] );
					cubeCamera.lookAt( 0, forwardSign[ i ], 0 );

				} else {

					cubeCamera.up.set( 0, upSign[ i ], 0 );
					cubeCamera.lookAt( 0, 0, forwardSign[ i ] );

				}

				_setViewport( cubeUVRenderTarget,
					col * SIZE_MAX, i > 2 ? SIZE_MAX : 0, SIZE_MAX, SIZE_MAX );
				renderer.setRenderTarget( cubeUVRenderTarget );
				renderer.render( scene, cubeCamera );

			}

			renderer.toneMapping = toneMapping;
			renderer.outputEncoding = outputEncoding;
			renderer.setClearColor( _clearColor, clearAlpha );

		}

		_textureToCubeUV( texture, cubeUVRenderTarget ) {

			const renderer = this._renderer;

			if ( texture.isCubeTexture ) {

				if ( this._cubemapShader == null ) {

					this._cubemapShader = _getCubemapShader();

				}

			} else {

				if ( this._equirectShader == null ) {

					this._equirectShader = _getEquirectShader();

				}

			}

			const material = texture.isCubeTexture ? this._cubemapShader : this._equirectShader;
			const mesh = new Mesh( _lodPlanes[ 0 ], material );

			const uniforms = material.uniforms;

			uniforms[ 'envMap' ].value = texture;

			if ( ! texture.isCubeTexture ) {

				uniforms[ 'texelSize' ].value.set( 1.0 / texture.image.width, 1.0 / texture.image.height );

			}

			uniforms[ 'inputEncoding' ].value = ENCODINGS[ texture.encoding ];
			uniforms[ 'outputEncoding' ].value = ENCODINGS[ cubeUVRenderTarget.texture.encoding ];

			_setViewport( cubeUVRenderTarget, 0, 0, 3 * SIZE_MAX, 2 * SIZE_MAX );

			renderer.setRenderTarget( cubeUVRenderTarget );
			renderer.render( mesh, _flatCamera );

		}

		_applyPMREM( cubeUVRenderTarget ) {

			const renderer = this._renderer;
			const autoClear = renderer.autoClear;
			renderer.autoClear = false;

			for ( let i = 1; i < TOTAL_LODS; i ++ ) {

				const sigma = Math.sqrt( _sigmas[ i ] * _sigmas[ i ] - _sigmas[ i - 1 ] * _sigmas[ i - 1 ] );

				const poleAxis = _axisDirections[ ( i - 1 ) % _axisDirections.length ];

				this._blur( cubeUVRenderTarget, i - 1, i, sigma, poleAxis );

			}

			renderer.autoClear = autoClear;

		}

		/**
		 * This is a two-pass Gaussian blur for a cubemap. Normally this is done
		 * vertically and horizontally, but this breaks down on a cube. Here we apply
		 * the blur latitudinally (around the poles), and then longitudinally (towards
		 * the poles) to approximate the orthogonally-separable blur. It is least
		 * accurate at the poles, but still does a decent job.
		 */
		_blur( cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis ) {

			const pingPongRenderTarget = this._pingPongRenderTarget;

			this._halfBlur(
				cubeUVRenderTarget,
				pingPongRenderTarget,
				lodIn,
				lodOut,
				sigma,
				'latitudinal',
				poleAxis );

			this._halfBlur(
				pingPongRenderTarget,
				cubeUVRenderTarget,
				lodOut,
				lodOut,
				sigma,
				'longitudinal',
				poleAxis );

		}

		_halfBlur( targetIn, targetOut, lodIn, lodOut, sigmaRadians, direction, poleAxis ) {

			const renderer = this._renderer;
			const blurMaterial = this._blurMaterial;

			if ( direction !== 'latitudinal' && direction !== 'longitudinal' ) {

				console.error(
					'blur direction must be either latitudinal or longitudinal!' );

			}

			// Number of standard deviations at which to cut off the discrete approximation.
			const STANDARD_DEVIATIONS = 3;

			const blurMesh = new Mesh( _lodPlanes[ lodOut ], blurMaterial );
			const blurUniforms = blurMaterial.uniforms;

			const pixels = _sizeLods[ lodIn ] - 1;
			const radiansPerPixel = isFinite( sigmaRadians ) ? Math.PI / ( 2 * pixels ) : 2 * Math.PI / ( 2 * MAX_SAMPLES - 1 );
			const sigmaPixels = sigmaRadians / radiansPerPixel;
			const samples = isFinite( sigmaRadians ) ? 1 + Math.floor( STANDARD_DEVIATIONS * sigmaPixels ) : MAX_SAMPLES;

			if ( samples > MAX_SAMPLES ) {

				console.warn( `sigmaRadians, ${
				sigmaRadians}, is too large and will clip, as it requested ${
				samples} samples when the maximum is set to ${MAX_SAMPLES}` );

			}

			const weights = [];
			let sum = 0;

			for ( let i = 0; i < MAX_SAMPLES; ++ i ) {

				const x = i / sigmaPixels;
				const weight = Math.exp( - x * x / 2 );
				weights.push( weight );

				if ( i == 0 ) {

					sum += weight;

				} else if ( i < samples ) {

					sum += 2 * weight;

				}

			}

			for ( let i = 0; i < weights.length; i ++ ) {

				weights[ i ] = weights[ i ] / sum;

			}

			blurUniforms[ 'envMap' ].value = targetIn.texture;
			blurUniforms[ 'samples' ].value = samples;
			blurUniforms[ 'weights' ].value = weights;
			blurUniforms[ 'latitudinal' ].value = direction === 'latitudinal';

			if ( poleAxis ) {

				blurUniforms[ 'poleAxis' ].value = poleAxis;

			}

			blurUniforms[ 'dTheta' ].value = radiansPerPixel;
			blurUniforms[ 'mipInt' ].value = LOD_MAX - lodIn;
			blurUniforms[ 'inputEncoding' ].value = ENCODINGS[ targetIn.texture.encoding ];
			blurUniforms[ 'outputEncoding' ].value = ENCODINGS[ targetIn.texture.encoding ];

			const outputSize = _sizeLods[ lodOut ];
			const x = 3 * Math.max( 0, SIZE_MAX - 2 * outputSize );
			const y = ( lodOut === 0 ? 0 : 2 * SIZE_MAX ) + 2 * outputSize * ( lodOut > LOD_MAX - LOD_MIN ? lodOut - LOD_MAX + LOD_MIN : 0 );

			_setViewport( targetOut, x, y, 3 * outputSize, 2 * outputSize );
			renderer.setRenderTarget( targetOut );
			renderer.render( blurMesh, _flatCamera );

		}

	}

	function _isLDR( texture ) {

		if ( texture === undefined || texture.type !== UnsignedByteType ) return false;

		return texture.encoding === LinearEncoding || texture.encoding === sRGBEncoding || texture.encoding === GammaEncoding;

	}

	function _createPlanes() {

		const _lodPlanes = [];
		const _sizeLods = [];
		const _sigmas = [];

		let lod = LOD_MAX;

		for ( let i = 0; i < TOTAL_LODS; i ++ ) {

			const sizeLod = Math.pow( 2, lod );
			_sizeLods.push( sizeLod );
			let sigma = 1.0 / sizeLod;

			if ( i > LOD_MAX - LOD_MIN ) {

				sigma = EXTRA_LOD_SIGMA[ i - LOD_MAX + LOD_MIN - 1 ];

			} else if ( i == 0 ) {

				sigma = 0;

			}

			_sigmas.push( sigma );

			const texelSize = 1.0 / ( sizeLod - 1 );
			const min = - texelSize / 2;
			const max = 1 + texelSize / 2;
			const uv1 = [ min, min, max, min, max, max, min, min, max, max, min, max ];

			const cubeFaces = 6;
			const vertices = 6;
			const positionSize = 3;
			const uvSize = 2;
			const faceIndexSize = 1;

			const position = new Float32Array( positionSize * vertices * cubeFaces );
			const uv = new Float32Array( uvSize * vertices * cubeFaces );
			const faceIndex = new Float32Array( faceIndexSize * vertices * cubeFaces );

			for ( let face = 0; face < cubeFaces; face ++ ) {

				const x = ( face % 3 ) * 2 / 3 - 1;
				const y = face > 2 ? 0 : - 1;
				const coordinates = [
					x, y, 0,
					x + 2 / 3, y, 0,
					x + 2 / 3, y + 1, 0,
					x, y, 0,
					x + 2 / 3, y + 1, 0,
					x, y + 1, 0
				];
				position.set( coordinates, positionSize * vertices * face );
				uv.set( uv1, uvSize * vertices * face );
				const fill = [ face, face, face, face, face, face ];
				faceIndex.set( fill, faceIndexSize * vertices * face );

			}

			const planes = new BufferGeometry();
			planes.setAttribute( 'position', new BufferAttribute( position, positionSize ) );
			planes.setAttribute( 'uv', new BufferAttribute( uv, uvSize ) );
			planes.setAttribute( 'faceIndex', new BufferAttribute( faceIndex, faceIndexSize ) );
			_lodPlanes.push( planes );

			if ( lod > LOD_MIN ) {

				lod --;

			}

		}

		return { _lodPlanes, _sizeLods, _sigmas };

	}

	function _createRenderTarget( params ) {

		const cubeUVRenderTarget = new WebGLRenderTarget( 3 * SIZE_MAX, 3 * SIZE_MAX, params );
		cubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping;
		cubeUVRenderTarget.texture.name = 'PMREM.cubeUv';
		cubeUVRenderTarget.scissorTest = true;
		return cubeUVRenderTarget;

	}

	function _setViewport( target, x, y, width, height ) {

		target.viewport.set( x, y, width, height );
		target.scissor.set( x, y, width, height );

	}

	function _getBlurShader( maxSamples ) {

		const weights = new Float32Array( maxSamples );
		const poleAxis = new Vector3( 0, 1, 0 );
		const shaderMaterial = new RawShaderMaterial( {

			name: 'SphericalGaussianBlur',

			defines: { 'n': maxSamples },

			uniforms: {
				'envMap': { value: null },
				'samples': { value: 1 },
				'weights': { value: weights },
				'latitudinal': { value: false },
				'dTheta': { value: 0 },
				'mipInt': { value: 0 },
				'poleAxis': { value: poleAxis },
				'inputEncoding': { value: ENCODINGS[ LinearEncoding ] },
				'outputEncoding': { value: ENCODINGS[ LinearEncoding ] }
			},

			vertexShader: _getCommonVertexShader(),

			fragmentShader: /* glsl */`

			precision mediump float;
			precision mediump int;

			varying vec3 vOutputDirection;

			uniform sampler2D envMap;
			uniform int samples;
			uniform float weights[ n ];
			uniform bool latitudinal;
			uniform float dTheta;
			uniform float mipInt;
			uniform vec3 poleAxis;

			${ _getEncodings() }

			#define ENVMAP_TYPE_CUBE_UV
			#include <cube_uv_reflection_fragment>

			vec3 getSample( float theta, vec3 axis ) {

				float cosTheta = cos( theta );
				// Rodrigues' axis-angle rotation
				vec3 sampleDirection = vOutputDirection * cosTheta
					+ cross( axis, vOutputDirection ) * sin( theta )
					+ axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta );

				return bilinearCubeUV( envMap, sampleDirection, mipInt );

			}

			void main() {

				vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection );

				if ( all( equal( axis, vec3( 0.0 ) ) ) ) {

					axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x );

				}

				axis = normalize( axis );

				gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );
				gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis );

				for ( int i = 1; i < n; i++ ) {

					if ( i >= samples ) {

						break;

					}

					float theta = dTheta * float( i );
					gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis );
					gl_FragColor.rgb += weights[ i ] * getSample( theta, axis );

				}

				gl_FragColor = linearToOutputTexel( gl_FragColor );

			}
		`,

			blending: NoBlending,
			depthTest: false,
			depthWrite: false

		} );

		return shaderMaterial;

	}

	function _getEquirectShader() {

		const texelSize = new Vector2( 1, 1 );
		const shaderMaterial = new RawShaderMaterial( {

			name: 'EquirectangularToCubeUV',

			uniforms: {
				'envMap': { value: null },
				'texelSize': { value: texelSize },
				'inputEncoding': { value: ENCODINGS[ LinearEncoding ] },
				'outputEncoding': { value: ENCODINGS[ LinearEncoding ] }
			},

			vertexShader: _getCommonVertexShader(),

			fragmentShader: /* glsl */`

			precision mediump float;
			precision mediump int;

			varying vec3 vOutputDirection;

			uniform sampler2D envMap;
			uniform vec2 texelSize;

			${ _getEncodings() }

			#include <common>

			void main() {

				gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );

				vec3 outputDirection = normalize( vOutputDirection );
				vec2 uv = equirectUv( outputDirection );

				vec2 f = fract( uv / texelSize - 0.5 );
				uv -= f * texelSize;
				vec3 tl = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;
				uv.x += texelSize.x;
				vec3 tr = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;
				uv.y += texelSize.y;
				vec3 br = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;
				uv.x -= texelSize.x;
				vec3 bl = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;

				vec3 tm = mix( tl, tr, f.x );
				vec3 bm = mix( bl, br, f.x );
				gl_FragColor.rgb = mix( tm, bm, f.y );

				gl_FragColor = linearToOutputTexel( gl_FragColor );

			}
		`,

			blending: NoBlending,
			depthTest: false,
			depthWrite: false

		} );

		return shaderMaterial;

	}

	function _getCubemapShader() {

		const shaderMaterial = new RawShaderMaterial( {

			name: 'CubemapToCubeUV',

			uniforms: {
				'envMap': { value: null },
				'inputEncoding': { value: ENCODINGS[ LinearEncoding ] },
				'outputEncoding': { value: ENCODINGS[ LinearEncoding ] }
			},

			vertexShader: _getCommonVertexShader(),

			fragmentShader: /* glsl */`

			precision mediump float;
			precision mediump int;

			varying vec3 vOutputDirection;

			uniform samplerCube envMap;

			${ _getEncodings() }

			void main() {

				gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );
				gl_FragColor.rgb = envMapTexelToLinear( textureCube( envMap, vec3( - vOutputDirection.x, vOutputDirection.yz ) ) ).rgb;
				gl_FragColor = linearToOutputTexel( gl_FragColor );

			}
		`,

			blending: NoBlending,
			depthTest: false,
			depthWrite: false

		} );

		return shaderMaterial;

	}

	function _getCommonVertexShader() {

		return /* glsl */`

		precision mediump float;
		precision mediump int;

		attribute vec3 position;
		attribute vec2 uv;
		attribute float faceIndex;

		varying vec3 vOutputDirection;

		// RH coordinate system; PMREM face-indexing convention
		vec3 getDirection( vec2 uv, float face ) {

			uv = 2.0 * uv - 1.0;

			vec3 direction = vec3( uv, 1.0 );

			if ( face == 0.0 ) {

				direction = direction.zyx; // ( 1, v, u ) pos x

			} else if ( face == 1.0 ) {

				direction = direction.xzy;
				direction.xz *= -1.0; // ( -u, 1, -v ) pos y

			} else if ( face == 2.0 ) {

				direction.x *= -1.0; // ( -u, v, 1 ) pos z

			} else if ( face == 3.0 ) {

				direction = direction.zyx;
				direction.xz *= -1.0; // ( -1, v, -u ) neg x

			} else if ( face == 4.0 ) {

				direction = direction.xzy;
				direction.xy *= -1.0; // ( -u, -1, v ) neg y

			} else if ( face == 5.0 ) {

				direction.z *= -1.0; // ( u, v, -1 ) neg z

			}

			return direction;

		}

		void main() {

			vOutputDirection = getDirection( uv, faceIndex );
			gl_Position = vec4( position, 1.0 );

		}
	`;

	}

	function _getEncodings() {

		return /* glsl */`

		uniform int inputEncoding;
		uniform int outputEncoding;

		#include <encodings_pars_fragment>

		vec4 inputTexelToLinear( vec4 value ) {

			if ( inputEncoding == 0 ) {

				return value;

			} else if ( inputEncoding == 1 ) {

				return sRGBToLinear( value );

			} else if ( inputEncoding == 2 ) {

				return RGBEToLinear( value );

			} else if ( inputEncoding == 3 ) {

				return RGBMToLinear( value, 7.0 );

			} else if ( inputEncoding == 4 ) {

				return RGBMToLinear( value, 16.0 );

			} else if ( inputEncoding == 5 ) {

				return RGBDToLinear( value, 256.0 );

			} else {

				return GammaToLinear( value, 2.2 );

			}

		}

		vec4 linearToOutputTexel( vec4 value ) {

			if ( outputEncoding == 0 ) {

				return value;

			} else if ( outputEncoding == 1 ) {

				return LinearTosRGB( value );

			} else if ( outputEncoding == 2 ) {

				return LinearToRGBE( value );

			} else if ( outputEncoding == 3 ) {

				return LinearToRGBM( value, 7.0 );

			} else if ( outputEncoding == 4 ) {

				return LinearToRGBM( value, 16.0 );

			} else if ( outputEncoding == 5 ) {

				return LinearToRGBD( value, 256.0 );

			} else {

				return LinearToGamma( value, 2.2 );

			}

		}

		vec4 envMapTexelToLinear( vec4 color ) {

			return inputTexelToLinear( color );

		}
	`;

	}

	function Face4( a, b, c, d, normal, color, materialIndex ) {

		console.warn( 'THREE.Face4 has been removed. A THREE.Face3 will be created instead.' );
		return new Face3( a, b, c, normal, color, materialIndex );

	}

	const LineStrip = 0;
	const LinePieces = 1;
	const NoColors = 0;
	const FaceColors = 1;
	const VertexColors = 2;

	function MeshFaceMaterial( materials ) {

		console.warn( 'THREE.MeshFaceMaterial has been removed. Use an Array instead.' );
		return materials;

	}

	function MultiMaterial( materials = [] ) {

		console.warn( 'THREE.MultiMaterial has been removed. Use an Array instead.' );
		materials.isMultiMaterial = true;
		materials.materials = materials;
		materials.clone = function () {

			return materials.slice();

		};

		return materials;

	}

	function PointCloud( geometry, material ) {

		console.warn( 'THREE.PointCloud has been renamed to THREE.Points.' );
		return new Points( geometry, material );

	}

	function Particle( material ) {

		console.warn( 'THREE.Particle has been renamed to THREE.Sprite.' );
		return new Sprite( material );

	}

	function ParticleSystem( geometry, material ) {

		console.warn( 'THREE.ParticleSystem has been renamed to THREE.Points.' );
		return new Points( geometry, material );

	}

	function PointCloudMaterial( parameters ) {

		console.warn( 'THREE.PointCloudMaterial has been renamed to THREE.PointsMaterial.' );
		return new PointsMaterial( parameters );

	}

	function ParticleBasicMaterial( parameters ) {

		console.warn( 'THREE.ParticleBasicMaterial has been renamed to THREE.PointsMaterial.' );
		return new PointsMaterial( parameters );

	}

	function ParticleSystemMaterial( parameters ) {

		console.warn( 'THREE.ParticleSystemMaterial has been renamed to THREE.PointsMaterial.' );
		return new PointsMaterial( parameters );

	}

	function Vertex( x, y, z ) {

		console.warn( 'THREE.Vertex has been removed. Use THREE.Vector3 instead.' );
		return new Vector3( x, y, z );

	}

	//

	function DynamicBufferAttribute( array, itemSize ) {

		console.warn( 'THREE.DynamicBufferAttribute has been removed. Use new THREE.BufferAttribute().setUsage( THREE.DynamicDrawUsage ) instead.' );
		return new BufferAttribute( array, itemSize ).setUsage( DynamicDrawUsage );

	}

	function Int8Attribute( array, itemSize ) {

		console.warn( 'THREE.Int8Attribute has been removed. Use new THREE.Int8BufferAttribute() instead.' );
		return new Int8BufferAttribute( array, itemSize );

	}

	function Uint8Attribute( array, itemSize ) {

		console.warn( 'THREE.Uint8Attribute has been removed. Use new THREE.Uint8BufferAttribute() instead.' );
		return new Uint8BufferAttribute( array, itemSize );

	}

	function Uint8ClampedAttribute( array, itemSize ) {

		console.warn( 'THREE.Uint8ClampedAttribute has been removed. Use new THREE.Uint8ClampedBufferAttribute() instead.' );
		return new Uint8ClampedBufferAttribute( array, itemSize );

	}

	function Int16Attribute( array, itemSize ) {

		console.warn( 'THREE.Int16Attribute has been removed. Use new THREE.Int16BufferAttribute() instead.' );
		return new Int16BufferAttribute( array, itemSize );

	}

	function Uint16Attribute( array, itemSize ) {

		console.warn( 'THREE.Uint16Attribute has been removed. Use new THREE.Uint16BufferAttribute() instead.' );
		return new Uint16BufferAttribute( array, itemSize );

	}

	function Int32Attribute( array, itemSize ) {

		console.warn( 'THREE.Int32Attribute has been removed. Use new THREE.Int32BufferAttribute() instead.' );
		return new Int32BufferAttribute( array, itemSize );

	}

	function Uint32Attribute( array, itemSize ) {

		console.warn( 'THREE.Uint32Attribute has been removed. Use new THREE.Uint32BufferAttribute() instead.' );
		return new Uint32BufferAttribute( array, itemSize );

	}

	function Float32Attribute( array, itemSize ) {

		console.warn( 'THREE.Float32Attribute has been removed. Use new THREE.Float32BufferAttribute() instead.' );
		return new Float32BufferAttribute( array, itemSize );

	}

	function Float64Attribute( array, itemSize ) {

		console.warn( 'THREE.Float64Attribute has been removed. Use new THREE.Float64BufferAttribute() instead.' );
		return new Float64BufferAttribute( array, itemSize );

	}

	//

	Curve.create = function ( construct, getPoint ) {

		console.log( 'THREE.Curve.create() has been deprecated' );

		construct.prototype = Object.create( Curve.prototype );
		construct.prototype.constructor = construct;
		construct.prototype.getPoint = getPoint;

		return construct;

	};

	//

	Object.assign( CurvePath.prototype, {

		createPointsGeometry: function ( divisions ) {

			console.warn( 'THREE.CurvePath: .createPointsGeometry() has been removed. Use new THREE.Geometry().setFromPoints( points ) instead.' );

			// generate geometry from path points (for Line or Points objects)

			const pts = this.getPoints( divisions );
			return this.createGeometry( pts );

		},

		createSpacedPointsGeometry: function ( divisions ) {

			console.warn( 'THREE.CurvePath: .createSpacedPointsGeometry() has been removed. Use new THREE.Geometry().setFromPoints( points ) instead.' );

			// generate geometry from equidistant sampling along the path

			const pts = this.getSpacedPoints( divisions );
			return this.createGeometry( pts );

		},

		createGeometry: function ( points ) {

			console.warn( 'THREE.CurvePath: .createGeometry() has been removed. Use new THREE.Geometry().setFromPoints( points ) instead.' );

			const geometry = new Geometry();

			for ( let i = 0, l = points.length; i < l; i ++ ) {

				const point = points[ i ];
				geometry.vertices.push( new Vector3( point.x, point.y, point.z || 0 ) );

			}

			return geometry;

		}

	} );

	//

	Object.assign( Path.prototype, {

		fromPoints: function ( points ) {

			console.warn( 'THREE.Path: .fromPoints() has been renamed to .setFromPoints().' );
			return this.setFromPoints( points );

		}

	} );

	//

	function ClosedSplineCurve3( points ) {

		console.warn( 'THREE.ClosedSplineCurve3 has been deprecated. Use THREE.CatmullRomCurve3 instead.' );

		CatmullRomCurve3.call( this, points );
		this.type = 'catmullrom';
		this.closed = true;

	}

	ClosedSplineCurve3.prototype = Object.create( CatmullRomCurve3.prototype );

	//

	function SplineCurve3( points ) {

		console.warn( 'THREE.SplineCurve3 has been deprecated. Use THREE.CatmullRomCurve3 instead.' );

		CatmullRomCurve3.call( this, points );
		this.type = 'catmullrom';

	}

	SplineCurve3.prototype = Object.create( CatmullRomCurve3.prototype );

	//

	function Spline( points ) {

		console.warn( 'THREE.Spline has been removed. Use THREE.CatmullRomCurve3 instead.' );

		CatmullRomCurve3.call( this, points );
		this.type = 'catmullrom';

	}

	Spline.prototype = Object.create( CatmullRomCurve3.prototype );

	Object.assign( Spline.prototype, {

		initFromArray: function ( /* a */ ) {

			console.error( 'THREE.Spline: .initFromArray() has been removed.' );

		},
		getControlPointsArray: function ( /* optionalTarget */ ) {

			console.error( 'THREE.Spline: .getControlPointsArray() has been removed.' );

		},
		reparametrizeByArcLength: function ( /* samplingCoef */ ) {

			console.error( 'THREE.Spline: .reparametrizeByArcLength() has been removed.' );

		}

	} );

	//

	function AxisHelper( size ) {

		console.warn( 'THREE.AxisHelper has been renamed to THREE.AxesHelper.' );
		return new AxesHelper( size );

	}

	function BoundingBoxHelper( object, color ) {

		console.warn( 'THREE.BoundingBoxHelper has been deprecated. Creating a THREE.BoxHelper instead.' );
		return new BoxHelper( object, color );

	}

	function EdgesHelper( object, hex ) {

		console.warn( 'THREE.EdgesHelper has been removed. Use THREE.EdgesGeometry instead.' );
		return new LineSegments( new EdgesGeometry( object.geometry ), new LineBasicMaterial( { color: hex !== undefined ? hex : 0xffffff } ) );

	}

	GridHelper.prototype.setColors = function () {

		console.error( 'THREE.GridHelper: setColors() has been deprecated, pass them in the constructor instead.' );

	};

	SkeletonHelper.prototype.update = function () {

		console.error( 'THREE.SkeletonHelper: update() no longer needs to be called.' );

	};

	function WireframeHelper( object, hex ) {

		console.warn( 'THREE.WireframeHelper has been removed. Use THREE.WireframeGeometry instead.' );
		return new LineSegments( new WireframeGeometry( object.geometry ), new LineBasicMaterial( { color: hex !== undefined ? hex : 0xffffff } ) );

	}

	//

	Object.assign( Loader.prototype, {

		extractUrlBase: function ( url ) {

			console.warn( 'THREE.Loader: .extractUrlBase() has been deprecated. Use THREE.LoaderUtils.extractUrlBase() instead.' );
			return LoaderUtils.extractUrlBase( url );

		}

	} );

	Loader.Handlers = {

		add: function ( /* regex, loader */ ) {

			console.error( 'THREE.Loader: Handlers.add() has been removed. Use LoadingManager.addHandler() instead.' );

		},

		get: function ( /* file */ ) {

			console.error( 'THREE.Loader: Handlers.get() has been removed. Use LoadingManager.getHandler() instead.' );

		}

	};

	function XHRLoader( manager ) {

		console.warn( 'THREE.XHRLoader has been renamed to THREE.FileLoader.' );
		return new FileLoader( manager );

	}

	function BinaryTextureLoader( manager ) {

		console.warn( 'THREE.BinaryTextureLoader has been renamed to THREE.DataTextureLoader.' );
		return new DataTextureLoader( manager );

	}

	//

	Object.assign( Box2.prototype, {

		center: function ( optionalTarget ) {

			console.warn( 'THREE.Box2: .center() has been renamed to .getCenter().' );
			return this.getCenter( optionalTarget );

		},
		empty: function () {

			console.warn( 'THREE.Box2: .empty() has been renamed to .isEmpty().' );
			return this.isEmpty();

		},
		isIntersectionBox: function ( box ) {

			console.warn( 'THREE.Box2: .isIntersectionBox() has been renamed to .intersectsBox().' );
			return this.intersectsBox( box );

		},
		size: function ( optionalTarget ) {

			console.warn( 'THREE.Box2: .size() has been renamed to .getSize().' );
			return this.getSize( optionalTarget );

		}
	} );

	Object.assign( Box3.prototype, {

		center: function ( optionalTarget ) {

			console.warn( 'THREE.Box3: .center() has been renamed to .getCenter().' );
			return this.getCenter( optionalTarget );

		},
		empty: function () {

			console.warn( 'THREE.Box3: .empty() has been renamed to .isEmpty().' );
			return this.isEmpty();

		},
		isIntersectionBox: function ( box ) {

			console.warn( 'THREE.Box3: .isIntersectionBox() has been renamed to .intersectsBox().' );
			return this.intersectsBox( box );

		},
		isIntersectionSphere: function ( sphere ) {

			console.warn( 'THREE.Box3: .isIntersectionSphere() has been renamed to .intersectsSphere().' );
			return this.intersectsSphere( sphere );

		},
		size: function ( optionalTarget ) {

			console.warn( 'THREE.Box3: .size() has been renamed to .getSize().' );
			return this.getSize( optionalTarget );

		}
	} );

	Object.assign( Sphere.prototype, {

		empty: function () {

			console.warn( 'THREE.Sphere: .empty() has been renamed to .isEmpty().' );
			return this.isEmpty();

		},

	} );

	Frustum.prototype.setFromMatrix = function ( m ) {

		console.warn( 'THREE.Frustum: .setFromMatrix() has been renamed to .setFromProjectionMatrix().' );
		return this.setFromProjectionMatrix( m );

	};

	Line3.prototype.center = function ( optionalTarget ) {

		console.warn( 'THREE.Line3: .center() has been renamed to .getCenter().' );
		return this.getCenter( optionalTarget );

	};

	Object.assign( MathUtils, {

		random16: function () {

			console.warn( 'THREE.Math: .random16() has been deprecated. Use Math.random() instead.' );
			return Math.random();

		},

		nearestPowerOfTwo: function ( value ) {

			console.warn( 'THREE.Math: .nearestPowerOfTwo() has been renamed to .floorPowerOfTwo().' );
			return MathUtils.floorPowerOfTwo( value );

		},

		nextPowerOfTwo: function ( value ) {

			console.warn( 'THREE.Math: .nextPowerOfTwo() has been renamed to .ceilPowerOfTwo().' );
			return MathUtils.ceilPowerOfTwo( value );

		}

	} );

	Object.assign( Matrix3.prototype, {

		flattenToArrayOffset: function ( array, offset ) {

			console.warn( 'THREE.Matrix3: .flattenToArrayOffset() has been deprecated. Use .toArray() instead.' );
			return this.toArray( array, offset );

		},
		multiplyVector3: function ( vector ) {

			console.warn( 'THREE.Matrix3: .multiplyVector3() has been removed. Use vector.applyMatrix3( matrix ) instead.' );
			return vector.applyMatrix3( this );

		},
		multiplyVector3Array: function ( /* a */ ) {

			console.error( 'THREE.Matrix3: .multiplyVector3Array() has been removed.' );

		},
		applyToBufferAttribute: function ( attribute ) {

			console.warn( 'THREE.Matrix3: .applyToBufferAttribute() has been removed. Use attribute.applyMatrix3( matrix ) instead.' );
			return attribute.applyMatrix3( this );

		},
		applyToVector3Array: function ( /* array, offset, length */ ) {

			console.error( 'THREE.Matrix3: .applyToVector3Array() has been removed.' );

		},
		getInverse: function ( matrix ) {

			console.warn( 'THREE.Matrix3: .getInverse() has been removed. Use matrixInv.copy( matrix ).invert(); instead.' );
			return this.copy( matrix ).invert();

		}

	} );

	Object.assign( Matrix4.prototype, {

		extractPosition: function ( m ) {

			console.warn( 'THREE.Matrix4: .extractPosition() has been renamed to .copyPosition().' );
			return this.copyPosition( m );

		},
		flattenToArrayOffset: function ( array, offset ) {

			console.warn( 'THREE.Matrix4: .flattenToArrayOffset() has been deprecated. Use .toArray() instead.' );
			return this.toArray( array, offset );

		},
		getPosition: function () {

			console.warn( 'THREE.Matrix4: .getPosition() has been removed. Use Vector3.setFromMatrixPosition( matrix ) instead.' );
			return new Vector3().setFromMatrixColumn( this, 3 );

		},
		setRotationFromQuaternion: function ( q ) {

			console.warn( 'THREE.Matrix4: .setRotationFromQuaternion() has been renamed to .makeRotationFromQuaternion().' );
			return this.makeRotationFromQuaternion( q );

		},
		multiplyToArray: function () {

			console.warn( 'THREE.Matrix4: .multiplyToArray() has been removed.' );

		},
		multiplyVector3: function ( vector ) {

			console.warn( 'THREE.Matrix4: .multiplyVector3() has been removed. Use vector.applyMatrix4( matrix ) instead.' );
			return vector.applyMatrix4( this );

		},
		multiplyVector4: function ( vector ) {

			console.warn( 'THREE.Matrix4: .multiplyVector4() has been removed. Use vector.applyMatrix4( matrix ) instead.' );
			return vector.applyMatrix4( this );

		},
		multiplyVector3Array: function ( /* a */ ) {

			console.error( 'THREE.Matrix4: .multiplyVector3Array() has been removed.' );

		},
		rotateAxis: function ( v ) {

			console.warn( 'THREE.Matrix4: .rotateAxis() has been removed. Use Vector3.transformDirection( matrix ) instead.' );
			v.transformDirection( this );

		},
		crossVector: function ( vector ) {

			console.warn( 'THREE.Matrix4: .crossVector() has been removed. Use vector.applyMatrix4( matrix ) instead.' );
			return vector.applyMatrix4( this );

		},
		translate: function () {

			console.error( 'THREE.Matrix4: .translate() has been removed.' );

		},
		rotateX: function () {

			console.error( 'THREE.Matrix4: .rotateX() has been removed.' );

		},
		rotateY: function () {

			console.error( 'THREE.Matrix4: .rotateY() has been removed.' );

		},
		rotateZ: function () {

			console.error( 'THREE.Matrix4: .rotateZ() has been removed.' );

		},
		rotateByAxis: function () {

			console.error( 'THREE.Matrix4: .rotateByAxis() has been removed.' );

		},
		applyToBufferAttribute: function ( attribute ) {

			console.warn( 'THREE.Matrix4: .applyToBufferAttribute() has been removed. Use attribute.applyMatrix4( matrix ) instead.' );
			return attribute.applyMatrix4( this );

		},
		applyToVector3Array: function ( /* array, offset, length */ ) {

			console.error( 'THREE.Matrix4: .applyToVector3Array() has been removed.' );

		},
		makeFrustum: function ( left, right, bottom, top, near, far ) {

			console.warn( 'THREE.Matrix4: .makeFrustum() has been removed. Use .makePerspective( left, right, top, bottom, near, far ) instead.' );
			return this.makePerspective( left, right, top, bottom, near, far );

		},
		getInverse: function ( matrix ) {

			console.warn( 'THREE.Matrix4: .getInverse() has been removed. Use matrixInv.copy( matrix ).invert(); instead.' );
			return this.copy( matrix ).invert();

		}

	} );

	Plane.prototype.isIntersectionLine = function ( line ) {

		console.warn( 'THREE.Plane: .isIntersectionLine() has been renamed to .intersectsLine().' );
		return this.intersectsLine( line );

	};

	Object.assign( Quaternion.prototype, {

		multiplyVector3: function ( vector ) {

			console.warn( 'THREE.Quaternion: .multiplyVector3() has been removed. Use is now vector.applyQuaternion( quaternion ) instead.' );
			return vector.applyQuaternion( this );

		},
		inverse: function ( ) {

			console.warn( 'THREE.Quaternion: .inverse() has been renamed to invert().' );
			return this.invert();

		}

	} );

	Object.assign( Ray.prototype, {

		isIntersectionBox: function ( box ) {

			console.warn( 'THREE.Ray: .isIntersectionBox() has been renamed to .intersectsBox().' );
			return this.intersectsBox( box );

		},
		isIntersectionPlane: function ( plane ) {

			console.warn( 'THREE.Ray: .isIntersectionPlane() has been renamed to .intersectsPlane().' );
			return this.intersectsPlane( plane );

		},
		isIntersectionSphere: function ( sphere ) {

			console.warn( 'THREE.Ray: .isIntersectionSphere() has been renamed to .intersectsSphere().' );
			return this.intersectsSphere( sphere );

		}

	} );

	Object.assign( Triangle.prototype, {

		area: function () {

			console.warn( 'THREE.Triangle: .area() has been renamed to .getArea().' );
			return this.getArea();

		},
		barycoordFromPoint: function ( point, target ) {

			console.warn( 'THREE.Triangle: .barycoordFromPoint() has been renamed to .getBarycoord().' );
			return this.getBarycoord( point, target );

		},
		midpoint: function ( target ) {

			console.warn( 'THREE.Triangle: .midpoint() has been renamed to .getMidpoint().' );
			return this.getMidpoint( target );

		},
		normal: function ( target ) {

			console.warn( 'THREE.Triangle: .normal() has been renamed to .getNormal().' );
			return this.getNormal( target );

		},
		plane: function ( target ) {

			console.warn( 'THREE.Triangle: .plane() has been renamed to .getPlane().' );
			return this.getPlane( target );

		}

	} );

	Object.assign( Triangle, {

		barycoordFromPoint: function ( point, a, b, c, target ) {

			console.warn( 'THREE.Triangle: .barycoordFromPoint() has been renamed to .getBarycoord().' );
			return Triangle.getBarycoord( point, a, b, c, target );

		},
		normal: function ( a, b, c, target ) {

			console.warn( 'THREE.Triangle: .normal() has been renamed to .getNormal().' );
			return Triangle.getNormal( a, b, c, target );

		}

	} );

	Object.assign( Shape.prototype, {

		extractAllPoints: function ( divisions ) {

			console.warn( 'THREE.Shape: .extractAllPoints() has been removed. Use .extractPoints() instead.' );
			return this.extractPoints( divisions );

		},
		extrude: function ( options ) {

			console.warn( 'THREE.Shape: .extrude() has been removed. Use ExtrudeGeometry() instead.' );
			return new ExtrudeGeometry( this, options );

		},
		makeGeometry: function ( options ) {

			console.warn( 'THREE.Shape: .makeGeometry() has been removed. Use ShapeGeometry() instead.' );
			return new ShapeGeometry( this, options );

		}

	} );

	Object.assign( Vector2.prototype, {

		fromAttribute: function ( attribute, index, offset ) {

			console.warn( 'THREE.Vector2: .fromAttribute() has been renamed to .fromBufferAttribute().' );
			return this.fromBufferAttribute( attribute, index, offset );

		},
		distanceToManhattan: function ( v ) {

			console.warn( 'THREE.Vector2: .distanceToManhattan() has been renamed to .manhattanDistanceTo().' );
			return this.manhattanDistanceTo( v );

		},
		lengthManhattan: function () {

			console.warn( 'THREE.Vector2: .lengthManhattan() has been renamed to .manhattanLength().' );
			return this.manhattanLength();

		}

	} );

	Object.assign( Vector3.prototype, {

		setEulerFromRotationMatrix: function () {

			console.error( 'THREE.Vector3: .setEulerFromRotationMatrix() has been removed. Use Euler.setFromRotationMatrix() instead.' );

		},
		setEulerFromQuaternion: function () {

			console.error( 'THREE.Vector3: .setEulerFromQuaternion() has been removed. Use Euler.setFromQuaternion() instead.' );

		},
		getPositionFromMatrix: function ( m ) {

			console.warn( 'THREE.Vector3: .getPositionFromMatrix() has been renamed to .setFromMatrixPosition().' );
			return this.setFromMatrixPosition( m );

		},
		getScaleFromMatrix: function ( m ) {

			console.warn( 'THREE.Vector3: .getScaleFromMatrix() has been renamed to .setFromMatrixScale().' );
			return this.setFromMatrixScale( m );

		},
		getColumnFromMatrix: function ( index, matrix ) {

			console.warn( 'THREE.Vector3: .getColumnFromMatrix() has been renamed to .setFromMatrixColumn().' );
			return this.setFromMatrixColumn( matrix, index );

		},
		applyProjection: function ( m ) {

			console.warn( 'THREE.Vector3: .applyProjection() has been removed. Use .applyMatrix4( m ) instead.' );
			return this.applyMatrix4( m );

		},
		fromAttribute: function ( attribute, index, offset ) {

			console.warn( 'THREE.Vector3: .fromAttribute() has been renamed to .fromBufferAttribute().' );
			return this.fromBufferAttribute( attribute, index, offset );

		},
		distanceToManhattan: function ( v ) {

			console.warn( 'THREE.Vector3: .distanceToManhattan() has been renamed to .manhattanDistanceTo().' );
			return this.manhattanDistanceTo( v );

		},
		lengthManhattan: function () {

			console.warn( 'THREE.Vector3: .lengthManhattan() has been renamed to .manhattanLength().' );
			return this.manhattanLength();

		}

	} );

	Object.assign( Vector4.prototype, {

		fromAttribute: function ( attribute, index, offset ) {

			console.warn( 'THREE.Vector4: .fromAttribute() has been renamed to .fromBufferAttribute().' );
			return this.fromBufferAttribute( attribute, index, offset );

		},
		lengthManhattan: function () {

			console.warn( 'THREE.Vector4: .lengthManhattan() has been renamed to .manhattanLength().' );
			return this.manhattanLength();

		}

	} );

	//

	Object.assign( Geometry.prototype, {

		computeTangents: function () {

			console.error( 'THREE.Geometry: .computeTangents() has been removed.' );

		},
		computeLineDistances: function () {

			console.error( 'THREE.Geometry: .computeLineDistances() has been removed. Use THREE.Line.computeLineDistances() instead.' );

		},
		applyMatrix: function ( matrix ) {

			console.warn( 'THREE.Geometry: .applyMatrix() has been renamed to .applyMatrix4().' );
			return this.applyMatrix4( matrix );

		}

	} );

	Object.assign( Object3D.prototype, {

		getChildByName: function ( name ) {

			console.warn( 'THREE.Object3D: .getChildByName() has been renamed to .getObjectByName().' );
			return this.getObjectByName( name );

		},
		renderDepth: function () {

			console.warn( 'THREE.Object3D: .renderDepth has been removed. Use .renderOrder, instead.' );

		},
		translate: function ( distance, axis ) {

			console.warn( 'THREE.Object3D: .translate() has been removed. Use .translateOnAxis( axis, distance ) instead.' );
			return this.translateOnAxis( axis, distance );

		},
		getWorldRotation: function () {

			console.error( 'THREE.Object3D: .getWorldRotation() has been removed. Use THREE.Object3D.getWorldQuaternion( target ) instead.' );

		},
		applyMatrix: function ( matrix ) {

			console.warn( 'THREE.Object3D: .applyMatrix() has been renamed to .applyMatrix4().' );
			return this.applyMatrix4( matrix );

		}

	} );

	Object.defineProperties( Object3D.prototype, {

		eulerOrder: {
			get: function () {

				console.warn( 'THREE.Object3D: .eulerOrder is now .rotation.order.' );
				return this.rotation.order;

			},
			set: function ( value ) {

				console.warn( 'THREE.Object3D: .eulerOrder is now .rotation.order.' );
				this.rotation.order = value;

			}
		},
		useQuaternion: {
			get: function () {

				console.warn( 'THREE.Object3D: .useQuaternion has been removed. The library now uses quaternions by default.' );

			},
			set: function () {

				console.warn( 'THREE.Object3D: .useQuaternion has been removed. The library now uses quaternions by default.' );

			}
		}

	} );

	Object.assign( Mesh.prototype, {

		setDrawMode: function () {

			console.error( 'THREE.Mesh: .setDrawMode() has been removed. The renderer now always assumes THREE.TrianglesDrawMode. Transform your geometry via BufferGeometryUtils.toTrianglesDrawMode() if necessary.' );

		},

	} );

	Object.defineProperties( Mesh.prototype, {

		drawMode: {
			get: function () {

				console.error( 'THREE.Mesh: .drawMode has been removed. The renderer now always assumes THREE.TrianglesDrawMode.' );
				return TrianglesDrawMode;

			},
			set: function () {

				console.error( 'THREE.Mesh: .drawMode has been removed. The renderer now always assumes THREE.TrianglesDrawMode. Transform your geometry via BufferGeometryUtils.toTrianglesDrawMode() if necessary.' );

			}
		}

	} );

	Object.defineProperties( LOD.prototype, {

		objects: {
			get: function () {

				console.warn( 'THREE.LOD: .objects has been renamed to .levels.' );
				return this.levels;

			}
		}

	} );

	Object.defineProperty( Skeleton.prototype, 'useVertexTexture', {

		get: function () {

			console.warn( 'THREE.Skeleton: useVertexTexture has been removed.' );

		},
		set: function () {

			console.warn( 'THREE.Skeleton: useVertexTexture has been removed.' );

		}

	} );

	SkinnedMesh.prototype.initBones = function () {

		console.error( 'THREE.SkinnedMesh: initBones() has been removed.' );

	};

	Object.defineProperty( Curve.prototype, '__arcLengthDivisions', {

		get: function () {

			console.warn( 'THREE.Curve: .__arcLengthDivisions is now .arcLengthDivisions.' );
			return this.arcLengthDivisions;

		},
		set: function ( value ) {

			console.warn( 'THREE.Curve: .__arcLengthDivisions is now .arcLengthDivisions.' );
			this.arcLengthDivisions = value;

		}

	} );

	//

	PerspectiveCamera.prototype.setLens = function ( focalLength, filmGauge ) {

		console.warn( 'THREE.PerspectiveCamera.setLens is deprecated. ' +
				'Use .setFocalLength and .filmGauge for a photographic setup.' );

		if ( filmGauge !== undefined ) this.filmGauge = filmGauge;
		this.setFocalLength( focalLength );

	};

	//

	Object.defineProperties( Light.prototype, {
		onlyShadow: {
			set: function () {

				console.warn( 'THREE.Light: .onlyShadow has been removed.' );

			}
		},
		shadowCameraFov: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowCameraFov is now .shadow.camera.fov.' );
				this.shadow.camera.fov = value;

			}
		},
		shadowCameraLeft: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowCameraLeft is now .shadow.camera.left.' );
				this.shadow.camera.left = value;

			}
		},
		shadowCameraRight: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowCameraRight is now .shadow.camera.right.' );
				this.shadow.camera.right = value;

			}
		},
		shadowCameraTop: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowCameraTop is now .shadow.camera.top.' );
				this.shadow.camera.top = value;

			}
		},
		shadowCameraBottom: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowCameraBottom is now .shadow.camera.bottom.' );
				this.shadow.camera.bottom = value;

			}
		},
		shadowCameraNear: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowCameraNear is now .shadow.camera.near.' );
				this.shadow.camera.near = value;

			}
		},
		shadowCameraFar: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowCameraFar is now .shadow.camera.far.' );
				this.shadow.camera.far = value;

			}
		},
		shadowCameraVisible: {
			set: function () {

				console.warn( 'THREE.Light: .shadowCameraVisible has been removed. Use new THREE.CameraHelper( light.shadow.camera ) instead.' );

			}
		},
		shadowBias: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowBias is now .shadow.bias.' );
				this.shadow.bias = value;

			}
		},
		shadowDarkness: {
			set: function () {

				console.warn( 'THREE.Light: .shadowDarkness has been removed.' );

			}
		},
		shadowMapWidth: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowMapWidth is now .shadow.mapSize.width.' );
				this.shadow.mapSize.width = value;

			}
		},
		shadowMapHeight: {
			set: function ( value ) {

				console.warn( 'THREE.Light: .shadowMapHeight is now .shadow.mapSize.height.' );
				this.shadow.mapSize.height = value;

			}
		}
	} );

	//

	Object.defineProperties( BufferAttribute.prototype, {

		length: {
			get: function () {

				console.warn( 'THREE.BufferAttribute: .length has been deprecated. Use .count instead.' );
				return this.array.length;

			}
		},
		dynamic: {
			get: function () {

				console.warn( 'THREE.BufferAttribute: .dynamic has been deprecated. Use .usage instead.' );
				return this.usage === DynamicDrawUsage;

			},
			set: function ( /* value */ ) {

				console.warn( 'THREE.BufferAttribute: .dynamic has been deprecated. Use .usage instead.' );
				this.setUsage( DynamicDrawUsage );

			}
		}

	} );

	Object.assign( BufferAttribute.prototype, {
		setDynamic: function ( value ) {

			console.warn( 'THREE.BufferAttribute: .setDynamic() has been deprecated. Use .setUsage() instead.' );
			this.setUsage( value === true ? DynamicDrawUsage : StaticDrawUsage );
			return this;

		},
		copyIndicesArray: function ( /* indices */ ) {

			console.error( 'THREE.BufferAttribute: .copyIndicesArray() has been removed.' );

		},
		setArray: function ( /* array */ ) {

			console.error( 'THREE.BufferAttribute: .setArray has been removed. Use BufferGeometry .setAttribute to replace/resize attribute buffers' );

		}
	} );

	Object.assign( BufferGeometry.prototype, {

		addIndex: function ( index ) {

			console.warn( 'THREE.BufferGeometry: .addIndex() has been renamed to .setIndex().' );
			this.setIndex( index );

		},
		addAttribute: function ( name, attribute ) {

			console.warn( 'THREE.BufferGeometry: .addAttribute() has been renamed to .setAttribute().' );

			if ( ! ( attribute && attribute.isBufferAttribute ) && ! ( attribute && attribute.isInterleavedBufferAttribute ) ) {

				console.warn( 'THREE.BufferGeometry: .addAttribute() now expects ( name, attribute ).' );

				return this.setAttribute( name, new BufferAttribute( arguments[ 1 ], arguments[ 2 ] ) );

			}

			if ( name === 'index' ) {

				console.warn( 'THREE.BufferGeometry.addAttribute: Use .setIndex() for index attribute.' );
				this.setIndex( attribute );

				return this;

			}

			return this.setAttribute( name, attribute );

		},
		addDrawCall: function ( start, count, indexOffset ) {

			if ( indexOffset !== undefined ) {

				console.warn( 'THREE.BufferGeometry: .addDrawCall() no longer supports indexOffset.' );

			}

			console.warn( 'THREE.BufferGeometry: .addDrawCall() is now .addGroup().' );
			this.addGroup( start, count );

		},
		clearDrawCalls: function () {

			console.warn( 'THREE.BufferGeometry: .clearDrawCalls() is now .clearGroups().' );
			this.clearGroups();

		},
		computeTangents: function () {

			console.warn( 'THREE.BufferGeometry: .computeTangents() has been removed.' );

		},
		computeOffsets: function () {

			console.warn( 'THREE.BufferGeometry: .computeOffsets() has been removed.' );

		},
		removeAttribute: function ( name ) {

			console.warn( 'THREE.BufferGeometry: .removeAttribute() has been renamed to .deleteAttribute().' );

			return this.deleteAttribute( name );

		},
		applyMatrix: function ( matrix ) {

			console.warn( 'THREE.BufferGeometry: .applyMatrix() has been renamed to .applyMatrix4().' );
			return this.applyMatrix4( matrix );

		}

	} );

	Object.defineProperties( BufferGeometry.prototype, {

		drawcalls: {
			get: function () {

				console.error( 'THREE.BufferGeometry: .drawcalls has been renamed to .groups.' );
				return this.groups;

			}
		},
		offsets: {
			get: function () {

				console.warn( 'THREE.BufferGeometry: .offsets has been renamed to .groups.' );
				return this.groups;

			}
		}

	} );

	Object.defineProperties( InstancedBufferGeometry.prototype, {

		maxInstancedCount: {
			get: function () {

				console.warn( 'THREE.InstancedBufferGeometry: .maxInstancedCount has been renamed to .instanceCount.' );
				return this.instanceCount;

			},
			set: function ( value ) {

				console.warn( 'THREE.InstancedBufferGeometry: .maxInstancedCount has been renamed to .instanceCount.' );
				this.instanceCount = value;

			}
		}

	} );

	Object.defineProperties( Raycaster.prototype, {

		linePrecision: {
			get: function () {

				console.warn( 'THREE.Raycaster: .linePrecision has been deprecated. Use .params.Line.threshold instead.' );
				return this.params.Line.threshold;

			},
			set: function ( value ) {

				console.warn( 'THREE.Raycaster: .linePrecision has been deprecated. Use .params.Line.threshold instead.' );
				this.params.Line.threshold = value;

			}
		}

	} );

	Object.defineProperties( InterleavedBuffer.prototype, {

		dynamic: {
			get: function () {

				console.warn( 'THREE.InterleavedBuffer: .length has been deprecated. Use .usage instead.' );
				return this.usage === DynamicDrawUsage;

			},
			set: function ( value ) {

				console.warn( 'THREE.InterleavedBuffer: .length has been deprecated. Use .usage instead.' );
				this.setUsage( value );

			}
		}

	} );

	Object.assign( InterleavedBuffer.prototype, {
		setDynamic: function ( value ) {

			console.warn( 'THREE.InterleavedBuffer: .setDynamic() has been deprecated. Use .setUsage() instead.' );
			this.setUsage( value === true ? DynamicDrawUsage : StaticDrawUsage );
			return this;

		},
		setArray: function ( /* array */ ) {

			console.error( 'THREE.InterleavedBuffer: .setArray has been removed. Use BufferGeometry .setAttribute to replace/resize attribute buffers' );

		}
	} );

	//

	Object.assign( ExtrudeBufferGeometry.prototype, {

		getArrays: function () {

			console.error( 'THREE.ExtrudeBufferGeometry: .getArrays() has been removed.' );

		},

		addShapeList: function () {

			console.error( 'THREE.ExtrudeBufferGeometry: .addShapeList() has been removed.' );

		},

		addShape: function () {

			console.error( 'THREE.ExtrudeBufferGeometry: .addShape() has been removed.' );

		}

	} );

	//

	Object.assign( Scene.prototype, {

		dispose: function () {

			console.error( 'THREE.Scene: .dispose() has been removed.' );

		}

	} );

	//

	Object.defineProperties( Uniform.prototype, {

		dynamic: {
			set: function () {

				console.warn( 'THREE.Uniform: .dynamic has been removed. Use object.onBeforeRender() instead.' );

			}
		},
		onUpdate: {
			value: function () {

				console.warn( 'THREE.Uniform: .onUpdate() has been removed. Use object.onBeforeRender() instead.' );
				return this;

			}
		}

	} );

	//

	Object.defineProperties( Material.prototype, {

		wrapAround: {
			get: function () {

				console.warn( 'THREE.Material: .wrapAround has been removed.' );

			},
			set: function () {

				console.warn( 'THREE.Material: .wrapAround has been removed.' );

			}
		},

		overdraw: {
			get: function () {

				console.warn( 'THREE.Material: .overdraw has been removed.' );

			},
			set: function () {

				console.warn( 'THREE.Material: .overdraw has been removed.' );

			}
		},

		wrapRGB: {
			get: function () {

				console.warn( 'THREE.Material: .wrapRGB has been removed.' );
				return new Color();

			}
		},

		shading: {
			get: function () {

				console.error( 'THREE.' + this.type + ': .shading has been removed. Use the boolean .flatShading instead.' );

			},
			set: function ( value ) {

				console.warn( 'THREE.' + this.type + ': .shading has been removed. Use the boolean .flatShading instead.' );
				this.flatShading = ( value === FlatShading );

			}
		},

		stencilMask: {
			get: function () {

				console.warn( 'THREE.' + this.type + ': .stencilMask has been removed. Use .stencilFuncMask instead.' );
				return this.stencilFuncMask;

			},
			set: function ( value ) {

				console.warn( 'THREE.' + this.type + ': .stencilMask has been removed. Use .stencilFuncMask instead.' );
				this.stencilFuncMask = value;

			}
		}

	} );

	Object.defineProperties( MeshPhongMaterial.prototype, {

		metal: {
			get: function () {

				console.warn( 'THREE.MeshPhongMaterial: .metal has been removed. Use THREE.MeshStandardMaterial instead.' );
				return false;

			},
			set: function () {

				console.warn( 'THREE.MeshPhongMaterial: .metal has been removed. Use THREE.MeshStandardMaterial instead' );

			}
		}

	} );

	Object.defineProperties( MeshPhysicalMaterial.prototype, {

		transparency: {
			get: function () {

				console.warn( 'THREE.MeshPhysicalMaterial: .transparency has been renamed to .transmission.' );
				return this.transmission;

			},
			set: function ( value ) {

				console.warn( 'THREE.MeshPhysicalMaterial: .transparency has been renamed to .transmission.' );
				this.transmission = value;

			}
		}

	} );

	Object.defineProperties( ShaderMaterial.prototype, {

		derivatives: {
			get: function () {

				console.warn( 'THREE.ShaderMaterial: .derivatives has been moved to .extensions.derivatives.' );
				return this.extensions.derivatives;

			},
			set: function ( value ) {

				console.warn( 'THREE. ShaderMaterial: .derivatives has been moved to .extensions.derivatives.' );
				this.extensions.derivatives = value;

			}
		}

	} );

	//

	Object.assign( WebGLRenderer.prototype, {

		clearTarget: function ( renderTarget, color, depth, stencil ) {

			console.warn( 'THREE.WebGLRenderer: .clearTarget() has been deprecated. Use .setRenderTarget() and .clear() instead.' );
			this.setRenderTarget( renderTarget );
			this.clear( color, depth, stencil );

		},
		animate: function ( callback ) {

			console.warn( 'THREE.WebGLRenderer: .animate() is now .setAnimationLoop().' );
			this.setAnimationLoop( callback );

		},
		getCurrentRenderTarget: function () {

			console.warn( 'THREE.WebGLRenderer: .getCurrentRenderTarget() is now .getRenderTarget().' );
			return this.getRenderTarget();

		},
		getMaxAnisotropy: function () {

			console.warn( 'THREE.WebGLRenderer: .getMaxAnisotropy() is now .capabilities.getMaxAnisotropy().' );
			return this.capabilities.getMaxAnisotropy();

		},
		getPrecision: function () {

			console.warn( 'THREE.WebGLRenderer: .getPrecision() is now .capabilities.precision.' );
			return this.capabilities.precision;

		},
		resetGLState: function () {

			console.warn( 'THREE.WebGLRenderer: .resetGLState() is now .state.reset().' );
			return this.state.reset();

		},
		supportsFloatTextures: function () {

			console.warn( 'THREE.WebGLRenderer: .supportsFloatTextures() is now .extensions.get( \'OES_texture_float\' ).' );
			return this.extensions.get( 'OES_texture_float' );

		},
		supportsHalfFloatTextures: function () {

			console.warn( 'THREE.WebGLRenderer: .supportsHalfFloatTextures() is now .extensions.get( \'OES_texture_half_float\' ).' );
			return this.extensions.get( 'OES_texture_half_float' );

		},
		supportsStandardDerivatives: function () {

			console.warn( 'THREE.WebGLRenderer: .supportsStandardDerivatives() is now .extensions.get( \'OES_standard_derivatives\' ).' );
			return this.extensions.get( 'OES_standard_derivatives' );

		},
		supportsCompressedTextureS3TC: function () {

			console.warn( 'THREE.WebGLRenderer: .supportsCompressedTextureS3TC() is now .extensions.get( \'WEBGL_compressed_texture_s3tc\' ).' );
			return this.extensions.get( 'WEBGL_compressed_texture_s3tc' );

		},
		supportsCompressedTexturePVRTC: function () {

			console.warn( 'THREE.WebGLRenderer: .supportsCompressedTexturePVRTC() is now .extensions.get( \'WEBGL_compressed_texture_pvrtc\' ).' );
			return this.extensions.get( 'WEBGL_compressed_texture_pvrtc' );

		},
		supportsBlendMinMax: function () {

			console.warn( 'THREE.WebGLRenderer: .supportsBlendMinMax() is now .extensions.get( \'EXT_blend_minmax\' ).' );
			return this.extensions.get( 'EXT_blend_minmax' );

		},
		supportsVertexTextures: function () {

			console.warn( 'THREE.WebGLRenderer: .supportsVertexTextures() is now .capabilities.vertexTextures.' );
			return this.capabilities.vertexTextures;

		},
		supportsInstancedArrays: function () {

			console.warn( 'THREE.WebGLRenderer: .supportsInstancedArrays() is now .extensions.get( \'ANGLE_instanced_arrays\' ).' );
			return this.extensions.get( 'ANGLE_instanced_arrays' );

		},
		enableScissorTest: function ( boolean ) {

			console.warn( 'THREE.WebGLRenderer: .enableScissorTest() is now .setScissorTest().' );
			this.setScissorTest( boolean );

		},
		initMaterial: function () {

			console.warn( 'THREE.WebGLRenderer: .initMaterial() has been removed.' );

		},
		addPrePlugin: function () {

			console.warn( 'THREE.WebGLRenderer: .addPrePlugin() has been removed.' );

		},
		addPostPlugin: function () {

			console.warn( 'THREE.WebGLRenderer: .addPostPlugin() has been removed.' );

		},
		updateShadowMap: function () {

			console.warn( 'THREE.WebGLRenderer: .updateShadowMap() has been removed.' );

		},
		setFaceCulling: function () {

			console.warn( 'THREE.WebGLRenderer: .setFaceCulling() has been removed.' );

		},
		allocTextureUnit: function () {

			console.warn( 'THREE.WebGLRenderer: .allocTextureUnit() has been removed.' );

		},
		setTexture: function () {

			console.warn( 'THREE.WebGLRenderer: .setTexture() has been removed.' );

		},
		setTexture2D: function () {

			console.warn( 'THREE.WebGLRenderer: .setTexture2D() has been removed.' );

		},
		setTextureCube: function () {

			console.warn( 'THREE.WebGLRenderer: .setTextureCube() has been removed.' );

		},
		getActiveMipMapLevel: function () {

			console.warn( 'THREE.WebGLRenderer: .getActiveMipMapLevel() is now .getActiveMipmapLevel().' );
			return this.getActiveMipmapLevel();

		}

	} );

	Object.defineProperties( WebGLRenderer.prototype, {

		shadowMapEnabled: {
			get: function () {

				return this.shadowMap.enabled;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderer: .shadowMapEnabled is now .shadowMap.enabled.' );
				this.shadowMap.enabled = value;

			}
		},
		shadowMapType: {
			get: function () {

				return this.shadowMap.type;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderer: .shadowMapType is now .shadowMap.type.' );
				this.shadowMap.type = value;

			}
		},
		shadowMapCullFace: {
			get: function () {

				console.warn( 'THREE.WebGLRenderer: .shadowMapCullFace has been removed. Set Material.shadowSide instead.' );
				return undefined;

			},
			set: function ( /* value */ ) {

				console.warn( 'THREE.WebGLRenderer: .shadowMapCullFace has been removed. Set Material.shadowSide instead.' );

			}
		},
		context: {
			get: function () {

				console.warn( 'THREE.WebGLRenderer: .context has been removed. Use .getContext() instead.' );
				return this.getContext();

			}
		},
		vr: {
			get: function () {

				console.warn( 'THREE.WebGLRenderer: .vr has been renamed to .xr' );
				return this.xr;

			}
		},
		gammaInput: {
			get: function () {

				console.warn( 'THREE.WebGLRenderer: .gammaInput has been removed. Set the encoding for textures via Texture.encoding instead.' );
				return false;

			},
			set: function () {

				console.warn( 'THREE.WebGLRenderer: .gammaInput has been removed. Set the encoding for textures via Texture.encoding instead.' );

			}
		},
		gammaOutput: {
			get: function () {

				console.warn( 'THREE.WebGLRenderer: .gammaOutput has been removed. Set WebGLRenderer.outputEncoding instead.' );
				return false;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderer: .gammaOutput has been removed. Set WebGLRenderer.outputEncoding instead.' );
				this.outputEncoding = ( value === true ) ? sRGBEncoding : LinearEncoding;

			}
		},
		toneMappingWhitePoint: {
			get: function () {

				console.warn( 'THREE.WebGLRenderer: .toneMappingWhitePoint has been removed.' );
				return 1.0;

			},
			set: function () {

				console.warn( 'THREE.WebGLRenderer: .toneMappingWhitePoint has been removed.' );

			}
		},

	} );

	Object.defineProperties( WebGLShadowMap.prototype, {

		cullFace: {
			get: function () {

				console.warn( 'THREE.WebGLRenderer: .shadowMap.cullFace has been removed. Set Material.shadowSide instead.' );
				return undefined;

			},
			set: function ( /* cullFace */ ) {

				console.warn( 'THREE.WebGLRenderer: .shadowMap.cullFace has been removed. Set Material.shadowSide instead.' );

			}
		},
		renderReverseSided: {
			get: function () {

				console.warn( 'THREE.WebGLRenderer: .shadowMap.renderReverseSided has been removed. Set Material.shadowSide instead.' );
				return undefined;

			},
			set: function () {

				console.warn( 'THREE.WebGLRenderer: .shadowMap.renderReverseSided has been removed. Set Material.shadowSide instead.' );

			}
		},
		renderSingleSided: {
			get: function () {

				console.warn( 'THREE.WebGLRenderer: .shadowMap.renderSingleSided has been removed. Set Material.shadowSide instead.' );
				return undefined;

			},
			set: function () {

				console.warn( 'THREE.WebGLRenderer: .shadowMap.renderSingleSided has been removed. Set Material.shadowSide instead.' );

			}
		}

	} );

	function WebGLRenderTargetCube( width, height, options ) {

		console.warn( 'THREE.WebGLRenderTargetCube( width, height, options ) is now WebGLCubeRenderTarget( size, options ).' );
		return new WebGLCubeRenderTarget( width, options );

	}

	//

	Object.defineProperties( WebGLRenderTarget.prototype, {

		wrapS: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .wrapS is now .texture.wrapS.' );
				return this.texture.wrapS;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .wrapS is now .texture.wrapS.' );
				this.texture.wrapS = value;

			}
		},
		wrapT: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .wrapT is now .texture.wrapT.' );
				return this.texture.wrapT;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .wrapT is now .texture.wrapT.' );
				this.texture.wrapT = value;

			}
		},
		magFilter: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .magFilter is now .texture.magFilter.' );
				return this.texture.magFilter;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .magFilter is now .texture.magFilter.' );
				this.texture.magFilter = value;

			}
		},
		minFilter: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .minFilter is now .texture.minFilter.' );
				return this.texture.minFilter;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .minFilter is now .texture.minFilter.' );
				this.texture.minFilter = value;

			}
		},
		anisotropy: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .anisotropy is now .texture.anisotropy.' );
				return this.texture.anisotropy;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .anisotropy is now .texture.anisotropy.' );
				this.texture.anisotropy = value;

			}
		},
		offset: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .offset is now .texture.offset.' );
				return this.texture.offset;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .offset is now .texture.offset.' );
				this.texture.offset = value;

			}
		},
		repeat: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .repeat is now .texture.repeat.' );
				return this.texture.repeat;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .repeat is now .texture.repeat.' );
				this.texture.repeat = value;

			}
		},
		format: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .format is now .texture.format.' );
				return this.texture.format;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .format is now .texture.format.' );
				this.texture.format = value;

			}
		},
		type: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .type is now .texture.type.' );
				return this.texture.type;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .type is now .texture.type.' );
				this.texture.type = value;

			}
		},
		generateMipmaps: {
			get: function () {

				console.warn( 'THREE.WebGLRenderTarget: .generateMipmaps is now .texture.generateMipmaps.' );
				return this.texture.generateMipmaps;

			},
			set: function ( value ) {

				console.warn( 'THREE.WebGLRenderTarget: .generateMipmaps is now .texture.generateMipmaps.' );
				this.texture.generateMipmaps = value;

			}
		}

	} );

	//

	Object.defineProperties( Audio.prototype, {

		load: {
			value: function ( file ) {

				console.warn( 'THREE.Audio: .load has been deprecated. Use THREE.AudioLoader instead.' );
				const scope = this;
				const audioLoader = new AudioLoader();
				audioLoader.load( file, function ( buffer ) {

					scope.setBuffer( buffer );

				} );
				return this;

			}
		},
		startTime: {
			set: function () {

				console.warn( 'THREE.Audio: .startTime is now .play( delay ).' );

			}
		}

	} );

	AudioAnalyser.prototype.getData = function () {

		console.warn( 'THREE.AudioAnalyser: .getData() is now .getFrequencyData().' );
		return this.getFrequencyData();

	};

	//

	CubeCamera.prototype.updateCubeMap = function ( renderer, scene ) {

		console.warn( 'THREE.CubeCamera: .updateCubeMap() is now .update().' );
		return this.update( renderer, scene );

	};

	CubeCamera.prototype.clear = function ( renderer, color, depth, stencil ) {

		console.warn( 'THREE.CubeCamera: .clear() is now .renderTarget.clear().' );
		return this.renderTarget.clear( renderer, color, depth, stencil );

	};

	//

	const GeometryUtils = {

		merge: function ( geometry1, geometry2, materialIndexOffset ) {

			console.warn( 'THREE.GeometryUtils: .merge() has been moved to Geometry. Use geometry.merge( geometry2, matrix, materialIndexOffset ) instead.' );
			let matrix;

			if ( geometry2.isMesh ) {

				geometry2.matrixAutoUpdate && geometry2.updateMatrix();

				matrix = geometry2.matrix;
				geometry2 = geometry2.geometry;

			}

			geometry1.merge( geometry2, matrix, materialIndexOffset );

		},

		center: function ( geometry ) {

			console.warn( 'THREE.GeometryUtils: .center() has been moved to Geometry. Use geometry.center() instead.' );
			return geometry.center();

		}

	};

	ImageUtils.crossOrigin = undefined;

	ImageUtils.loadTexture = function ( url, mapping, onLoad, onError ) {

		console.warn( 'THREE.ImageUtils.loadTexture has been deprecated. Use THREE.TextureLoader() instead.' );

		const loader = new TextureLoader();
		loader.setCrossOrigin( this.crossOrigin );

		const texture = loader.load( url, onLoad, undefined, onError );

		if ( mapping ) texture.mapping = mapping;

		return texture;

	};

	ImageUtils.loadTextureCube = function ( urls, mapping, onLoad, onError ) {

		console.warn( 'THREE.ImageUtils.loadTextureCube has been deprecated. Use THREE.CubeTextureLoader() instead.' );

		const loader = new CubeTextureLoader();
		loader.setCrossOrigin( this.crossOrigin );

		const texture = loader.load( urls, onLoad, undefined, onError );

		if ( mapping ) texture.mapping = mapping;

		return texture;

	};

	ImageUtils.loadCompressedTexture = function () {

		console.error( 'THREE.ImageUtils.loadCompressedTexture has been removed. Use THREE.DDSLoader instead.' );

	};

	ImageUtils.loadCompressedTextureCube = function () {

		console.error( 'THREE.ImageUtils.loadCompressedTextureCube has been removed. Use THREE.DDSLoader instead.' );

	};

	//

	function CanvasRenderer() {

		console.error( 'THREE.CanvasRenderer has been removed' );

	}

	//

	function JSONLoader() {

		console.error( 'THREE.JSONLoader has been removed.' );

	}

	//

	const SceneUtils = {

		createMultiMaterialObject: function ( /* geometry, materials */ ) {

			console.error( 'THREE.SceneUtils has been moved to /examples/jsm/utils/SceneUtils.js' );

		},

		detach: function ( /* child, parent, scene */ ) {

			console.error( 'THREE.SceneUtils has been moved to /examples/jsm/utils/SceneUtils.js' );

		},

		attach: function ( /* child, scene, parent */ ) {

			console.error( 'THREE.SceneUtils has been moved to /examples/jsm/utils/SceneUtils.js' );

		}

	};

	//

	function LensFlare() {

		console.error( 'THREE.LensFlare has been moved to /examples/jsm/objects/Lensflare.js' );

	}

	if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {

		/* eslint-disable no-undef */
		__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'register', { detail: {
			revision: REVISION,
		} } ) );
		/* eslint-enable no-undef */

	}

	class PathAnimation{
		
		constructor(path, start, end, speed, callback){
				this.path = path;
				this.length = this.path.spline.getLength();
				this.speed = speed;
				this.callback = callback;
				this.tween = null;
				this.startPoint = Math.max(start, 0);
				this.endPoint = Math.min(end, this.length);
				this.t = 0.0;
		}

		start(resume = false){
			if(this.tween){
				this.tween.stop();
				this.tween = null;
			}
		
			let tStart;
			if(resume){
				tStart = this.t;
			}else {
				tStart = this.startPoint / this.length;
			}
			let tEnd = this.endPoint / this.length;
			let animationDuration = (tEnd - tStart) * this.length * 1000 / this.speed;
		
			let progress = {t: tStart};
			this.tween = new TWEEN.Tween(progress).to({t: tEnd}, animationDuration);
			this.tween.easing(TWEEN.Easing.Linear.None);
			this.tween.onUpdate((e) => {
				this.t = progress.t;
				this.callback(progress.t);
			});
			this.tween.onComplete(() => {
				if(this.repeat){
					this.start();
				}
			});

			setTimeout(() => {
				this.tween.start();
			}, 0);
		}

		stop(){
			if(!this.tween){
				return;
			}
			this.tween.stop();
			this.tween = null;
			this.t = 0;
		}

		pause(){
			if(!this.tween){
				return;
			}
			
			this.tween.stop();
			TWEEN.remove(this.tween);
			this.tween = null;
		}

		resume(){
			this.start(true);
		}

		getPoint(t){
			return this.path.spline.getPoint(t);
		}

	}

	class AnimationPath{
		constructor (points = []) {
			this.points = points;
			this.spline = new CatmullRomCurve3(points);
			//this.spline.reparametrizeByArcLength(1 / this.spline.getLength().total);
		}

		get (t) {
			return this.spline.getPoint(t);
		}

		getLength () {
			return this.spline.getLength();
		}

		animate (start, end, speed, callback) {
			let animation = new PathAnimation(this, start, end, speed, callback);
			animation.start();

			return animation;
		}

		pause () {
			if (this.tween) {
				this.tween.stop();
			}
		}

		resume () {
			if (this.tween) {
				this.tween.start();
			}
		}

		getGeometry () {
			let geometry = new Geometry();

			let samples = 500;
			let i = 0;
			for (let u = 0; u <= 1; u += 1 / samples) {
				let position = this.spline.getPoint(u);
				geometry.vertices[i] = new Vector3(position.x, position.y, position.z);

				i++;
			}

			if(this.closed){
				let position = this.spline.getPoint(0);
				geometry.vertices[i] = new Vector3(position.x, position.y, position.z);
			}

			return geometry;
		}

		get closed(){
			return this.spline.closed;
		}

		set closed(value){
			this.spline.closed = value;
		}

	}

	const XHRFactory = {
		config: {
			withCredentials: false,
			customHeaders: [
				{ header: null, value: null }
			]
		},

		createXMLHttpRequest: function () {
			let xhr = new XMLHttpRequest();

			if (this.config.customHeaders &&
				Array.isArray(this.config.customHeaders) &&
				this.config.customHeaders.length > 0) {
				let baseOpen = xhr.open;
				let customHeaders = this.config.customHeaders;
				xhr.open = function () {
					baseOpen.apply(this, [].slice.call(arguments));
					customHeaders.forEach(function (customHeader) {
						if (!!customHeader.header && !!customHeader.value) {
							xhr.setRequestHeader(customHeader.header, customHeader.value);
						}
					});
				};
			}

			return xhr;
		}
	};

	class TextSprite extends Object3D{
		
		constructor(text){
			super();

			let texture = new Texture();
			texture.minFilter = LinearFilter;
			texture.magFilter = LinearFilter;
			let spriteMaterial = new SpriteMaterial({
				map: texture,
				depthTest: false,
				depthWrite: false});

			this.texture = texture;

			this.material = spriteMaterial;
			//this.material = getRawMaterial(texture);
			this.sprite = new Sprite(this.material);
			this.add(this.sprite);

			this.borderThickness = 4;
			this.fontface = 'Arial';
			this.fontsize = 28;
			this.borderColor = { r: 0, g: 0, b: 0, a: 1.0 };
			this.backgroundColor = { r: 255, g: 255, b: 255, a: 1.0 };
			this.textColor = {r: 255, g: 255, b: 255, a: 1.0};
			this.text = '';

			this.setText(text);
		}

		setText(text){
			if (this.text !== text){
				this.text = text;

				this.update();
			}
		}

		setTextColor(color){
			this.textColor = color;

			this.update();
		}

		setBorderColor(color){
			this.borderColor = color;

			this.update();
		}

		setBackgroundColor(color){
			this.backgroundColor = color;

			this.update();
		}

		update(){
			let canvas = document.createElement('canvas');
			let context = canvas.getContext('2d');
			context.font = 'Bold ' + this.fontsize + 'px ' + this.fontface;

			// get size data (height depends only on font size)
			let metrics = context.measureText(this.text);
			let textWidth = metrics.width;
			let margin = 5;
			let spriteWidth = 2 * margin + textWidth + 2 * this.borderThickness;
			let spriteHeight = this.fontsize * 1.4 + 2 * this.borderThickness;

			context.canvas.width = spriteWidth;
			context.canvas.height = spriteHeight;
			context.font = 'Bold ' + this.fontsize + 'px ' + this.fontface;

			// background color
			context.fillStyle = 'rgba(' + this.backgroundColor.r + ',' + this.backgroundColor.g + ',' +
				this.backgroundColor.b + ',' + this.backgroundColor.a + ')';
			// border color
			context.strokeStyle = 'rgba(' + this.borderColor.r + ',' + this.borderColor.g + ',' +
				this.borderColor.b + ',' + this.borderColor.a + ')';

			context.lineWidth = this.borderThickness;
			this.roundRect(context, this.borderThickness / 2, this.borderThickness / 2,
				textWidth + this.borderThickness + 2 * margin, this.fontsize * 1.4 + this.borderThickness, 6);

			// text color
			context.strokeStyle = 'rgba(0, 0, 0, 1.0)';
			context.strokeText(this.text, this.borderThickness + margin, this.fontsize + this.borderThickness);

			context.fillStyle = 'rgba(' + this.textColor.r + ',' + this.textColor.g + ',' +
				this.textColor.b + ',' + this.textColor.a + ')';
			context.fillText(this.text, this.borderThickness + margin, this.fontsize + this.borderThickness);

			let texture = new Texture(canvas);
			texture.minFilter = LinearFilter;
			texture.magFilter = LinearFilter;
			texture.needsUpdate = true;
			//this.material.needsUpdate = true;

			// { // screen-space sprite
			// 	let [screenWidth, screenHeight] = [1620, 937];

			// 	let uniforms = this.sprite.material.uniforms;
			// 	let aspect = spriteHeight / spriteWidth;
			// 	let factor = 0.5;

			// 	let w = spriteWidth / screenWidth;
			// 	let h = spriteHeight / screenHeight;

			// 	uniforms.uScale.value = [2 * w, 2 * h];
			// 	//uniforms.uScale.value = [factor * 1, factor * aspect];
			//	this.sprite.material.uniforms.map.value = texture;
			// }

			this.sprite.material.map = texture;
			this.texture = texture;

			this.sprite.scale.set(spriteWidth * 0.01, spriteHeight * 0.01, 1.0);
		}

		roundRect(ctx, x, y, w, h, r){
			ctx.beginPath();
			ctx.moveTo(x + r, y);
			ctx.lineTo(x + w - r, y);
			ctx.quadraticCurveTo(x + w, y, x + w, y + r);
			ctx.lineTo(x + w, y + h - r);
			ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
			ctx.lineTo(x + r, y + h);
			ctx.quadraticCurveTo(x, y + h, x, y + h - r);
			ctx.lineTo(x, y + r);
			ctx.quadraticCurveTo(x, y, x + r, y);
			ctx.closePath();
			ctx.fill();
			ctx.stroke();
		}

	}

	class Volume extends Object3D {
		constructor (args = {}) {
			super();

			if(this.constructor.name === "Volume"){
				console.warn("Can't create object of class Volume directly. Use classes BoxVolume or SphereVolume instead.");
			}

			//console.log(this);
			//console.log(this.constructor);
			//console.log(this.constructor.name);

			this._clip = args.clip || false;
			this._visible = true;
			this.showVolumeLabel = true;
			this._modifiable = args.modifiable || true;

			this.label = new TextSprite('0');
			this.label.setBorderColor({r: 0, g: 255, b: 0, a: 0.0});
			this.label.setBackgroundColor({r: 0, g: 255, b: 0, a: 0.0});
			this.label.material.depthTest = false;
			this.label.material.depthWrite = false;
			this.label.material.transparent = true;
			this.label.position.y -= 0.5;
			this.add(this.label);

			this.label.updateMatrixWorld = () => {
				let volumeWorldPos = new Vector3();
				volumeWorldPos.setFromMatrixPosition(this.matrixWorld);
				this.label.position.copy(volumeWorldPos);
				this.label.updateMatrix();
				this.label.matrixWorld.copy(this.label.matrix);
				this.label.matrixWorldNeedsUpdate = false;

				for (let i = 0, l = this.label.children.length; i < l; i++) {
					this.label.children[ i ].updateMatrixWorld(true);
				}
			};

			{ // event listeners
				this.addEventListener('select', e => {});
				this.addEventListener('deselect', e => {});
			}

		}

		get visible(){
			return this._visible;
		}

		set visible(value){
			if(this._visible !== value){
				this._visible = value;

				this.dispatchEvent({type: "visibility_changed", object: this});
			}
		}

		getVolume () {
			console.warn("override this in subclass");
		}

		update () {
			
		};

		raycast (raycaster, intersects) {

		}

		get clip () {
			return this._clip;
		}

		set clip (value) {

			if(this._clip !== value){
				this._clip = value;

				this.update();

				this.dispatchEvent({
					type: "clip_changed",
					object: this
				});
			}
			
		}

		get modifieable () {
			return this._modifiable;
		}

		set modifieable (value) {
			this._modifiable = value;

			this.update();
		}
	};


	class BoxVolume extends Volume{

		constructor(args = {}){
			super(args);

			this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
			this.name = 'box_' + this.constructor.counter;

			let boxGeometry = new BoxGeometry(1, 1, 1);
			boxGeometry.computeBoundingBox();

			let boxFrameGeometry = new Geometry();
			{
				let Vector3$1 = Vector3;

				boxFrameGeometry.vertices.push(

					// bottom
					new Vector3$1(-0.5, -0.5, 0.5),
					new Vector3$1(0.5, -0.5, 0.5),
					new Vector3$1(0.5, -0.5, 0.5),
					new Vector3$1(0.5, -0.5, -0.5),
					new Vector3$1(0.5, -0.5, -0.5),
					new Vector3$1(-0.5, -0.5, -0.5),
					new Vector3$1(-0.5, -0.5, -0.5),
					new Vector3$1(-0.5, -0.5, 0.5),
					// top
					new Vector3$1(-0.5, 0.5, 0.5),
					new Vector3$1(0.5, 0.5, 0.5),
					new Vector3$1(0.5, 0.5, 0.5),
					new Vector3$1(0.5, 0.5, -0.5),
					new Vector3$1(0.5, 0.5, -0.5),
					new Vector3$1(-0.5, 0.5, -0.5),
					new Vector3$1(-0.5, 0.5, -0.5),
					new Vector3$1(-0.5, 0.5, 0.5),
					// sides
					new Vector3$1(-0.5, -0.5, 0.5),
					new Vector3$1(-0.5, 0.5, 0.5),
					new Vector3$1(0.5, -0.5, 0.5),
					new Vector3$1(0.5, 0.5, 0.5),
					new Vector3$1(0.5, -0.5, -0.5),
					new Vector3$1(0.5, 0.5, -0.5),
					new Vector3$1(-0.5, -0.5, -0.5),
					new Vector3$1(-0.5, 0.5, -0.5),

				);

			}

			this.material = new MeshBasicMaterial({
				color: 0x00ff00,
				transparent: true,
				opacity: 0.3,
				depthTest: true,
				depthWrite: false});
			this.box = new Mesh(boxGeometry, this.material);
			this.box.geometry.computeBoundingBox();
			this.boundingBox = this.box.geometry.boundingBox;
			this.add(this.box);

			this.frame = new LineSegments(boxFrameGeometry, new LineBasicMaterial({color: 0x000000}));
			// this.frame.mode = THREE.Lines;
			this.add(this.frame);

			this.update();
		}

		update(){
			this.boundingBox = this.box.geometry.boundingBox;
			this.boundingSphere = this.boundingBox.getBoundingSphere(new Sphere());

			if (this._clip) {
				this.box.visible = false;
				this.label.visible = false;
			} else {
				this.box.visible = true;
				this.label.visible = this.showVolumeLabel;
			}
		}

		raycast (raycaster, intersects) {
			let is = [];
			this.box.raycast(raycaster, is);

			if (is.length > 0) {
				let I = is[0];
				intersects.push({
					distance: I.distance,
					object: this,
					point: I.point.clone()
				});
			}
		}

		getVolume(){
			return Math.abs(this.scale.x * this.scale.y * this.scale.z);
		}

	};

	class SphereVolume extends Volume{

		constructor(args = {}){
			super(args);

			this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
			this.name = 'sphere_' + this.constructor.counter;

			let sphereGeometry = new SphereGeometry(1, 32, 32);
			sphereGeometry.computeBoundingBox();

			this.material = new MeshBasicMaterial({
				color: 0x00ff00,
				transparent: true,
				opacity: 0.3,
				depthTest: true,
				depthWrite: false});
			this.sphere = new Mesh(sphereGeometry, this.material);
			this.sphere.visible = false;
			this.sphere.geometry.computeBoundingBox();
			this.boundingBox = this.sphere.geometry.boundingBox;
			this.add(this.sphere);

			this.label.visible = false;


			let frameGeometry = new Geometry();
			{
				let steps = 64;
				let uSegments = 8;
				let vSegments = 5;
				let r = 1;

				for(let uSegment = 0; uSegment < uSegments; uSegment++){

					let alpha = (uSegment / uSegments) * Math.PI * 2;
					let dirx = Math.cos(alpha);
					let diry = Math.sin(alpha);

					for(let i = 0; i <= steps; i++){
						let v = (i / steps) * Math.PI * 2;
						let vNext = v + 2 * Math.PI / steps;

						let height = Math.sin(v);
						let xyAmount = Math.cos(v);

						let heightNext = Math.sin(vNext);
						let xyAmountNext = Math.cos(vNext);

						let vertex = new Vector3(dirx * xyAmount, diry * xyAmount, height);
						frameGeometry.vertices.push(vertex);

						let vertexNext = new Vector3(dirx * xyAmountNext, diry * xyAmountNext, heightNext);
						frameGeometry.vertices.push(vertexNext);
					}
				}

				// creates rings at poles, just because it's easier to implement
				for(let vSegment = 0; vSegment <= vSegments + 1; vSegment++){

					//let height = (vSegment / (vSegments + 1)) * 2 - 1; // -1 to 1
					let uh = (vSegment / (vSegments + 1)); // -1 to 1
					uh = (1 - uh) * (-Math.PI / 2) + uh *(Math.PI / 2);
					let height = Math.sin(uh);

					console.log(uh, height);

					for(let i = 0; i <= steps; i++){
						let u = (i / steps) * Math.PI * 2;
						let uNext = u + 2 * Math.PI / steps;

						let dirx = Math.cos(u);
						let diry = Math.sin(u);

						let dirxNext = Math.cos(uNext);
						let diryNext = Math.sin(uNext);

						let xyAmount = Math.sqrt(1 - height * height);

						let vertex = new Vector3(dirx * xyAmount, diry * xyAmount, height);
						frameGeometry.vertices.push(vertex);

						let vertexNext = new Vector3(dirxNext * xyAmount, diryNext * xyAmount, height);
						frameGeometry.vertices.push(vertexNext);
					}
				}
			}

			this.frame = new LineSegments(frameGeometry, new LineBasicMaterial({color: 0x000000}));
			this.add(this.frame);

			let frameMaterial = new MeshBasicMaterial({wireframe: true, color: 0x000000});
			this.frame = new Mesh(sphereGeometry, frameMaterial);
			//this.add(this.frame);

			//this.frame = new THREE.LineSegments(boxFrameGeometry, new THREE.LineBasicMaterial({color: 0x000000}));
			// this.frame.mode = THREE.Lines;
			//this.add(this.frame);

			this.update();
		}

		update(){
			this.boundingBox = this.sphere.geometry.boundingBox;
			this.boundingSphere = this.boundingBox.getBoundingSphere(new Sphere());

			//if (this._clip) {
			//	this.sphere.visible = false;
			//	this.label.visible = false;
			//} else {
			//	this.sphere.visible = true;
			//	this.label.visible = this.showVolumeLabel;
			//}
		}

		raycast (raycaster, intersects) {
			let is = [];
			this.sphere.raycast(raycaster, is);

			if (is.length > 0) {
				let I = is[0];
				intersects.push({
					distance: I.distance,
					object: this,
					point: I.point.clone()
				});
			}
		}
		
		// see https://en.wikipedia.org/wiki/Ellipsoid#Volume
		getVolume(){
			return (4 / 3) * Math.PI * this.scale.x * this.scale.y * this.scale.z;
		}

	};

	class Profile extends Object3D{

		constructor () {
			super();

			this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;

			this.name = 'Profile_' + this.constructor.counter;
			this.points = [];
			this.spheres = [];
			this.edges = [];
			this.boxes = [];
			this.width = 1;
			this.height = 20;
			this._modifiable = true;

			this.sphereGeometry = new SphereGeometry(0.4, 10, 10);
			this.color = new Color(0xff0000);
			this.lineColor = new Color(0xff0000);
		}

		createSphereMaterial () {
			let sphereMaterial = new MeshLambertMaterial({
				//shading: THREE.SmoothShading,
				color: 0xff0000,
				depthTest: false,
				depthWrite: false}
			);

			return sphereMaterial;
		};

		getSegments () {
			let segments = [];

			for (let i = 0; i < this.points.length - 1; i++) {
				let start = this.points[i].clone();
				let end = this.points[i + 1].clone();
				segments.push({start: start, end: end});
			}

			return segments;
		}

		getSegmentMatrices () {
			let segments = this.getSegments();
			let matrices = [];

			for (let segment of segments) {
				let {start, end} = segment;

				let box = new Object3D();

				let length = start.clone().setZ(0).distanceTo(end.clone().setZ(0));
				box.scale.set(length, 10000, this.width);
				box.up.set(0, 0, 1);

				let center = new Vector3().addVectors(start, end).multiplyScalar(0.5);
				let diff = new Vector3().subVectors(end, start);
				let target = new Vector3(diff.y, -diff.x, 0);

				box.position.set(0, 0, 0);
				box.lookAt(target);
				box.position.copy(center);

				box.updateMatrixWorld();
				matrices.push(box.matrixWorld);
			}

			return matrices;
		}

		addMarker (point) {
			this.points.push(point);

			let sphere = new Mesh(this.sphereGeometry, this.createSphereMaterial());

			this.add(sphere);
			this.spheres.push(sphere);

			// edges & boxes
			if (this.points.length > 1) {
				let lineGeometry = new Geometry();
				lineGeometry.vertices.push(new Vector3(), new Vector3());
				lineGeometry.colors.push(this.lineColor, this.lineColor, this.lineColor);
				let lineMaterial = new LineBasicMaterial({
					vertexColors: VertexColors,
					linewidth: 2,
					transparent: true,
					opacity: 0.4
				});
				lineMaterial.depthTest = false;
				let edge = new Line(lineGeometry, lineMaterial);
				edge.visible = false;

				this.add(edge);
				this.edges.push(edge);

				let boxGeometry = new BoxGeometry(1, 1, 1);
				let boxMaterial = new MeshBasicMaterial({color: 0xff0000, transparent: true, opacity: 0.2});
				let box = new Mesh(boxGeometry, boxMaterial);
				box.visible = false;

				this.add(box);
				this.boxes.push(box);
			}

			{ // event listeners
				let drag = (e) => {
					let I = Utils.getMousePointCloudIntersection(
						e.drag.end, 
						e.viewer.scene.getActiveCamera(), 
						e.viewer, 
						e.viewer.scene.pointclouds);

					if (I) {
						let i = this.spheres.indexOf(e.drag.object);
						if (i !== -1) {
							this.setPosition(i, I.location);
							//this.dispatchEvent({
							//	'type': 'marker_moved',
							//	'profile': this,
							//	'index': i
							//});
						}
					}
				};

				let drop = e => {
					let i = this.spheres.indexOf(e.drag.object);
					if (i !== -1) {
						this.dispatchEvent({
							'type': 'marker_dropped',
							'profile': this,
							'index': i
						});
					}
				};

				let mouseover = (e) => e.object.material.emissive.setHex(0x888888);
				let mouseleave = (e) => e.object.material.emissive.setHex(0x000000);

				sphere.addEventListener('drag', drag);
				sphere.addEventListener('drop', drop);
				sphere.addEventListener('mouseover', mouseover);
				sphere.addEventListener('mouseleave', mouseleave);
			}

			let event = {
				type: 'marker_added',
				profile: this,
				sphere: sphere
			};
			this.dispatchEvent(event);

			this.setPosition(this.points.length - 1, point);
		}

		removeMarker (index) {
			this.points.splice(index, 1);

			this.remove(this.spheres[index]);

			let edgeIndex = (index === 0) ? 0 : (index - 1);
			this.remove(this.edges[edgeIndex]);
			this.edges.splice(edgeIndex, 1);
			this.remove(this.boxes[edgeIndex]);
			this.boxes.splice(edgeIndex, 1);

			this.spheres.splice(index, 1);

			this.update();

			this.dispatchEvent({
				'type': 'marker_removed',
				'profile': this
			});
		}

		setPosition (index, position) {
			let point = this.points[index];
			point.copy(position);

			let event = {
				type: 'marker_moved',
				profile:	this,
				index:	index,
				position: point.clone()
			};
			this.dispatchEvent(event);

			this.update();
		}

		setWidth (width) {
			this.width = width;

			let event = {
				type: 'width_changed',
				profile:	this,
				width:	width
			};
			this.dispatchEvent(event);

			this.update();
		}

		getWidth () {
			return this.width;
		}

		update () {
			if (this.points.length === 0) {
				return;
			} else if (this.points.length === 1) {
				let point = this.points[0];
				this.spheres[0].position.copy(point);

				return;
			}

			let min = this.points[0].clone();
			let max = this.points[0].clone();
			let centroid = new Vector3();
			let lastIndex = this.points.length - 1;
			for (let i = 0; i <= lastIndex; i++) {
				let point = this.points[i];
				let sphere = this.spheres[i];
				let leftIndex = (i === 0) ? lastIndex : i - 1;
				// let rightIndex = (i === lastIndex) ? 0 : i + 1;
				let leftVertex = this.points[leftIndex];
				// let rightVertex = this.points[rightIndex];
				let leftEdge = this.edges[leftIndex];
				let rightEdge = this.edges[i];
				let leftBox = this.boxes[leftIndex];
				// rightBox = this.boxes[i];

				// let leftEdgeLength = point.distanceTo(leftVertex);
				// let rightEdgeLength = point.distanceTo(rightVertex);
				// let leftEdgeCenter = new THREE.Vector3().addVectors(leftVertex, point).multiplyScalar(0.5);
				// let rightEdgeCenter = new THREE.Vector3().addVectors(point, rightVertex).multiplyScalar(0.5);

				sphere.position.copy(point);

				if (this._modifiable) {
					sphere.visible = true;
				} else {
					sphere.visible = false;
				}

				if (leftEdge) {
					leftEdge.geometry.vertices[1].copy(point);
					leftEdge.geometry.verticesNeedUpdate = true;
					leftEdge.geometry.computeBoundingSphere();
				}

				if (rightEdge) {
					rightEdge.geometry.vertices[0].copy(point);
					rightEdge.geometry.verticesNeedUpdate = true;
					rightEdge.geometry.computeBoundingSphere();
				}

				if (leftBox) {
					let start = leftVertex;
					let end = point;
					let length = start.clone().setZ(0).distanceTo(end.clone().setZ(0));
					leftBox.scale.set(length, 1000000, this.width);
					leftBox.up.set(0, 0, 1);

					let center = new Vector3().addVectors(start, end).multiplyScalar(0.5);
					let diff = new Vector3().subVectors(end, start);
					let target = new Vector3(diff.y, -diff.x, 0);

					leftBox.position.set(0, 0, 0);
					leftBox.lookAt(target);
					leftBox.position.copy(center);
				}

				centroid.add(point);
				min.min(point);
				max.max(point);
			}
			centroid.multiplyScalar(1 / this.points.length);

			for (let i = 0; i < this.boxes.length; i++) {
				let box = this.boxes[i];

				box.position.z = min.z + (max.z - min.z) / 2;
			}
		}

		raycast (raycaster, intersects) {
			for (let i = 0; i < this.points.length; i++) {
				let sphere = this.spheres[i];

				sphere.raycast(raycaster, intersects);
			}

			// recalculate distances because they are not necessarely correct
			// for scaled objects.
			// see https://github.com/mrdoob/three.js/issues/5827
			// TODO: remove this once the bug has been fixed
			for (let i = 0; i < intersects.length; i++) {
				let I = intersects[i];
				I.distance = raycaster.ray.origin.distanceTo(I.point);
			}
			intersects.sort(function (a, b) { return a.distance - b.distance; });
		};

		get modifiable () {
			return this._modifiable;
		}

		set modifiable (value) {
			this._modifiable = value;
			this.update();
		}

	}

	var LineSegmentsGeometry = function () {

		InstancedBufferGeometry.call( this );

		this.type = 'LineSegmentsGeometry';

		var positions = [ - 1, 2, 0, 1, 2, 0, - 1, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, - 1, - 1, 0, 1, - 1, 0 ];
		var uvs = [ - 1, 2, 1, 2, - 1, 1, 1, 1, - 1, - 1, 1, - 1, - 1, - 2, 1, - 2 ];
		var index = [ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ];

		this.setIndex( index );
		this.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
		this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );

	};

	LineSegmentsGeometry.prototype = Object.assign( Object.create( InstancedBufferGeometry.prototype ), {

		constructor: LineSegmentsGeometry,

		isLineSegmentsGeometry: true,

		applyMatrix4: function ( matrix ) {

			var start = this.attributes.instanceStart;
			var end = this.attributes.instanceEnd;

			if ( start !== undefined ) {

				start.applyMatrix4( matrix );

				end.applyMatrix4( matrix );

				start.needsUpdate = true;

			}

			if ( this.boundingBox !== null ) {

				this.computeBoundingBox();

			}

			if ( this.boundingSphere !== null ) {

				this.computeBoundingSphere();

			}

			return this;

		},

		setPositions: function ( array ) {

			var lineSegments;

			if ( array instanceof Float32Array ) {

				lineSegments = array;

			} else if ( Array.isArray( array ) ) {

				lineSegments = new Float32Array( array );

			}

			var instanceBuffer = new InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz

			this.setAttribute( 'instanceStart', new InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz
			this.setAttribute( 'instanceEnd', new InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz

			//

			this.computeBoundingBox();
			this.computeBoundingSphere();

			return this;

		},

		setColors: function ( array ) {

			var colors;

			if ( array instanceof Float32Array ) {

				colors = array;

			} else if ( Array.isArray( array ) ) {

				colors = new Float32Array( array );

			}

			var instanceColorBuffer = new InstancedInterleavedBuffer( colors, 6, 1 ); // rgb, rgb

			this.setAttribute( 'instanceColorStart', new InterleavedBufferAttribute( instanceColorBuffer, 3, 0 ) ); // rgb
			this.setAttribute( 'instanceColorEnd', new InterleavedBufferAttribute( instanceColorBuffer, 3, 3 ) ); // rgb

			return this;

		},

		fromWireframeGeometry: function ( geometry ) {

			this.setPositions( geometry.attributes.position.array );

			return this;

		},

		fromEdgesGeometry: function ( geometry ) {

			this.setPositions( geometry.attributes.position.array );

			return this;

		},

		fromMesh: function ( mesh ) {

			this.fromWireframeGeometry( new WireframeGeometry( mesh.geometry ) );

			// set colors, maybe

			return this;

		},

		fromLineSegments: function ( lineSegments ) {

			var geometry = lineSegments.geometry;

			if ( geometry.isGeometry ) {

				this.setPositions( geometry.vertices );

			} else if ( geometry.isBufferGeometry ) {

				this.setPositions( geometry.attributes.position.array ); // assumes non-indexed

			}

			// set colors, maybe

			return this;

		},

		computeBoundingBox: function () {

			var box = new Box3();

			return function computeBoundingBox() {

				if ( this.boundingBox === null ) {

					this.boundingBox = new Box3();

				}

				var start = this.attributes.instanceStart;
				var end = this.attributes.instanceEnd;

				if ( start !== undefined && end !== undefined ) {

					this.boundingBox.setFromBufferAttribute( start );

					box.setFromBufferAttribute( end );

					this.boundingBox.union( box );

				}

			};

		}(),

		computeBoundingSphere: function () {

			var vector = new Vector3();

			return function computeBoundingSphere() {

				if ( this.boundingSphere === null ) {

					this.boundingSphere = new Sphere();

				}

				if ( this.boundingBox === null ) {

					this.computeBoundingBox();

				}

				var start = this.attributes.instanceStart;
				var end = this.attributes.instanceEnd;

				if ( start !== undefined && end !== undefined ) {

					var center = this.boundingSphere.center;

					this.boundingBox.getCenter( center );

					var maxRadiusSq = 0;

					for ( var i = 0, il = start.count; i < il; i ++ ) {

						vector.fromBufferAttribute( start, i );
						maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) );

						vector.fromBufferAttribute( end, i );
						maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) );

					}

					this.boundingSphere.radius = Math.sqrt( maxRadiusSq );

					if ( isNaN( this.boundingSphere.radius ) ) {

						console.error( 'THREE.LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this );

					}

				}

			};

		}(),

		toJSON: function () {

			// todo

		},

		applyMatrix: function ( matrix ) {

			console.warn( 'THREE.LineSegmentsGeometry: applyMatrix() has been renamed to applyMatrix4().' );

			return this.applyMatrix4( matrix );

		}

	} );

	/**
	 * parameters = {
	 *  color: <hex>,
	 *  linewidth: <float>,
	 *  dashed: <boolean>,
	 *  dashScale: <float>,
	 *  dashSize: <float>,
	 *  dashOffset: <float>,
	 *  gapSize: <float>,
	 *  resolution: <Vector2>, // to be set by renderer
	 * }
	 */

	UniformsLib.line = {

		linewidth: { value: 1 },
		resolution: { value: new Vector2( 1, 1 ) },
		dashScale: { value: 1 },
		dashSize: { value: 1 },
		dashOffset: { value: 0 },
		gapSize: { value: 1 }, // todo FIX - maybe change to totalSize
		opacity: { value: 1 }

	};

	ShaderLib[ 'line' ] = {

		uniforms: UniformsUtils.merge( [
			UniformsLib.common,
			UniformsLib.fog,
			UniformsLib.line
		] ),

		vertexShader:
			`
		#include <common>
		#include <color_pars_vertex>
		#include <fog_pars_vertex>
		#include <logdepthbuf_pars_vertex>
		#include <clipping_planes_pars_vertex>

		uniform float linewidth;
		uniform vec2 resolution;

		attribute vec3 instanceStart;
		attribute vec3 instanceEnd;

		attribute vec3 instanceColorStart;
		attribute vec3 instanceColorEnd;

		varying vec2 vUv;

		#ifdef USE_DASH

			uniform float dashScale;
			attribute float instanceDistanceStart;
			attribute float instanceDistanceEnd;
			varying float vLineDistance;

		#endif

		void trimSegment( const in vec4 start, inout vec4 end ) {

			// trim end segment so it terminates between the camera plane and the near plane

			// conservative estimate of the near plane
			float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column
			float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column
			float nearEstimate = - 0.5 * b / a;

			float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );

			end.xyz = mix( start.xyz, end.xyz, alpha );

		}

		void main() {

			#ifdef USE_COLOR

				vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;

			#endif

			#ifdef USE_DASH

				vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd;

			#endif

			float aspect = resolution.x / resolution.y;

			vUv = uv;

			// camera space
			vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );
			vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );

			// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
			// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
			// but we need to perform ndc-space calculations in the shader, so we must address this issue directly
			// perhaps there is a more elegant solution -- WestLangley

			bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column

			if ( perspective ) {

				if ( start.z < 0.0 && end.z >= 0.0 ) {

					trimSegment( start, end );

				} else if ( end.z < 0.0 && start.z >= 0.0 ) {

					trimSegment( end, start );

				}

			}

			// clip space
			vec4 clipStart = projectionMatrix * start;
			vec4 clipEnd = projectionMatrix * end;

			// ndc space
			vec2 ndcStart = clipStart.xy / clipStart.w;
			vec2 ndcEnd = clipEnd.xy / clipEnd.w;

			// direction
			vec2 dir = ndcEnd - ndcStart;

			// account for clip-space aspect ratio
			dir.x *= aspect;
			dir = normalize( dir );

			// perpendicular to dir
			vec2 offset = vec2( dir.y, - dir.x );

			// undo aspect ratio adjustment
			dir.x /= aspect;
			offset.x /= aspect;

			// sign flip
			if ( position.x < 0.0 ) offset *= - 1.0;

			// endcaps
			if ( position.y < 0.0 ) {

				offset += - dir;

			} else if ( position.y > 1.0 ) {

				offset += dir;

			}

			// adjust for linewidth
			offset *= linewidth;

			// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
			offset /= resolution.y;

			// select end
			vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;

			// back to clip space
			offset *= clip.w;

			clip.xy += offset;

			gl_Position = clip;

			vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation

			#include <logdepthbuf_vertex>
			#include <clipping_planes_vertex>
			#include <fog_vertex>

		}
		`,

		fragmentShader:
			`
		uniform vec3 diffuse;
		uniform float opacity;

		#ifdef USE_DASH

			uniform float dashSize;
			uniform float dashOffset;
			uniform float gapSize;

		#endif

		varying float vLineDistance;

		#include <common>
		#include <color_pars_fragment>
		#include <fog_pars_fragment>
		#include <logdepthbuf_pars_fragment>
		#include <clipping_planes_pars_fragment>

		varying vec2 vUv;

		void main() {

			#include <clipping_planes_fragment>

			#ifdef USE_DASH

				if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps

				if ( mod( vLineDistance + dashOffset, dashSize + gapSize ) > dashSize ) discard; // todo - FIX

			#endif

			if ( abs( vUv.y ) > 1.0 ) {

				float a = vUv.x;
				float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
				float len2 = a * a + b * b;

				if ( len2 > 1.0 ) discard;

			}

			vec4 diffuseColor = vec4( diffuse, opacity );

			#include <logdepthbuf_fragment>
			#include <color_fragment>

			gl_FragColor = vec4( diffuseColor.rgb, diffuseColor.a );

			#include <tonemapping_fragment>
			#include <encodings_fragment>
			#include <fog_fragment>
			#include <premultiplied_alpha_fragment>

		}
		`
	};

	var LineMaterial = function ( parameters ) {

		ShaderMaterial.call( this, {

			type: 'LineMaterial',

			uniforms: UniformsUtils.clone( ShaderLib[ 'line' ].uniforms ),

			vertexShader: ShaderLib[ 'line' ].vertexShader,
			fragmentShader: ShaderLib[ 'line' ].fragmentShader,

			clipping: true // required for clipping support

		} );

		this.dashed = false;

		Object.defineProperties( this, {

			color: {

				enumerable: true,

				get: function () {

					return this.uniforms.diffuse.value;

				},

				set: function ( value ) {

					this.uniforms.diffuse.value = value;

				}

			},

			linewidth: {

				enumerable: true,

				get: function () {

					return this.uniforms.linewidth.value;

				},

				set: function ( value ) {

					this.uniforms.linewidth.value = value;

				}

			},

			dashScale: {

				enumerable: true,

				get: function () {

					return this.uniforms.dashScale.value;

				},

				set: function ( value ) {

					this.uniforms.dashScale.value = value;

				}

			},

			dashSize: {

				enumerable: true,

				get: function () {

					return this.uniforms.dashSize.value;

				},

				set: function ( value ) {

					this.uniforms.dashSize.value = value;

				}

			},

			dashOffset: {

				enumerable: true,

				get: function () {

					return this.uniforms.dashOffset.value;

				},

				set: function ( value ) {

					this.uniforms.dashOffset.value = value;

				}

			},

			gapSize: {

				enumerable: true,

				get: function () {

					return this.uniforms.gapSize.value;

				},

				set: function ( value ) {

					this.uniforms.gapSize.value = value;

				}

			},

			opacity: {

				enumerable: true,

				get: function () {

					return this.uniforms.opacity.value;

				},

				set: function ( value ) {

					this.uniforms.opacity.value = value;

				}

			},

			resolution: {

				enumerable: true,

				get: function () {

					return this.uniforms.resolution.value;

				},

				set: function ( value ) {

					this.uniforms.resolution.value.copy( value );

				}

			}

		} );

		this.setValues( parameters );

	};

	LineMaterial.prototype = Object.create( ShaderMaterial.prototype );
	LineMaterial.prototype.constructor = LineMaterial;

	LineMaterial.prototype.isLineMaterial = true;

	var LineSegments2 = function ( geometry, material ) {

		if ( geometry === undefined ) geometry = new LineSegmentsGeometry();
		if ( material === undefined ) material = new LineMaterial( { color: Math.random() * 0xffffff } );

		Mesh.call( this, geometry, material );

		this.type = 'LineSegments2';

	};

	LineSegments2.prototype = Object.assign( Object.create( Mesh.prototype ), {

		constructor: LineSegments2,

		isLineSegments2: true,

		computeLineDistances: ( function () { // for backwards-compatability, but could be a method of LineSegmentsGeometry...

			var start = new Vector3();
			var end = new Vector3();

			return function computeLineDistances() {

				var geometry = this.geometry;

				var instanceStart = geometry.attributes.instanceStart;
				var instanceEnd = geometry.attributes.instanceEnd;
				var lineDistances = new Float32Array( 2 * instanceStart.data.count );

				for ( var i = 0, j = 0, l = instanceStart.data.count; i < l; i ++, j += 2 ) {

					start.fromBufferAttribute( instanceStart, i );
					end.fromBufferAttribute( instanceEnd, i );

					lineDistances[ j ] = ( j === 0 ) ? 0 : lineDistances[ j - 1 ];
					lineDistances[ j + 1 ] = lineDistances[ j ] + start.distanceTo( end );

				}

				var instanceDistanceBuffer = new InstancedInterleavedBuffer( lineDistances, 2, 1 ); // d0, d1

				geometry.setAttribute( 'instanceDistanceStart', new InterleavedBufferAttribute( instanceDistanceBuffer, 1, 0 ) ); // d0
				geometry.setAttribute( 'instanceDistanceEnd', new InterleavedBufferAttribute( instanceDistanceBuffer, 1, 1 ) ); // d1

				return this;

			};

		}() ),

		raycast: ( function () {

			var start = new Vector4();
			var end = new Vector4();

			var ssOrigin = new Vector4();
			var ssOrigin3 = new Vector3();
			var mvMatrix = new Matrix4();
			var line = new Line3();
			var closestPoint = new Vector3();

			return function raycast( raycaster, intersects ) {

				if ( raycaster.camera === null ) {

					console.error( 'LineSegments2: "Raycaster.camera" needs to be set in order to raycast against LineSegments2.' );

				}

				var threshold = ( raycaster.params.Line2 !== undefined ) ? raycaster.params.Line2.threshold || 0 : 0;

				var ray = raycaster.ray;
				var camera = raycaster.camera;
				var projectionMatrix = camera.projectionMatrix;

				var geometry = this.geometry;
				var material = this.material;
				var resolution = material.resolution;
				var lineWidth = material.linewidth + threshold;

				var instanceStart = geometry.attributes.instanceStart;
				var instanceEnd = geometry.attributes.instanceEnd;

				// pick a point 1 unit out along the ray to avoid the ray origin
				// sitting at the camera origin which will cause "w" to be 0 when
				// applying the projection matrix.
				ray.at( 1, ssOrigin );

				// ndc space [ - 1.0, 1.0 ]
				ssOrigin.w = 1;
				ssOrigin.applyMatrix4( camera.matrixWorldInverse );
				ssOrigin.applyMatrix4( projectionMatrix );
				ssOrigin.multiplyScalar( 1 / ssOrigin.w );

				// screen space
				ssOrigin.x *= resolution.x / 2;
				ssOrigin.y *= resolution.y / 2;
				ssOrigin.z = 0;

				ssOrigin3.copy( ssOrigin );

				var matrixWorld = this.matrixWorld;
				mvMatrix.multiplyMatrices( camera.matrixWorldInverse, matrixWorld );

				for ( var i = 0, l = instanceStart.count; i < l; i ++ ) {

					start.fromBufferAttribute( instanceStart, i );
					end.fromBufferAttribute( instanceEnd, i );

					start.w = 1;
					end.w = 1;

					// camera space
					start.applyMatrix4( mvMatrix );
					end.applyMatrix4( mvMatrix );

					// clip space
					start.applyMatrix4( projectionMatrix );
					end.applyMatrix4( projectionMatrix );

					// ndc space [ - 1.0, 1.0 ]
					start.multiplyScalar( 1 / start.w );
					end.multiplyScalar( 1 / end.w );

					// skip the segment if it's outside the camera near and far planes
					var isBehindCameraNear = start.z < - 1 && end.z < - 1;
					var isPastCameraFar = start.z > 1 && end.z > 1;
					if ( isBehindCameraNear || isPastCameraFar ) {

						continue;

					}

					// screen space
					start.x *= resolution.x / 2;
					start.y *= resolution.y / 2;

					end.x *= resolution.x / 2;
					end.y *= resolution.y / 2;

					// create 2d segment
					line.start.copy( start );
					line.start.z = 0;

					line.end.copy( end );
					line.end.z = 0;

					// get closest point on ray to segment
					var param = line.closestPointToPointParameter( ssOrigin3, true );
					line.at( param, closestPoint );

					// check if the intersection point is within clip space
					var zPos = MathUtils.lerp( start.z, end.z, param );
					var isInClipSpace = zPos >= - 1 && zPos <= 1;

					var isInside = ssOrigin3.distanceTo( closestPoint ) < lineWidth * 0.5;

					if ( isInClipSpace && isInside ) {

						line.start.fromBufferAttribute( instanceStart, i );
						line.end.fromBufferAttribute( instanceEnd, i );

						line.start.applyMatrix4( matrixWorld );
						line.end.applyMatrix4( matrixWorld );

						var pointOnLine = new Vector3();
						var point = new Vector3();

						ray.distanceSqToSegment( line.start, line.end, point, pointOnLine );

						intersects.push( {

							point: point,
							pointOnLine: pointOnLine,
							distance: ray.origin.distanceTo( point ),

							object: this,
							face: null,
							faceIndex: i,
							uv: null,
							uv2: null,

						} );

					}

				}

			};

		}() )

	} );

	var LineGeometry = function () {

		LineSegmentsGeometry.call( this );

		this.type = 'LineGeometry';

	};

	LineGeometry.prototype = Object.assign( Object.create( LineSegmentsGeometry.prototype ), {

		constructor: LineGeometry,

		isLineGeometry: true,

		setPositions: function ( array ) {

			// converts [ x1, y1, z1,  x2, y2, z2, ... ] to pairs format

			var length = array.length - 3;
			var points = new Float32Array( 2 * length );

			for ( var i = 0; i < length; i += 3 ) {

				points[ 2 * i ] = array[ i ];
				points[ 2 * i + 1 ] = array[ i + 1 ];
				points[ 2 * i + 2 ] = array[ i + 2 ];

				points[ 2 * i + 3 ] = array[ i + 3 ];
				points[ 2 * i + 4 ] = array[ i + 4 ];
				points[ 2 * i + 5 ] = array[ i + 5 ];

			}

			LineSegmentsGeometry.prototype.setPositions.call( this, points );

			return this;

		},

		setColors: function ( array ) {

			// converts [ r1, g1, b1,  r2, g2, b2, ... ] to pairs format

			var length = array.length - 3;
			var colors = new Float32Array( 2 * length );

			for ( var i = 0; i < length; i += 3 ) {

				colors[ 2 * i ] = array[ i ];
				colors[ 2 * i + 1 ] = array[ i + 1 ];
				colors[ 2 * i + 2 ] = array[ i + 2 ];

				colors[ 2 * i + 3 ] = array[ i + 3 ];
				colors[ 2 * i + 4 ] = array[ i + 4 ];
				colors[ 2 * i + 5 ] = array[ i + 5 ];

			}

			LineSegmentsGeometry.prototype.setColors.call( this, colors );

			return this;

		},

		fromLine: function ( line ) {

			var geometry = line.geometry;

			if ( geometry.isGeometry ) {

				this.setPositions( geometry.vertices );

			} else if ( geometry.isBufferGeometry ) {

				this.setPositions( geometry.attributes.position.array ); // assumes non-indexed

			}

			// set colors, maybe

			return this;

		},

		copy: function ( /* source */ ) {

			// todo

			return this;

		}

	} );

	var Line2 = function ( geometry, material ) {

		if ( geometry === undefined ) geometry = new LineGeometry();
		if ( material === undefined ) material = new LineMaterial( { color: Math.random() * 0xffffff } );

		LineSegments2.call( this, geometry, material );

		this.type = 'Line2';

	};

	Line2.prototype = Object.assign( Object.create( LineSegments2.prototype ), {

		constructor: Line2,

		isLine2: true

	} );

	function createHeightLine(){
		let lineGeometry = new LineGeometry();

		lineGeometry.setPositions([
			0, 0, 0,
			0, 0, 0,
		]);

		let lineMaterial = new LineMaterial({ 
			color: 0x00ff00, 
			dashSize: 5, 
			gapSize: 2,
			linewidth: 2, 
			resolution:  new Vector2(1000, 1000),
		});

		lineMaterial.depthTest = false;
		const heightEdge = new Line2(lineGeometry, lineMaterial);
		heightEdge.visible = false;

		//this.add(this.heightEdge);
		
		return heightEdge;
	}

	function createHeightLabel(){
		const heightLabel = new TextSprite('');

		heightLabel.setTextColor({r: 140, g: 250, b: 140, a: 1.0});
		heightLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
		heightLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
		heightLabel.fontsize = 16;
		heightLabel.material.depthTest = false;
		heightLabel.material.opacity = 1;
		heightLabel.visible = false;

		return heightLabel;
	}

	function createAreaLabel(){
		const areaLabel = new TextSprite('');

		areaLabel.setTextColor({r: 140, g: 250, b: 140, a: 1.0});
		areaLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
		areaLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
		areaLabel.fontsize = 16;
		areaLabel.material.depthTest = false;
		areaLabel.material.opacity = 1;
		areaLabel.visible = false;
		
		return areaLabel;
	}

	function createCircleRadiusLabel(){
		const circleRadiusLabel = new TextSprite("");

		circleRadiusLabel.setTextColor({r: 140, g: 250, b: 140, a: 1.0});
		circleRadiusLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
		circleRadiusLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
		circleRadiusLabel.fontsize = 16;
		circleRadiusLabel.material.depthTest = false;
		circleRadiusLabel.material.opacity = 1;
		circleRadiusLabel.visible = false;
		
		return circleRadiusLabel;
	}

	function createCircleRadiusLine(){
		const lineGeometry = new LineGeometry();

		lineGeometry.setPositions([
			0, 0, 0,
			0, 0, 0,
		]);

		const lineMaterial = new LineMaterial({ 
			color: 0xff0000, 
			linewidth: 2, 
			resolution:  new Vector2(1000, 1000),
			gapSize: 1,
			dashed: true,
		});

		lineMaterial.depthTest = false;

		const circleRadiusLine = new Line2(lineGeometry, lineMaterial);
		circleRadiusLine.visible = false;

		return circleRadiusLine;
	}

	function createCircleLine(){
		const coordinates = [];

		let n = 128;
		for(let i = 0; i <= n; i++){
			let u0 = 2 * Math.PI * (i / n);
			let u1 = 2 * Math.PI * (i + 1) / n;

			let p0 = new Vector3(
				Math.cos(u0), 
				Math.sin(u0), 
				0
			);

			let p1 = new Vector3(
				Math.cos(u1), 
				Math.sin(u1), 
				0
			);

			coordinates.push(
				...p0.toArray(),
				...p1.toArray(),
			);
		}

		const geometry = new LineGeometry();
		geometry.setPositions(coordinates);

		const material = new LineMaterial({ 
			color: 0xff0000, 
			dashSize: 5, 
			gapSize: 2,
			linewidth: 2, 
			resolution:  new Vector2(1000, 1000),
		});

		material.depthTest = false;

		const circleLine = new Line2(geometry, material);
		circleLine.visible = false;
		circleLine.computeLineDistances();

		return circleLine;
	}

	function createCircleCenter(){
		const sg = new SphereGeometry(1, 32, 32);
		const sm = new MeshNormalMaterial();
		
		const circleCenter = new Mesh(sg, sm);
		circleCenter.visible = false;

		return circleCenter;
	}

	function createLine(){
		const geometry = new LineGeometry();

		geometry.setPositions([
			0, 0, 0,
			0, 0, 0,
		]);

		const material = new LineMaterial({ 
			color: 0xff0000, 
			linewidth: 2, 
			resolution:  new Vector2(1000, 1000),
			gapSize: 1,
			dashed: true,
		});

		material.depthTest = false;

		const line = new Line2(geometry, material);

		return line;
	}

	function createCircle(){

		const coordinates = [];

		let n = 128;
		for(let i = 0; i <= n; i++){
			let u0 = 2 * Math.PI * (i / n);
			let u1 = 2 * Math.PI * (i + 1) / n;

			let p0 = new Vector3(
				Math.cos(u0), 
				Math.sin(u0), 
				0
			);

			let p1 = new Vector3(
				Math.cos(u1), 
				Math.sin(u1), 
				0
			);

			coordinates.push(
				...p0.toArray(),
				...p1.toArray(),
			);
		}

		const geometry = new LineGeometry();
		geometry.setPositions(coordinates);

		const material = new LineMaterial({ 
			color: 0xff0000, 
			dashSize: 5, 
			gapSize: 2,
			linewidth: 2, 
			resolution:  new Vector2(1000, 1000),
		});

		material.depthTest = false;

		const line = new Line2(geometry, material);
		line.computeLineDistances();

		return line;

	}

	function createAzimuth(){

		const azimuth = {
			label: null,
			center: null,
			target: null,
			north: null,
			centerToNorth: null,
			centerToTarget: null,
			centerToTargetground: null,
			targetgroundToTarget: null,
			circle: null,

			node: null,
		};

		const sg = new SphereGeometry(1, 32, 32);
		const sm = new MeshNormalMaterial();

		{
			const label = new TextSprite("");

			label.setTextColor({r: 140, g: 250, b: 140, a: 1.0});
			label.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
			label.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
			label.fontsize = 16;
			label.material.depthTest = false;
			label.material.opacity = 1;

			azimuth.label = label;
		}

		azimuth.center = new Mesh(sg, sm);
		azimuth.target = new Mesh(sg, sm);
		azimuth.north = new Mesh(sg, sm);
		azimuth.centerToNorth = createLine();
		azimuth.centerToTarget = createLine();
		azimuth.centerToTargetground = createLine();
		azimuth.targetgroundToTarget = createLine();
		azimuth.circle = createCircle();

		azimuth.node = new Object3D();
		azimuth.node.add(
			azimuth.centerToNorth,
			azimuth.centerToTarget,
			azimuth.centerToTargetground,
			azimuth.targetgroundToTarget,
			azimuth.circle,
			azimuth.label,
			azimuth.center,
			azimuth.target,
			azimuth.north,
		);

		return azimuth;
	}

	class Measure extends Object3D {
		constructor () {
			super();

			this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;

			this.name = 'Measure_' + this.constructor.counter;
			this.points = [];
			this._showDistances = true;
			this._showCoordinates = false;
			this._showArea = false;
			this._closed = true;
			this._showAngles = false;
			this._showCircle = false;
			this._showHeight = false;
			this._showEdges = true;
			this._showAzimuth = false;
			this.maxMarkers = Number.MAX_SAFE_INTEGER;

			this.sphereGeometry = new SphereGeometry(0.4, 10, 10);
			this.color = new Color(0xff0000);

			this.spheres = [];
			this.edges = [];
			this.sphereLabels = [];
			this.edgeLabels = [];
			this.angleLabels = [];
			this.coordinateLabels = [];

			this.heightEdge = createHeightLine();
			this.heightLabel = createHeightLabel();
			this.areaLabel = createAreaLabel();
			this.circleRadiusLabel = createCircleRadiusLabel();
			this.circleRadiusLine = createCircleRadiusLine();
			this.circleLine = createCircleLine();
			this.circleCenter = createCircleCenter();

			this.azimuth = createAzimuth();

			this.add(this.heightEdge);
			this.add(this.heightLabel);
			this.add(this.areaLabel);
			this.add(this.circleRadiusLabel);
			this.add(this.circleRadiusLine);
			this.add(this.circleLine);
			this.add(this.circleCenter);

			this.add(this.azimuth.node);

		}

		createSphereMaterial () {
			let sphereMaterial = new MeshLambertMaterial({
				//shading: THREE.SmoothShading,
				color: this.color,
				depthTest: false,
				depthWrite: false}
			);

			return sphereMaterial;
		};

		addMarker (point) {
			if (point.x != null) {
				point = {position: point};
			}else if(point instanceof Array){
				point = {position: new Vector3(...point)};
			}
			this.points.push(point);

			// sphere
			let sphere = new Mesh(this.sphereGeometry, this.createSphereMaterial());

			this.add(sphere);
			this.spheres.push(sphere);

			{ // edges
				let lineGeometry = new LineGeometry();
				lineGeometry.setPositions( [
						0, 0, 0,
						0, 0, 0,
				]);

				let lineMaterial = new LineMaterial({
					color: 0xff0000, 
					linewidth: 2, 
					resolution:  new Vector2(1000, 1000),
				});

				lineMaterial.depthTest = false;

				let edge = new Line2(lineGeometry, lineMaterial);
				edge.visible = true;

				this.add(edge);
				this.edges.push(edge);
			}

			{ // edge labels
				let edgeLabel = new TextSprite();
				edgeLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
				edgeLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
				edgeLabel.material.depthTest = false;
				edgeLabel.visible = false;
				edgeLabel.fontsize = 16;
				this.edgeLabels.push(edgeLabel);
				this.add(edgeLabel);
			}

			{ // angle labels
				let angleLabel = new TextSprite();
				angleLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
				angleLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
				angleLabel.fontsize = 16;
				angleLabel.material.depthTest = false;
				angleLabel.material.opacity = 1;
				angleLabel.visible = false;
				this.angleLabels.push(angleLabel);
				this.add(angleLabel);
			}

			{ // coordinate labels
				let coordinateLabel = new TextSprite();
				coordinateLabel.setBorderColor({r: 0, g: 0, b: 0, a: 1.0});
				coordinateLabel.setBackgroundColor({r: 0, g: 0, b: 0, a: 1.0});
				coordinateLabel.fontsize = 16;
				coordinateLabel.material.depthTest = false;
				coordinateLabel.material.opacity = 1;
				coordinateLabel.visible = false;
				this.coordinateLabels.push(coordinateLabel);
				this.add(coordinateLabel);
			}

			{ // Event Listeners
				let drag = (e) => {
					let I = Utils.getMousePointCloudIntersection(
						e.drag.end, 
						e.viewer.scene.getActiveCamera(), 
						e.viewer, 
						e.viewer.scene.pointclouds,
						{pickClipped: true});

					if (I) {
						let i = this.spheres.indexOf(e.drag.object);
						if (i !== -1) {
							let point = this.points[i];
							
							// loop through current keys and cleanup ones that will be orphaned
							for (let key of Object.keys(point)) {
								if (!I.point[key]) {
									delete point[key];
								}
							}

							for (let key of Object.keys(I.point).filter(e => e !== 'position')) {
								point[key] = I.point[key];
							}

							this.setPosition(i, I.location);
						}
					}
				};

				let drop = e => {
					let i = this.spheres.indexOf(e.drag.object);
					if (i !== -1) {
						this.dispatchEvent({
							'type': 'marker_dropped',
							'measurement': this,
							'index': i
						});
					}
				};

				let mouseover = (e) => e.object.material.emissive.setHex(0x888888);
				let mouseleave = (e) => e.object.material.emissive.setHex(0x000000);

				sphere.addEventListener('drag', drag);
				sphere.addEventListener('drop', drop);
				sphere.addEventListener('mouseover', mouseover);
				sphere.addEventListener('mouseleave', mouseleave);
			}

			let event = {
				type: 'marker_added',
				measurement: this,
				sphere: sphere
			};
			this.dispatchEvent(event);

			this.setMarker(this.points.length - 1, point);
		};

		removeMarker (index) {
			this.points.splice(index, 1);

			this.remove(this.spheres[index]);

			let edgeIndex = (index === 0) ? 0 : (index - 1);
			this.remove(this.edges[edgeIndex]);
			this.edges.splice(edgeIndex, 1);

			this.remove(this.edgeLabels[edgeIndex]);
			this.edgeLabels.splice(edgeIndex, 1);
			this.coordinateLabels.splice(index, 1);

			this.remove(this.angleLabels[index]);
			this.angleLabels.splice(index, 1);

			this.spheres.splice(index, 1);

			this.update();

			this.dispatchEvent({type: 'marker_removed', measurement: this});
		};

		setMarker (index, point) {
			this.points[index] = point;

			let event = {
				type: 'marker_moved',
				measure:	this,
				index:	index,
				position: point.position.clone()
			};
			this.dispatchEvent(event);

			this.update();
		}

		setPosition (index, position) {
			let point = this.points[index];
			point.position.copy(position);

			let event = {
				type: 'marker_moved',
				measure:	this,
				index:	index,
				position: position.clone()
			};
			this.dispatchEvent(event);

			this.update();
		};

		getArea () {
			let area = 0;
			let j = this.points.length - 1;

			for (let i = 0; i < this.points.length; i++) {
				let p1 = this.points[i].position;
				let p2 = this.points[j].position;
				area += (p2.x + p1.x) * (p1.y - p2.y);
				j = i;
			}

			return Math.abs(area / 2);
		};

		getTotalDistance () {
			if (this.points.length === 0) {
				return 0;
			}

			let distance = 0;

			for (let i = 1; i < this.points.length; i++) {
				let prev = this.points[i - 1].position;
				let curr = this.points[i].position;
				let d = prev.distanceTo(curr);

				distance += d;
			}

			if (this.closed && this.points.length > 1) {
				let first = this.points[0].position;
				let last = this.points[this.points.length - 1].position;
				let d = last.distanceTo(first);

				distance += d;
			}

			return distance;
		}

		getAngleBetweenLines (cornerPoint, point1, point2) {
			let v1 = new Vector3().subVectors(point1.position, cornerPoint.position);
			let v2 = new Vector3().subVectors(point2.position, cornerPoint.position);

			// avoid the error printed by threejs if denominator is 0
			const denominator = Math.sqrt( v1.lengthSq() * v2.lengthSq() );
			if(denominator === 0){
				return 0;
			}else {
				return v1.angleTo(v2);
			}
		};

		getAngle (index) {
			if (this.points.length < 3 || index >= this.points.length) {
				return 0;
			}

			let previous = (index === 0) ? this.points[this.points.length - 1] : this.points[index - 1];
			let point = this.points[index];
			let next = this.points[(index + 1) % (this.points.length)];

			return this.getAngleBetweenLines(point, previous, next);
		}

		// updateAzimuth(){
		// 	// if(this.points.length !== 2){
		// 	// 	return;
		// 	// }

		// 	// const azimuth = this.azimuth;

		// 	// const [p0, p1] = this.points;

		// 	// const r = p0.position.distanceTo(p1.position);
			
		// }

		update () {
			if (this.points.length === 0) {
				return;
			} else if (this.points.length === 1) {
				let point = this.points[0];
				let position = point.position;
				this.spheres[0].position.copy(position);

				{ // coordinate labels
					let coordinateLabel = this.coordinateLabels[0];
					
					let msg = position.toArray().map(p => Utils.addCommas(p.toFixed(2))).join(" / ");
					coordinateLabel.setText(msg);

					coordinateLabel.visible = this.showCoordinates;
				}

				return;
			}

			let lastIndex = this.points.length - 1;

			let centroid = new Vector3();
			for (let i = 0; i <= lastIndex; i++) {
				let point = this.points[i];
				centroid.add(point.position);
			}
			centroid.divideScalar(this.points.length);

			for (let i = 0; i <= lastIndex; i++) {
				let index = i;
				let nextIndex = (i + 1 > lastIndex) ? 0 : i + 1;
				let previousIndex = (i === 0) ? lastIndex : i - 1;

				let point = this.points[index];
				let nextPoint = this.points[nextIndex];
				let previousPoint = this.points[previousIndex];

				let sphere = this.spheres[index];

				// spheres
				sphere.position.copy(point.position);
				sphere.material.color = this.color;

				{ // edges
					let edge = this.edges[index];

					edge.material.color = this.color;

					edge.position.copy(point.position);

					edge.geometry.setPositions([
						0, 0, 0,
						...nextPoint.position.clone().sub(point.position).toArray(),
					]);

					edge.geometry.verticesNeedUpdate = true;
					edge.geometry.computeBoundingSphere();
					edge.computeLineDistances();
					edge.visible = index < lastIndex || this.closed;
					
					if(!this.showEdges){
						edge.visible = false;
					}
				}

				{ // edge labels
					let edgeLabel = this.edgeLabels[i];

					let center = new Vector3().add(point.position);
					center.add(nextPoint.position);
					center = center.multiplyScalar(0.5);
					let distance = point.position.distanceTo(nextPoint.position);

					edgeLabel.position.copy(center);

					let suffix = "";
					if(this.lengthUnit != null && this.lengthUnitDisplay != null){
						distance = distance / this.lengthUnit.unitspermeter * this.lengthUnitDisplay.unitspermeter;  //convert to meters then to the display unit
						suffix = this.lengthUnitDisplay.code;
					}

					let txtLength = Utils.addCommas(distance.toFixed(2));
					edgeLabel.setText(`${txtLength} ${suffix}`);
					edgeLabel.visible = this.showDistances && (index < lastIndex || this.closed) && this.points.length >= 2 && distance > 0;
				}

				{ // angle labels
					let angleLabel = this.angleLabels[i];
					let angle = this.getAngleBetweenLines(point, previousPoint, nextPoint);

					let dir = nextPoint.position.clone().sub(previousPoint.position);
					dir.multiplyScalar(0.5);
					dir = previousPoint.position.clone().add(dir).sub(point.position).normalize();

					let dist = Math.min(point.position.distanceTo(previousPoint.position), point.position.distanceTo(nextPoint.position));
					dist = dist / 9;

					let labelPos = point.position.clone().add(dir.multiplyScalar(dist));
					angleLabel.position.copy(labelPos);

					let msg = Utils.addCommas((angle * (180.0 / Math.PI)).toFixed(1)) + '\u00B0';
					angleLabel.setText(msg);

					angleLabel.visible = this.showAngles && (index < lastIndex || this.closed) && this.points.length >= 3 && angle > 0;
				}
			}

			{ // update height stuff
				let heightEdge = this.heightEdge;
				heightEdge.visible = this.showHeight;
				this.heightLabel.visible = this.showHeight;

				if (this.showHeight) {
					let sorted = this.points.slice().sort((a, b) => a.position.z - b.position.z);
					let lowPoint = sorted[0].position.clone();
					let highPoint = sorted[sorted.length - 1].position.clone();
					let min = lowPoint.z;
					let max = highPoint.z;
					let height = max - min;

					let start = new Vector3(highPoint.x, highPoint.y, min);
					let end = new Vector3(highPoint.x, highPoint.y, max);

					heightEdge.position.copy(lowPoint);

					heightEdge.geometry.setPositions([
						0, 0, 0,
						...start.clone().sub(lowPoint).toArray(),
						...start.clone().sub(lowPoint).toArray(),
						...end.clone().sub(lowPoint).toArray(),
					]);

					heightEdge.geometry.verticesNeedUpdate = true;
					// heightEdge.geometry.computeLineDistances();
					// heightEdge.geometry.lineDistancesNeedUpdate = true;
					heightEdge.geometry.computeBoundingSphere();
					heightEdge.computeLineDistances();

					// heightEdge.material.dashSize = height / 40;
					// heightEdge.material.gapSize = height / 40;

					let heightLabelPosition = start.clone().add(end).multiplyScalar(0.5);
					this.heightLabel.position.copy(heightLabelPosition);

					let suffix = "";
					if(this.lengthUnit != null && this.lengthUnitDisplay != null){
						height = height / this.lengthUnit.unitspermeter * this.lengthUnitDisplay.unitspermeter;  //convert to meters then to the display unit
						suffix = this.lengthUnitDisplay.code;
					}

					let txtHeight = Utils.addCommas(height.toFixed(2));
					let msg = `${txtHeight} ${suffix}`;
					this.heightLabel.setText(msg);
				}
			}

			{ // update circle stuff
				const circleRadiusLabel = this.circleRadiusLabel;
				const circleRadiusLine = this.circleRadiusLine;
				const circleLine = this.circleLine;
				const circleCenter = this.circleCenter;

				const circleOkay = this.points.length === 3;

				circleRadiusLabel.visible = this.showCircle && circleOkay;
				circleRadiusLine.visible = this.showCircle && circleOkay;
				circleLine.visible = this.showCircle && circleOkay;
				circleCenter.visible = this.showCircle && circleOkay;

				if(this.showCircle && circleOkay){

					const A = this.points[0].position;
					const B = this.points[1].position;
					const C = this.points[2].position;
					const AB = B.clone().sub(A);
					const AC = C.clone().sub(A);
					const N = AC.clone().cross(AB).normalize();

					const center = Potree.Utils.computeCircleCenter(A, B, C);
					const radius = center.distanceTo(A);


					const scale = radius / 20;
					circleCenter.position.copy(center);
					circleCenter.scale.set(scale, scale, scale);

					//circleRadiusLine.geometry.vertices[0].set(0, 0, 0);
					//circleRadiusLine.geometry.vertices[1].copy(B.clone().sub(center));

					circleRadiusLine.geometry.setPositions( [
						0, 0, 0,
						...B.clone().sub(center).toArray()
					] );

					circleRadiusLine.geometry.verticesNeedUpdate = true;
					circleRadiusLine.geometry.computeBoundingSphere();
					circleRadiusLine.position.copy(center);
					circleRadiusLine.computeLineDistances();

					const target = center.clone().add(N);
					circleLine.position.copy(center);
					circleLine.scale.set(radius, radius, radius);
					circleLine.lookAt(target);
					
					circleRadiusLabel.visible = true;
					circleRadiusLabel.position.copy(center.clone().add(B).multiplyScalar(0.5));
					circleRadiusLabel.setText(`${radius.toFixed(3)}`);

				}
			}

			{ // update area label
				this.areaLabel.position.copy(centroid);
				this.areaLabel.visible = this.showArea && this.points.length >= 3;
				let area = this.getArea();

				let suffix = "";
				if(this.lengthUnit != null && this.lengthUnitDisplay != null){
					area = area / Math.pow(this.lengthUnit.unitspermeter, 2) * Math.pow(this.lengthUnitDisplay.unitspermeter, 2);  //convert to square meters then to the square display unit
					suffix = this.lengthUnitDisplay.code;
				}

				let txtArea = Utils.addCommas(area.toFixed(1));
				let msg =  `${txtArea} ${suffix}\u00B2`;
				this.areaLabel.setText(msg);
			}

			// this.updateAzimuth();
		};

		raycast (raycaster, intersects) {
			for (let i = 0; i < this.points.length; i++) {
				let sphere = this.spheres[i];

				sphere.raycast(raycaster, intersects);
			}

			// recalculate distances because they are not necessarely correct
			// for scaled objects.
			// see https://github.com/mrdoob/three.js/issues/5827
			// TODO: remove this once the bug has been fixed
			for (let i = 0; i < intersects.length; i++) {
				let I = intersects[i];
				I.distance = raycaster.ray.origin.distanceTo(I.point);
			}
			intersects.sort(function (a, b) { return a.distance - b.distance; });
		};

		get showCoordinates () {
			return this._showCoordinates;
		}

		set showCoordinates (value) {
			this._showCoordinates = value;
			this.update();
		}

		get showAngles () {
			return this._showAngles;
		}

		set showAngles (value) {
			this._showAngles = value;
			this.update();
		}

		get showCircle () {
			return this._showCircle;
		}

		set showCircle (value) {
			this._showCircle = value;
			this.update();
		}

		get showAzimuth(){
			return this._showAzimuth;
		}

		set showAzimuth(value){
			this._showAzimuth = value;
			this.update();
		}

		get showEdges () {
			return this._showEdges;
		}

		set showEdges (value) {
			this._showEdges = value;
			this.update();
		}

		get showHeight () {
			return this._showHeight;
		}

		set showHeight (value) {
			this._showHeight = value;
			this.update();
		}

		get showArea () {
			return this._showArea;
		}

		set showArea (value) {
			this._showArea = value;
			this.update();
		}

		get closed () {
			return this._closed;
		}

		set closed (value) {
			this._closed = value;
			this.update();
		}

		get showDistances () {
			return this._showDistances;
		}

		set showDistances (value) {
			this._showDistances = value;
			this.update();
		}

	}

	class PolygonClipVolume extends Object3D{
		
		constructor(camera){
			super();

			this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
			this.name = "polygon_clip_volume_" + this.constructor.counter;

			this.camera = camera.clone();
			this.camera.rotation.set(...camera.rotation.toArray()); // [r85] workaround because camera.clone() doesn't work on rotation
			this.camera.rotation.order = camera.rotation.order;
			this.camera.updateMatrixWorld();
			this.camera.updateProjectionMatrix();
			this.camera.matrixWorldInverse.copy(this.camera.matrixWorld).invert();

			this.viewMatrix = this.camera.matrixWorldInverse.clone();
			this.projMatrix = this.camera.projectionMatrix.clone();

			// projected markers
			this.markers = [];
			this.initialized = false;
		}

		addMarker() {

			let marker = new Mesh();

			let cancel;

			let drag = e => {
				let size = e.viewer.renderer.getSize(new Vector2());
				let projectedPos = new Vector3(
					2.0 * (e.drag.end.x / size.width) - 1.0,
					-2.0 * (e.drag.end.y / size.height) + 1.0,
					0
				);

				marker.position.copy(projectedPos);
			};
			
			let drop = e => {	
				cancel();
			};
			
			cancel = e => {
				marker.removeEventListener("drag", drag);
				marker.removeEventListener("drop", drop);
			};
			
			marker.addEventListener("drag", drag);
			marker.addEventListener("drop", drop);


			this.markers.push(marker);
		}

		removeLastMarker() {
			if(this.markers.length > 0) {
				this.markers.splice(this.markers.length - 1, 1);
			}
		}

	};

	class Utils {
		static async loadShapefileFeatures (file, callback) {
			let features = [];

			let handleFinish = () => {
				callback(features);
			};

			let source = await shapefile.open(file);

			while(true){
				let result = await source.read();

				if (result.done) {
					handleFinish();
					break;
				}

				if (result.value && result.value.type === 'Feature' && result.value.geometry !== undefined) {
					features.push(result.value);
				}
			}

		}

		static toString (value) {
			if (value.x != null) {
				return value.x.toFixed(2) + ', ' + value.y.toFixed(2) + ', ' + value.z.toFixed(2);
			} else {
				return '' + value + '';
			}
		}

		static normalizeURL (url) {
			let u = new URL(url);

			return u.protocol + '//' + u.hostname + u.pathname.replace(/\/+/g, '/');
		};

		static pathExists (url) {
			let req = XHRFactory.createXMLHttpRequest();
			req.open('GET', url, false);
			req.send(null);
			if (req.status !== 200) {
				return false;
			}
			return true;
		};

		static debugSphere(parent, position, scale, color){
			let geometry = new SphereGeometry(1, 8, 8);
			let material;

			if(color !== undefined){
				material = new MeshBasicMaterial({color: color});
			}else {
				material = new MeshNormalMaterial();
			}
			let sphere = new Mesh(geometry, material);
			sphere.position.copy(position);
			sphere.scale.set(scale, scale, scale);
			parent.add(sphere);

			return sphere;
		}

		static debugLine(parent, start, end, color){

			let material = new LineBasicMaterial({ color: color }); 
			let geometry = new Geometry();

			const p1 = new Vector3(0, 0, 0);
			const p2 = end.clone().sub(start);

			geometry.vertices.push(p1, p2);

			let tl = new Line( geometry, material );
			tl.position.copy(start);

			parent.add(tl);

			let line = {
				node: tl,
				set: (start, end) => {
					geometry.vertices[0].copy(start);
					geometry.vertices[1].copy(end);
					geometry.verticesNeedUpdate = true;
				},
			};

			return line;
		}

		static debugCircle(parent, center, radius, normal, color){
			let material = new LineBasicMaterial({ color: color });

			let geometry = new Geometry();

			let n = 32;
			for(let i = 0; i <= n; i++){
				let u0 = 2 * Math.PI * (i / n);
				let u1 = 2 * Math.PI * (i + 1) / n;

				let p0 = new Vector3(
					Math.cos(u0), 
					Math.sin(u0), 
					0
				);

				let p1 = new Vector3(
					Math.cos(u1), 
					Math.sin(u1), 
					0
				);

				geometry.vertices.push(p0, p1); 
			}

			let tl = new Line( geometry, material ); 
			tl.position.copy(center);
			tl.scale.set(radius, radius, radius);

			parent.add(tl);
		}

		static debugBox(parent, box, transform = new Matrix4(), color = 0xFFFF00){
			
			let vertices = [
				[box.min.x, box.min.y, box.min.z],
				[box.min.x, box.min.y, box.max.z],
				[box.min.x, box.max.y, box.min.z],
				[box.min.x, box.max.y, box.max.z],

				[box.max.x, box.min.y, box.min.z],
				[box.max.x, box.min.y, box.max.z],
				[box.max.x, box.max.y, box.min.z],
				[box.max.x, box.max.y, box.max.z],
			].map(v => new Vector3(...v));

			let edges = [
				[0, 4], [4, 5], [5, 1], [1, 0],
				[2, 6], [6, 7], [7, 3], [3, 2],
				[0, 2], [4, 6], [5, 7], [1, 3]
			];

			let center = box.getCenter(new Vector3());

			let centroids = [
				{position: [box.min.x, center.y, center.z], color: 0xFF0000},
				{position: [box.max.x, center.y, center.z], color: 0x880000},

				{position: [center.x, box.min.y, center.z], color: 0x00FF00},
				{position: [center.x, box.max.y, center.z], color: 0x008800},

				{position: [center.x, center.y, box.min.z], color: 0x0000FF},
				{position: [center.x, center.y, box.max.z], color: 0x000088},
			];

			for(let vertex of vertices){
				let pos = vertex.clone().applyMatrix4(transform);

				Utils.debugSphere(parent, pos, 0.1, 0xFF0000);
			}

			for(let edge of edges){
				let start = vertices[edge[0]].clone().applyMatrix4(transform);
				let end = vertices[edge[1]].clone().applyMatrix4(transform);

				Utils.debugLine(parent, start, end, color);
			}

			for(let centroid of centroids){
				let pos = new Vector3(...centroid.position).applyMatrix4(transform);

				Utils.debugSphere(parent, pos, 0.1, centroid.color);
			}
		}

		static debugPlane(parent, plane, size = 1, color = 0x0000FF){

			let planehelper = new PlaneHelper(plane, size, color);

			parent.add(planehelper);

		}

		/**
		 * adapted from mhluska at https://github.com/mrdoob/three.js/issues/1561
		 */
		static computeTransformedBoundingBox (box, transform) {
			let vertices = [
				new Vector3(box.min.x, box.min.y, box.min.z).applyMatrix4(transform),
				new Vector3(box.min.x, box.min.y, box.min.z).applyMatrix4(transform),
				new Vector3(box.max.x, box.min.y, box.min.z).applyMatrix4(transform),
				new Vector3(box.min.x, box.max.y, box.min.z).applyMatrix4(transform),
				new Vector3(box.min.x, box.min.y, box.max.z).applyMatrix4(transform),
				new Vector3(box.min.x, box.max.y, box.max.z).applyMatrix4(transform),
				new Vector3(box.max.x, box.max.y, box.min.z).applyMatrix4(transform),
				new Vector3(box.max.x, box.min.y, box.max.z).applyMatrix4(transform),
				new Vector3(box.max.x, box.max.y, box.max.z).applyMatrix4(transform)
			];

			let boundingBox = new Box3();
			boundingBox.setFromPoints(vertices);

			return boundingBox;
		};

		/**
		 * add separators to large numbers
		 *
		 * @param nStr
		 * @returns
		 */
		static addCommas (nStr) {
			nStr += '';
			let x = nStr.split('.');
			let x1 = x[0];
			let x2 = x.length > 1 ? '.' + x[1] : '';
			let rgx = /(\d+)(\d{3})/;
			while (rgx.test(x1)) {
				x1 = x1.replace(rgx, '$1' + ',' + '$2');
			}
			return x1 + x2;
		};

		static removeCommas (str) {
			return str.replace(/,/g, '');
		}

		/**
		 * create worker from a string
		 *
		 * code from http://stackoverflow.com/questions/10343913/how-to-create-a-web-worker-from-a-string
		 */
		static createWorker (code) {
			let blob = new Blob([code], {type: 'application/javascript'});
			let worker = new Worker(URL.createObjectURL(blob));

			return worker;
		};

		static moveTo(scene, endPosition, endTarget){

			let view = scene.view;
			let camera = scene.getActiveCamera();
			let animationDuration = 500;
			let easing = TWEEN.Easing.Quartic.Out;

			{ // animate camera position
				let tween = new TWEEN.Tween(view.position).to(endPosition, animationDuration);
				tween.easing(easing);
				tween.start();
			}

			{ // animate camera target
				let camTargetDistance = camera.position.distanceTo(endTarget);
				let target = new Vector3().addVectors(
					camera.position,
					camera.getWorldDirection(new Vector3()).clone().multiplyScalar(camTargetDistance)
				);
				let tween = new TWEEN.Tween(target).to(endTarget, animationDuration);
				tween.easing(easing);
				tween.onUpdate(() => {
					view.lookAt(target);
				});
				tween.onComplete(() => {
					view.lookAt(target);
				});
				tween.start();
			}

		}

		static loadSkybox (path) {
			let parent = new Object3D("skybox_root");

			let camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 100000);
			camera.up.set(0, 0, 1);
			let scene = new Scene();

			let format = '.jpg';
			let urls = [
				path + 'px' + format, path + 'nx' + format,
				path + 'py' + format, path + 'ny' + format,
				path + 'pz' + format, path + 'nz' + format
			];

			let materialArray = [];
			{
				for (let i = 0; i < 6; i++) {
					let material = new MeshBasicMaterial({
						map: null,
						side: BackSide,
						depthTest: false,
						depthWrite: false,
						color: 0x424556
					});

					materialArray.push(material);

					let loader = new TextureLoader();
					loader.load(urls[i],
						function loaded (texture) {
							material.map = texture;
							material.needsUpdate = true;
							material.color.setHex(0xffffff);
						}, function progress (xhr) {
							// console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
						}, function error (xhr) {
							console.log('An error happened', xhr);
						}
					);
				}
			}

			let skyGeometry = new BoxGeometry(700, 700, 700);
			let skybox = new Mesh(skyGeometry, materialArray);

			scene.add(skybox);

			scene.traverse(n => n.frustumCulled = false);

			// z up
			scene.rotation.x = Math.PI / 2;

			parent.children.push(camera);
			camera.parent = parent;

			return {camera, scene, parent};
		};

		static createGrid (width, length, spacing, color) {
			let material = new LineBasicMaterial({
				color: color || 0x888888
			});

			let geometry = new Geometry();
			for (let i = 0; i <= length; i++) {
				geometry.vertices.push(new Vector3(-(spacing * width) / 2, i * spacing - (spacing * length) / 2, 0));
				geometry.vertices.push(new Vector3(+(spacing * width) / 2, i * spacing - (spacing * length) / 2, 0));
			}

			for (let i = 0; i <= width; i++) {
				geometry.vertices.push(new Vector3(i * spacing - (spacing * width) / 2, -(spacing * length) / 2, 0));
				geometry.vertices.push(new Vector3(i * spacing - (spacing * width) / 2, +(spacing * length) / 2, 0));
			}

			let line = new LineSegments(geometry, material, LinePieces);
			line.receiveShadow = true;
			return line;
		}

		static createBackgroundTexture (width, height) {
			function gauss (x, y) {
				return (1 / (2 * Math.PI)) * Math.exp(-(x * x + y * y) / 2);
			};

			// map.magFilter = THREE.NearestFilter;
			let size = width * height;
			let data = new Uint8Array(3 * size);

			let chroma = [1, 1.5, 1.7];
			let max = gauss(0, 0);

			for (let x = 0; x < width; x++) {
				for (let y = 0; y < height; y++) {
					let u = 2 * (x / width) - 1;
					let v = 2 * (y / height) - 1;

					let i = x + width * y;
					let d = gauss(2 * u, 2 * v) / max;
					let r = (Math.random() + Math.random() + Math.random()) / 3;
					r = (d * 0.5 + 0.5) * r * 0.03;
					r = r * 0.4;

					// d = Math.pow(d, 0.6);

					data[3 * i + 0] = 255 * (d / 15 + 0.05 + r) * chroma[0];
					data[3 * i + 1] = 255 * (d / 15 + 0.05 + r) * chroma[1];
					data[3 * i + 2] = 255 * (d / 15 + 0.05 + r) * chroma[2];
				}
			}

			let texture = new DataTexture(data, width, height, RGBFormat);
			texture.needsUpdate = true;

			return texture;
		}

		static getMousePointCloudIntersection (mouse, camera, viewer, pointclouds, params = {}) {
			
			let renderer = viewer.renderer;
			
			let nmouse = {
				x: (mouse.x / renderer.domElement.clientWidth) * 2 - 1,
				y: -(mouse.y / renderer.domElement.clientHeight) * 2 + 1
			};

			let pickParams = {};

			if(params.pickClipped){
				pickParams.pickClipped = params.pickClipped;
			}

			pickParams.x = mouse.x;
			pickParams.y = renderer.domElement.clientHeight - mouse.y;

			let raycaster = new Raycaster();
			raycaster.setFromCamera(nmouse, camera);
			let ray = raycaster.ray;

			let selectedPointcloud = null;
			let closestDistance = Infinity;
			let closestIntersection = null;
			let closestPoint = null;
			
			for(let pointcloud of pointclouds){
				let point = pointcloud.pick(viewer, camera, ray, pickParams);
				
				if(!point){
					continue;
				}

				let distance = camera.position.distanceTo(point.position);

				if (distance < closestDistance) {
					closestDistance = distance;
					selectedPointcloud = pointcloud;
					closestIntersection = point.position;
					closestPoint = point;
				}
			}

			if (selectedPointcloud) {
				return {
					location: closestIntersection,
					distance: closestDistance,
					pointcloud: selectedPointcloud,
					point: closestPoint
				};
			} else {
				return null;
			}
		}

		static pixelsArrayToImage (pixels, width, height) {
			let canvas = document.createElement('canvas');
			canvas.width = width;
			canvas.height = height;

			let context = canvas.getContext('2d');

			pixels = new pixels.constructor(pixels);

			for (let i = 0; i < pixels.length; i++) {
				pixels[i * 4 + 3] = 255;
			}

			let imageData = context.createImageData(width, height);
			imageData.data.set(pixels);
			context.putImageData(imageData, 0, 0);

			let img = new Image();
			img.src = canvas.toDataURL();
			// img.style.transform = "scaleY(-1)";

			return img;
		}

		static pixelsArrayToDataUrl(pixels, width, height) {
			let canvas = document.createElement('canvas');
			canvas.width = width;
			canvas.height = height;

			let context = canvas.getContext('2d');

			pixels = new pixels.constructor(pixels);

			for (let i = 0; i < pixels.length; i++) {
				pixels[i * 4 + 3] = 255;
			}

			let imageData = context.createImageData(width, height);
			imageData.data.set(pixels);
			context.putImageData(imageData, 0, 0);

			let dataURL = canvas.toDataURL();

			return dataURL;
		}

		static pixelsArrayToCanvas(pixels, width, height){
			let canvas = document.createElement('canvas');
			canvas.width = width;
			canvas.height = height;

			let context = canvas.getContext('2d');

			pixels = new pixels.constructor(pixels);

			//for (let i = 0; i < pixels.length; i++) {
			//	pixels[i * 4 + 3] = 255;
			//}

			// flip vertically
			let bytesPerLine = width * 4;
			for(let i = 0; i < parseInt(height / 2); i++){
				let j = height - i - 1;

				let lineI = pixels.slice(i * bytesPerLine, i * bytesPerLine + bytesPerLine);
				let lineJ = pixels.slice(j * bytesPerLine, j * bytesPerLine + bytesPerLine);
				pixels.set(lineJ, i * bytesPerLine);
				pixels.set(lineI, j * bytesPerLine);
			}

			let imageData = context.createImageData(width, height);
			imageData.data.set(pixels);
			context.putImageData(imageData, 0, 0);

			return canvas;
		}

		static removeListeners(dispatcher, type){
			if (dispatcher._listeners === undefined) {
				return;
			}

			if (dispatcher._listeners[ type ]) {
				delete dispatcher._listeners[ type ];
			}
		}

		static mouseToRay(mouse, camera, width, height){

			let normalizedMouse = {
				x: (mouse.x / width) * 2 - 1,
				y: -(mouse.y / height) * 2 + 1
			};

			let vector = new Vector3(normalizedMouse.x, normalizedMouse.y, 0.5);
			let origin = camera.position.clone();
			vector.unproject(camera);
			let direction = new Vector3().subVectors(vector, origin).normalize();

			let ray = new Ray(origin, direction);

			return ray;
		}

		static projectedRadius(radius, camera, distance, screenWidth, screenHeight){
			if(camera instanceof OrthographicCamera){
				return Utils.projectedRadiusOrtho(radius, camera.projectionMatrix, screenWidth, screenHeight);
			}else if(camera instanceof PerspectiveCamera){
				return Utils.projectedRadiusPerspective(radius, camera.fov * Math.PI / 180, distance, screenHeight);
			}else {
				throw new Error("invalid parameters");
			}
		}

		static projectedRadiusPerspective(radius, fov, distance, screenHeight) {
			let projFactor = (1 / Math.tan(fov / 2)) / distance;
			projFactor = projFactor * screenHeight / 2;

			return radius * projFactor;
		}

		static projectedRadiusOrtho(radius, proj, screenWidth, screenHeight) {
			let p1 = new Vector4(0);
			let p2 = new Vector4(radius);

			p1.applyMatrix4(proj);
			p2.applyMatrix4(proj);
			p1 = new Vector3(p1.x, p1.y, p1.z);
			p2 = new Vector3(p2.x, p2.y, p2.z);
			p1.x = (p1.x + 1.0) * 0.5 * screenWidth;
			p1.y = (p1.y + 1.0) * 0.5 * screenHeight;
			p2.x = (p2.x + 1.0) * 0.5 * screenWidth;
			p2.y = (p2.y + 1.0) * 0.5 * screenHeight;
			return p1.distanceTo(p2);
		}
			
			
		static topView(camera, node){
			camera.position.set(0, 1, 0);
			camera.rotation.set(-Math.PI / 2, 0, 0);
			camera.zoomTo(node, 1);
		}

		static frontView (camera, node) {
			camera.position.set(0, 0, 1);
			camera.rotation.set(0, 0, 0);
			camera.zoomTo(node, 1);
		}

		static leftView (camera, node) {
			camera.position.set(-1, 0, 0);
			camera.rotation.set(0, -Math.PI / 2, 0);
			camera.zoomTo(node, 1);
		}

		static rightView (camera, node) {
			camera.position.set(1, 0, 0);
			camera.rotation.set(0, Math.PI / 2, 0);
			camera.zoomTo(node, 1);
		}

		
		static findClosestGpsTime(target, viewer){
			const start = performance.now();

			const nodes = [];
			for(const pc of viewer.scene.pointclouds){
				nodes.push(pc.root);

				for(const child of pc.root.children){
					if(child){
						nodes.push(child);
					}
				}
			}

			let closestNode = null;
			let closestIndex = Infinity;
			let closestDistance = Infinity;
			let closestValue = 0;

			for(const node of nodes){

				const isOkay = node.geometryNode != null 
					&& node.geometryNode.geometry != null
					&& node.sceneNode != null;

				if(!isOkay){
					continue;
				}

				let geometry = node.geometryNode.geometry;
				let gpsTime = geometry.attributes["gps-time"];
				let range = gpsTime.potree.range;

				for(let i = 0; i < gpsTime.array.length; i++){
					let value = gpsTime.array[i];
					value = value * (range[1] - range[0]) + range[0];
					const distance = Math.abs(target - value);

					if(distance < closestDistance){
						closestIndex = i;
						closestDistance = distance;
						closestValue = value;
						closestNode = node;
						//console.log("found a closer one: " + value);
					}
				}
			}

			const geometry = closestNode.geometryNode.geometry;
			const position = new Vector3(
				geometry.attributes.position.array[3 * closestIndex + 0],
				geometry.attributes.position.array[3 * closestIndex + 1],
				geometry.attributes.position.array[3 * closestIndex + 2],
			);

			position.applyMatrix4(closestNode.sceneNode.matrixWorld);

			const end = performance.now();
			const duration = (end - start);
			console.log(`duration: ${duration.toFixed(3)}ms`);

			return {
				node: closestNode,
				index: closestIndex,
				position: position,
			};
		}

		/**
		 *
		 * 0: no intersection
		 * 1: intersection
		 * 2: fully inside
		 */
		static frustumSphereIntersection (frustum, sphere) {
			let planes = frustum.planes;
			let center = sphere.center;
			let negRadius = -sphere.radius;

			let minDistance = Number.MAX_VALUE;

			for (let i = 0; i < 6; i++) {
				let distance = planes[ i ].distanceToPoint(center);

				if (distance < negRadius) {
					return 0;
				}

				minDistance = Math.min(minDistance, distance);
			}

			return (minDistance >= sphere.radius) ? 2 : 1;
		}

		// code taken from three.js
		// ImageUtils - generateDataTexture()
		static generateDataTexture (width, height, color) {
			let size = width * height;
			let data = new Uint8Array(4 * width * height);

			let r = Math.floor(color.r * 255);
			let g = Math.floor(color.g * 255);
			let b = Math.floor(color.b * 255);

			for (let i = 0; i < size; i++) {
				data[ i * 3 ] = r;
				data[ i * 3 + 1 ] = g;
				data[ i * 3 + 2 ] = b;
			}

			let texture = new DataTexture(data, width, height, RGBAFormat);
			texture.needsUpdate = true;
			texture.magFilter = NearestFilter;

			return texture;
		}

		// from http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
		static getParameterByName (name) {
			name = name.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
			let regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
			let results = regex.exec(document.location.search);
			return results === null ? null : decodeURIComponent(results[1].replace(/\+/g, ' '));
		}

		static setParameter (name, value) {
			// value = encodeURIComponent(value);

			name = name.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
			let regex = new RegExp('([\\?&])(' + name + '=([^&#]*))');
			let results = regex.exec(document.location.search);

			let url = window.location.href;
			if (results === null) {
				if (window.location.search.length === 0) {
					url = url + '?';
				} else {
					url = url + '&';
				}

				url = url + name + '=' + value;
			} else {
				let newValue = name + '=' + value;
				url = url.replace(results[2], newValue);
			}
			window.history.replaceState({}, '', url);
		}

		static createChildAABB(aabb, index){
			let min = aabb.min.clone();
			let max = aabb.max.clone();
			let size = new Vector3().subVectors(max, min);

			if ((index & 0b0001) > 0) {
				min.z += size.z / 2;
			} else {
				max.z -= size.z / 2;
			}

			if ((index & 0b0010) > 0) {
				min.y += size.y / 2;
			} else {
				max.y -= size.y / 2;
			}

			if ((index & 0b0100) > 0) {
				min.x += size.x / 2;
			} else {
				max.x -= size.x / 2;
			}

			return new Box3(min, max);
		}

		// see https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript
		static clipboardCopy(text){
			let textArea = document.createElement("textarea");

			textArea.style.position = 'fixed';
			textArea.style.top = 0;
			textArea.style.left = 0;

			textArea.style.width = '2em';
			textArea.style.height = '2em';

			textArea.style.padding = 0;

			textArea.style.border = 'none';
			textArea.style.outline = 'none';
			textArea.style.boxShadow = 'none';

			textArea.style.background = 'transparent';

			textArea.value = text;

			document.body.appendChild(textArea);

			textArea.select();

			 try {
				let success = document.execCommand('copy');
				if(success){
					console.log("copied text to clipboard");
				}else {
					console.log("copy to clipboard failed");
				}
			} catch (err) {
				console.log("error while trying to copy to clipboard");
			}

			document.body.removeChild(textArea);

		}

		static getMeasurementIcon(measurement){
			if (measurement instanceof Measure) {
				if (measurement.showDistances && !measurement.showArea && !measurement.showAngles) {
					return `${Potree.resourcePath}/icons/distance.svg`;
				} else if (measurement.showDistances && measurement.showArea && !measurement.showAngles) {
					return `${Potree.resourcePath}/icons/area.svg`;
				} else if (measurement.maxMarkers === 1) {
					return `${Potree.resourcePath}/icons/point.svg`;
				} else if (!measurement.showDistances && !measurement.showArea && measurement.showAngles) {
					return `${Potree.resourcePath}/icons/angle.png`;
				} else if (measurement.showHeight) {
					return `${Potree.resourcePath}/icons/height.svg`;
				} else {
					return `${Potree.resourcePath}/icons/distance.svg`;
				}
			} else if (measurement instanceof Profile) {
				return `${Potree.resourcePath}/icons/profile.svg`;
			} else if (measurement instanceof Volume) {
				return `${Potree.resourcePath}/icons/volume.svg`;
			} else if (measurement instanceof PolygonClipVolume) {
				return `${Potree.resourcePath}/icons/clip-polygon.svg`;
			}
		}

		static lineToLineIntersection(P0, P1, P2, P3){

			const P = [P0, P1, P2, P3];

			const d = (m, n, o, p) => {
				let result =  
					  (P[m].x - P[n].x) * (P[o].x - P[p].x)
					+ (P[m].y - P[n].y) * (P[o].y - P[p].y)
					+ (P[m].z - P[n].z) * (P[o].z - P[p].z);

				return result;
			};


			const mua = (d(0, 2, 3, 2) * d(3, 2, 1, 0) - d(0, 2, 1, 0) * d(3, 2, 3, 2))
			        /**-----------------------------------------------------------------**/ /
			            (d(1, 0, 1, 0) * d(3, 2, 3, 2) - d(3, 2, 1, 0) * d(3, 2, 1, 0));


			const mub = (d(0, 2, 3, 2) + mua * d(3, 2, 1, 0))
			        /**--------------------------------------**/ /
			                       d(3, 2, 3, 2);


			const P01 = P1.clone().sub(P0);
			const P23 = P3.clone().sub(P2);
			
			const Pa = P0.clone().add(P01.multiplyScalar(mua));
			const Pb = P2.clone().add(P23.multiplyScalar(mub));

			const center = Pa.clone().add(Pb).multiplyScalar(0.5);

			return center;
		}

		static computeCircleCenter(A, B, C){
			const AB = B.clone().sub(A);
			const AC = C.clone().sub(A);

			const N = AC.clone().cross(AB).normalize();

			const ab_dir = AB.clone().cross(N).normalize();
			const ac_dir = AC.clone().cross(N).normalize();

			const ab_origin = A.clone().add(B).multiplyScalar(0.5);
			const ac_origin = A.clone().add(C).multiplyScalar(0.5);

			const P0 = ab_origin;
			const P1 = ab_origin.clone().add(ab_dir);

			const P2 = ac_origin;
			const P3 = ac_origin.clone().add(ac_dir);

			const center = Utils.lineToLineIntersection(P0, P1, P2, P3);

			return center;

			// Potree.Utils.debugLine(viewer.scene.scene, P0, P1, 0x00ff00);
			// Potree.Utils.debugLine(viewer.scene.scene, P2, P3, 0x0000ff);

			// Potree.Utils.debugSphere(viewer.scene.scene, center, 0.03, 0xff00ff);

			// const radius = center.distanceTo(A);
			// Potree.Utils.debugCircle(viewer.scene.scene, center, radius, new THREE.Vector3(0, 0, 1), 0xff00ff);
		}

		static getNorthVec(p1, distance, projection){
			if(projection){
				// if there is a projection, transform coordinates to WGS84
				// and compute angle to north there

				proj4.defs("pointcloud", projection);
				const transform = proj4("pointcloud", "WGS84");

				const llP1 = transform.forward(p1.toArray());
				let llP2 = transform.forward([p1.x, p1.y + distance]);
				const polarRadius = Math.sqrt((llP2[0] - llP1[0]) ** 2 + (llP2[1] - llP1[1]) ** 2);
				llP2 = [llP1[0], llP1[1] + polarRadius];

				const northVec = transform.inverse(llP2);
				
				return new Vector3(...northVec, p1.z).sub(p1);
			}else {
				// if there is no projection, assume [0, 1, 0] as north direction

				const vec = new Vector3(0, 1, 0).multiplyScalar(distance);
				
				return vec;
			}
		}

		static computeAzimuth(p1, p2, projection){

			let azimuth = 0;

			if(projection){
				// if there is a projection, transform coordinates to WGS84
				// and compute angle to north there

				let transform;

				if (projection.includes('EPSG')) {
					transform = proj4(projection, "WGS84");
				} else {
					proj4.defs("pointcloud", projection);
					transform = proj4("pointcloud", "WGS84");
				}

				const llP1 = transform.forward(p1.toArray());
				const llP2 = transform.forward(p2.toArray());
				const dir = [
					llP2[0] - llP1[0],
					llP2[1] - llP1[1],
				];
				azimuth = Math.atan2(dir[1], dir[0]) - Math.PI / 2;
			}else {
				// if there is no projection, assume [0, 1, 0] as north direction

				const dir = [p2.x - p1.x, p2.y - p1.y];
				azimuth = Math.atan2(dir[1], dir[0]) - Math.PI / 2;
			}

			// make clockwise
			azimuth = -azimuth;

			return azimuth;
		}

		static async loadScript(url){

			return new Promise( resolve => {

				const element = document.getElementById(url);

				if(element){
					resolve();
				}else {
					const script = document.createElement("script");

					script.id = url;

					script.onload = () => {
						resolve();
					};
					script.src = url;

					document.body.appendChild(script);
				}
			});
		}

		static createSvgGradient(scheme){

			// this is what we are creating:
			//
			//<svg width="1em" height="3em"  xmlns="http://www.w3.org/2000/svg">
			//	<defs>
			//		<linearGradient id="gradientID" gradientTransform="rotate(90)">
			//		<stop offset="0%"  stop-color="rgb(93, 78, 162)" />
			//		...
			//		<stop offset="100%"  stop-color="rgb(157, 0, 65)" />
			//		</linearGradient>
			//	</defs>
			//	
			//	<rect width="100%" height="100%" fill="url('#myGradient')" stroke="black" stroke-width="0.1em"/>
			//</svg>


			const gradientId = `${Math.random()}_${Date.now()}`;
			
			const svgn = "http://www.w3.org/2000/svg";
			const svg = document.createElementNS(svgn, "svg");
			svg.setAttributeNS(null, "width", "2em");
			svg.setAttributeNS(null, "height", "3em");
			
			{ // <defs>
				const defs = document.createElementNS(svgn, "defs");
				
				const linearGradient = document.createElementNS(svgn, "linearGradient");
				linearGradient.setAttributeNS(null, "id", gradientId);
				linearGradient.setAttributeNS(null, "gradientTransform", "rotate(90)");

				for(let i = scheme.length - 1; i >= 0; i--){
					const stopVal = scheme[i];
					const percent = parseInt(100 - stopVal[0] * 100);
					const [r, g, b] = stopVal[1].toArray().map(v => parseInt(v * 255));

					const stop = document.createElementNS(svgn, "stop");
					stop.setAttributeNS(null, "offset", `${percent}%`);
					stop.setAttributeNS(null, "stop-color", `rgb(${r}, ${g}, ${b})`);

					linearGradient.appendChild(stop);
				}

				defs.appendChild(linearGradient);
				svg.appendChild(defs);
			}

			const rect = document.createElementNS(svgn, "rect");
			rect.setAttributeNS(null, "width", `100%`);
			rect.setAttributeNS(null, "height", `100%`);
			rect.setAttributeNS(null, "fill", `url("#${gradientId}")`);
			rect.setAttributeNS(null, "stroke", `black`);
			rect.setAttributeNS(null, "stroke-width", `0.1em`);

			svg.appendChild(rect);
			
			return svg;
		}

		static async waitAny(promises){
			
			return new Promise( (resolve) => {

				promises.map( promise => {
					promise.then( () => {
						resolve();
					});
				});

			});

		}

	}

	Utils.screenPass = new function () {
		this.screenScene = new Scene();
		this.screenQuad = new Mesh(new PlaneBufferGeometry(2, 2, 1));
		this.screenQuad.material.depthTest = true;
		this.screenQuad.material.depthWrite = true;
		this.screenQuad.material.transparent = true;
		this.screenScene.add(this.screenQuad);
		this.camera = new Camera();

		this.render = function (renderer, material, target) {
			this.screenQuad.material = material;

			if (typeof target === 'undefined') {
				renderer.render(this.screenScene, this.camera);
			} else {
				renderer.render(this.screenScene, this.camera, target);
			}
		};
	}();

	class Annotation extends EventDispatcher {
		constructor (args = {}) {
			super();

			this.scene = null;
			this._title = args.title || 'No Title';
			this._description = args.description || '';
			this.offset = new Vector3();
			this.uuid = MathUtils.generateUUID();

			if (!args.position) {
				this.position = null;
			} else if (args.position.x != null) {
				this.position = args.position;
			} else {
				this.position = new Vector3(...args.position);
			}

			this.cameraPosition = (args.cameraPosition instanceof Array)
				? new Vector3().fromArray(args.cameraPosition) : args.cameraPosition;
			this.cameraTarget = (args.cameraTarget instanceof Array)
				? new Vector3().fromArray(args.cameraTarget) : args.cameraTarget;
			this.radius = args.radius;
			this.view = args.view || null;
			this.keepOpen = false;
			this.descriptionVisible = false;
			this.showDescription = true;
			this.actions = args.actions || [];
			this.isHighlighted = false;
			this._visible = true;
			this.__visible = true;
			this._display = true;
			this._expand = false;
			this.collapseThreshold = [args.collapseThreshold, 100].find(e => e !== undefined);

			this.children = [];
			this.parent = null;
			this.boundingBox = new Box3();

			let iconClose = exports.resourcePath + '/icons/close.svg';

			this.domElement = $(`
			<div class="annotation" oncontextmenu="return false;">
				<div class="annotation-titlebar">
					<span class="annotation-label"></span>
				</div>
				<div class="annotation-description">
					<span class="annotation-description-close">
						<img src="${iconClose}" width="16px">
					</span>
					<span class="annotation-description-content">${this._description}</span>
				</div>
			</div>
		`);

			this.elTitlebar = this.domElement.find('.annotation-titlebar');
			this.elTitle = this.elTitlebar.find('.annotation-label');
			this.elTitle.append(this._title);
			this.elDescription = this.domElement.find('.annotation-description');
			this.elDescriptionClose = this.elDescription.find('.annotation-description-close');
			// this.elDescriptionContent = this.elDescription.find(".annotation-description-content");

			this.clickTitle = () => {
				if(this.hasView()){
					this.moveHere(this.scene.getActiveCamera());
				}
				this.dispatchEvent({type: 'click', target: this});
			};

			this.elTitle.click(this.clickTitle);

			this.actions = this.actions.map(a => {
				if (a instanceof Action) {
					return a;
				} else {
					return new Action(a);
				}
			});

			for (let action of this.actions) {
				action.pairWith(this);
			}

			let actions = this.actions.filter(
				a => a.showIn === undefined || a.showIn.includes('scene'));

			for (let action of actions) {
				let elButton = $(`<img src="${action.icon}" class="annotation-action-icon">`);
				this.elTitlebar.append(elButton);
				elButton.click(() => action.onclick({annotation: this}));
			}

			this.elDescriptionClose.hover(
				e => this.elDescriptionClose.css('opacity', '1'),
				e => this.elDescriptionClose.css('opacity', '0.5')
			);
			this.elDescriptionClose.click(e => this.setHighlighted(false));
			// this.elDescriptionContent.html(this._description);

			this.domElement.mouseenter(e => this.setHighlighted(true));
			this.domElement.mouseleave(e => this.setHighlighted(false));

			this.domElement.on('touchstart', e => {
				this.setHighlighted(!this.isHighlighted);
			});

			this.display = false;
			//this.display = true;

		}

		installHandles(viewer){
			if(this.handles !== undefined){
				return;
			}

			let domElement = $(`
			<div style="position: absolute; left: 300; top: 200; pointer-events: none">
				<svg width="300" height="600">
					<line x1="0" y1="0" x2="1200" y2="200" style="stroke: black; stroke-width:2" />
					<circle cx="50" cy="50" r="4" stroke="black" stroke-width="2" fill="gray" />
					<circle cx="150" cy="50" r="4" stroke="black" stroke-width="2" fill="gray" />
				</svg>
			</div>
		`);
			
			let svg = domElement.find("svg")[0];
			let elLine = domElement.find("line")[0];
			let elStart = domElement.find("circle")[0];
			let elEnd = domElement.find("circle")[1];

			let setCoordinates = (start, end) => {
				elStart.setAttribute("cx", `${start.x}`);
				elStart.setAttribute("cy", `${start.y}`);

				elEnd.setAttribute("cx", `${end.x}`);
				elEnd.setAttribute("cy", `${end.y}`);

				elLine.setAttribute("x1", start.x);
				elLine.setAttribute("y1", start.y);
				elLine.setAttribute("x2", end.x);
				elLine.setAttribute("y2", end.y);

				let box = svg.getBBox();
				svg.setAttribute("width", `${box.width}`);
				svg.setAttribute("height", `${box.height}`);
				svg.setAttribute("viewBox", `${box.x} ${box.y} ${box.width} ${box.height}`);

				let ya = start.y - end.y;
				let xa = start.x - end.x;

				if(ya > 0){
					start.y = start.y - ya;
				}
				if(xa > 0){
					start.x = start.x - xa;
				}

				domElement.css("left", `${start.x}px`);
				domElement.css("top", `${start.y}px`);

			};

			$(viewer.renderArea).append(domElement);


			let annotationStartPos = this.position.clone();
			let annotationStartOffset = this.offset.clone();

			$(this.domElement).draggable({
				start: (event, ui) => {
					annotationStartPos = this.position.clone();
					annotationStartOffset = this.offset.clone();
					$(this.domElement).find(".annotation-titlebar").css("pointer-events", "none");

					console.log($(this.domElement).find(".annotation-titlebar"));
				},
				stop: () => {
					$(this.domElement).find(".annotation-titlebar").css("pointer-events", "");
				},
				drag: (event, ui ) => {
					let renderAreaWidth = viewer.renderer.getSize(new Vector2()).width;
					//let renderAreaHeight = viewer.renderer.getSize().height;

					let diff = {
						x: ui.originalPosition.left - ui.position.left, 
						y: ui.originalPosition.top - ui.position.top
					};

					let nDiff = {
						x: -(diff.x / renderAreaWidth) * 2,
						y: (diff.y / renderAreaWidth) * 2
					};

					let camera = viewer.scene.getActiveCamera();
					let oldScreenPos = new Vector3()
						.addVectors(annotationStartPos, annotationStartOffset)
						.project(camera);

					let newScreenPos = oldScreenPos.clone();
					newScreenPos.x += nDiff.x;
					newScreenPos.y += nDiff.y;

					let newPos = newScreenPos.clone();
					newPos.unproject(camera);

					let newOffset = new Vector3().subVectors(newPos, this.position);
					this.offset.copy(newOffset);
				}
			});

			let updateCallback = () => {
				let position = this.position;
				let scene = viewer.scene;

				const renderAreaSize = viewer.renderer.getSize(new Vector2());
				let renderAreaWidth = renderAreaSize.width;
				let renderAreaHeight = renderAreaSize.height;

				let start = this.position.clone();
				let end = new Vector3().addVectors(this.position, this.offset);

				let toScreen = (position) => {
					let camera = scene.getActiveCamera();
					let screenPos = new Vector3();

					let worldView = new Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
					let ndc = new Vector4(position.x, position.y, position.z, 1.0).applyMatrix4(worldView);
					// limit w to small positive value, in case position is behind the camera
					ndc.w = Math.max(ndc.w, 0.1);
					ndc.divideScalar(ndc.w);

					screenPos.copy(ndc);
					screenPos.x = renderAreaWidth * (screenPos.x + 1) / 2;
					screenPos.y = renderAreaHeight * (1 - (screenPos.y + 1) / 2);

					return screenPos;
				};
				
				start = toScreen(start);
				end = toScreen(end);

				setCoordinates(start, end);

			};

			viewer.addEventListener("update", updateCallback);

			this.handles = {
				domElement: domElement,
				setCoordinates: setCoordinates,
				updateCallback: updateCallback
			};
		}

		removeHandles(viewer){
			if(this.handles === undefined){
				return;
			}

			//$(viewer.renderArea).remove(this.handles.domElement);
			this.handles.domElement.remove();
			viewer.removeEventListener("update", this.handles.updateCallback);

			delete this.handles;
		}

		get visible () {
			return this._visible;
		}

		set visible (value) {
			if (this._visible === value) {
				return;
			}

			this._visible = value;

			//this.traverse(node => {
			//	node.display = value;
			//});

			this.dispatchEvent({
				type: 'visibility_changed',
				annotation: this
			});
		}

		get display () {
			return this._display;
		}

		set display (display) {
			if (this._display === display) {
				return;
			}

			this._display = display;

			if (display) {
				// this.domElement.fadeIn(200);
				this.domElement.show();
			} else {
				// this.domElement.fadeOut(200);
				this.domElement.hide();
			}
		}

		get expand () {
			return this._expand;
		}

		set expand (expand) {
			if (this._expand === expand) {
				return;
			}

			if (expand) {
				this.display = false;
			} else {
				this.display = true;
				this.traverseDescendants(node => {
					node.display = false;
				});
			}

			this._expand = expand;
		}

		get title () {
			return this._title;
		}

		set title (title) {
			if (this._title === title) {
				return;
			}

			this._title = title;
			this.elTitle.empty();
			this.elTitle.append(this._title);

			this.dispatchEvent({
				type: "annotation_changed",
				annotation: this,
			});
		}

		get description () {
			return this._description;
		}

		set description (description) {
			if (this._description === description) {
				return;
			}

			this._description = description;

			const elDescriptionContent = this.elDescription.find(".annotation-description-content");
			elDescriptionContent.empty();
			elDescriptionContent.append(this._description);

			this.dispatchEvent({
				type: "annotation_changed",
				annotation: this,
			});
		}

		add (annotation) {
			if (!this.children.includes(annotation)) {
				this.children.push(annotation);
				annotation.parent = this;

				let descendants = [];
				annotation.traverse(a => { descendants.push(a); });

				for (let descendant of descendants) {
					let c = this;
					while (c !== null) {
						c.dispatchEvent({
							'type': 'annotation_added',
							'annotation': descendant
						});
						c = c.parent;
					}
				}
			}
		}

		level () {
			if (this.parent === null) {
				return 0;
			} else {
				return this.parent.level() + 1;
			}
		}

		hasChild(annotation) {
			return this.children.includes(annotation);
		}

		remove (annotation) {
			if (this.hasChild(annotation)) {
				annotation.removeAllChildren();
				annotation.dispose();
				this.children = this.children.filter(e => e !== annotation);
				annotation.parent = null;
			}
		}

		removeAllChildren() {
			this.children.forEach((child) => {
				if (child.children.length > 0) {
					child.removeAllChildren();
				}

				this.remove(child);
			});
		}

		updateBounds () {
			let box = new Box3();

			if (this.position) {
				box.expandByPoint(this.position);
			}

			for (let child of this.children) {
				child.updateBounds();

				box.union(child.boundingBox);
			}

			this.boundingBox.copy(box);
		}

		traverse (handler) {
			let expand = handler(this);

			if (expand === undefined || expand === true) {
				for (let child of this.children) {
					child.traverse(handler);
				}
			}
		}

		traverseDescendants (handler) {
			for (let child of this.children) {
				child.traverse(handler);
			}
		}

		flatten () {
			let annotations = [];

			this.traverse(annotation => {
				annotations.push(annotation);
			});

			return annotations;
		}

		descendants () {
			let annotations = [];

			this.traverse(annotation => {
				if (annotation !== this) {
					annotations.push(annotation);
				}
			});

			return annotations;
		}

		setHighlighted (highlighted) {
			if (highlighted) {
				this.domElement.css('opacity', '0.8');
				this.elTitlebar.css('box-shadow', '0 0 5px #fff');
				this.domElement.css('z-index', '1000');

				if (this._description) {
					this.descriptionVisible = true;
					this.elDescription.fadeIn(200);
					this.elDescription.css('position', 'relative');
				}
			} else {
				this.domElement.css('opacity', '0.5');
				this.elTitlebar.css('box-shadow', '');
				this.domElement.css('z-index', '100');
				this.descriptionVisible = false;
				this.elDescription.css('display', 'none');
			}

			this.isHighlighted = highlighted;
		}

		hasView () {
			let hasPosTargetView = this.cameraTarget.x != null;
			hasPosTargetView = hasPosTargetView && this.cameraPosition.x != null;

			let hasRadiusView = this.radius !== undefined;

			let hasView = hasPosTargetView || hasRadiusView;

			return hasView;
		};

		moveHere (camera) {
			if (!this.hasView()) {
				return;
			}

			let view = this.scene.view;
			let animationDuration = 500;
			let easing = TWEEN.Easing.Quartic.Out;

			let endTarget;
			if (this.cameraTarget) {
				endTarget = this.cameraTarget;
			} else if (this.position) {
				endTarget = this.position;
			} else {
				endTarget = this.boundingBox.getCenter(new Vector3());
			}

			if (this.cameraPosition) {
				let endPosition = this.cameraPosition;

				Utils.moveTo(this.scene, endPosition, endTarget);
			} else if (this.radius) {
				let direction = view.direction;
				let endPosition = endTarget.clone().add(direction.multiplyScalar(-this.radius));
				let startRadius = view.radius;
				let endRadius = this.radius;

				{ // animate camera position
					let tween = new TWEEN.Tween(view.position).to(endPosition, animationDuration);
					tween.easing(easing);
					tween.start();
				}

				{ // animate radius
					let t = {x: 0};

					let tween = new TWEEN.Tween(t)
						.to({x: 1}, animationDuration)
						.onUpdate(function () {
							view.radius = this.x * endRadius + (1 - this.x) * startRadius;
						});
					tween.easing(easing);
					tween.start();
				}
			}
		};

		dispose () {
			if (this.domElement.parentElement) {
				this.domElement.parentElement.removeChild(this.domElement);
			}
		};

		toString () {
			return 'Annotation: ' + this._title;
		}
	};

	class EnumItem{
		constructor(object){
			for(let key of Object.keys(object)){
				this[key] = object[key];
			}
		}

		inspect(){
			return `Enum(${this.name}: ${this.value})`;
		}
	};

	class Enum{

		constructor(object){
			this.object = object;

			for(let key of Object.keys(object)){
				let value = object[key];

				if(typeof value === "object"){
					value.name = key;
				}else {
					value = {name: key, value: value};
				}
				
				this[key] = new EnumItem(value);
			}
		}

		fromValue(value){
			for(let key of Object.keys(this.object)){
				if(this[key].value === value){
					return this[key];
				}
			}

			throw new Error(`No enum for value: ${value}`);
		}
		
	};

	const CameraMode = {
		ORTHOGRAPHIC: 0,
		PERSPECTIVE: 1,
		VR: 2,
	};

	const ClipTask = {
		NONE: 0,
		HIGHLIGHT: 1,
		SHOW_INSIDE: 2,
		SHOW_OUTSIDE: 3
	};

	const ClipMethod = {
		INSIDE_ANY: 0,
		INSIDE_ALL: 1
	};

	const ElevationGradientRepeat = {
		CLAMP: 0,
		REPEAT: 1,
		MIRRORED_REPEAT: 2,
	};

	const MOUSE$1 = {
		LEFT: 0b0001,
		RIGHT: 0b0010,
		MIDDLE: 0b0100
	};

	const PointSizeType = {
		FIXED: 0,
		ATTENUATED: 1,
		ADAPTIVE: 2
	};

	const PointShape = {
		SQUARE: 0,
		CIRCLE: 1,
		PARABOLOID: 2
	};

	const TreeType = {
		OCTREE:	0,
		KDTREE:	1
	};

	const LengthUnits = {
		METER: {code: 'm', unitspermeter: 1.0},
		FEET: {code: 'ft', unitspermeter: 3.28084},
		INCH: {code: '\u2033', unitspermeter: 39.3701}
	};

	let ftCanvas = document.createElement('canvas');

	const Features = (function () {

		let gl = ftCanvas.getContext('webgl') || ftCanvas.getContext('experimental-webgl');
		if (gl === null){ 
			return null; 
		}

		// -- code taken from THREE.WebGLRenderer --
		let _vertexShaderPrecisionHighpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT);
		let _vertexShaderPrecisionMediumpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.MEDIUM_FLOAT);
		// Unused: let _vertexShaderPrecisionLowpFloat = gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.LOW_FLOAT);

		let _fragmentShaderPrecisionHighpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT);
		let _fragmentShaderPrecisionMediumpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.MEDIUM_FLOAT);
		// Unused: let _fragmentShaderPrecisionLowpFloat = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.LOW_FLOAT);

		let highpAvailable = _vertexShaderPrecisionHighpFloat.precision > 0 && _fragmentShaderPrecisionHighpFloat.precision > 0;
		let mediumpAvailable = _vertexShaderPrecisionMediumpFloat.precision > 0 && _fragmentShaderPrecisionMediumpFloat.precision > 0;
		// -----------------------------------------

		let precision;
		if (highpAvailable) {
			precision = 'highp';
		} else if (mediumpAvailable) {
			precision = 'mediump';
		} else {
			precision = 'lowp';
		}

		return {
			SHADER_INTERPOLATION: {
				isSupported: function () {
					let supported = true;

					supported = supported && gl.getExtension('EXT_frag_depth');
					supported = supported && gl.getParameter(gl.MAX_VARYING_VECTORS) >= 8;

					return supported;
				}
			},
			SHADER_SPLATS: {
				isSupported: function () {
					let supported = true;

					supported = supported && gl.getExtension('EXT_frag_depth');
					supported = supported && gl.getExtension('OES_texture_float');
					supported = supported && gl.getParameter(gl.MAX_VARYING_VECTORS) >= 8;

					return supported;
				}

			},
			SHADER_EDL: {
				isSupported: function () {
					let supported = true;

					supported = supported && gl.getExtension('EXT_frag_depth');
					supported = supported && gl.getExtension('OES_texture_float');
					supported = supported && gl.getParameter(gl.MAX_VARYING_VECTORS) >= 8;

					//supported = supported || (gl instanceof WebGL2RenderingContext);

					return supported;
				}

			},
			//WEBGL2: {
			//	isSupported: function(){
			//		return gl instanceof WebGL2RenderingContext;
			//	}
			//},
			precision: precision
		};
	}());

	const KeyCodes = {

		LEFT: 37,
		UP: 38,
		RIGHT: 39,
		BOTTOM: 40,
		DELETE: 46,

		A: 'A'.charCodeAt(0),
		S: 'S'.charCodeAt(0),
		D: 'D'.charCodeAt(0),
		W: 'W'.charCodeAt(0),
		Q: 'Q'.charCodeAt(0),
		E: 'E'.charCodeAt(0),
		R: 'R'.charCodeAt(0),
		F: 'F'.charCodeAt(0)
		
	};

	class LRUItem{

		constructor(node){
			this.previous = null;
			this.next = null;
			this.node = node;
		}

	}

	/**
	 *
	 * @class A doubly-linked-list of the least recently used elements.
	 */
	class LRU{

		constructor(){
			// the least recently used item
			this.first = null;
			// the most recently used item
			this.last = null;
			// a list of all items in the lru list
			this.items = {};
			this.elements = 0;
			this.numPoints = 0;
		}

		size(){
			return this.elements;
		}

		contains(node){
			return this.items[node.id] == null;
		}

		touch(node){
			if (!node.loaded) {
				return;
			}

			let item;
			if (this.items[node.id] == null) {
				// add to list
				item = new LRUItem(node);
				item.previous = this.last;
				this.last = item;
				if (item.previous !== null) {
					item.previous.next = item;
				}

				this.items[node.id] = item;
				this.elements++;

				if (this.first === null) {
					this.first = item;
				}
				this.numPoints += node.numPoints;
			} else {
				// update in list
				item = this.items[node.id];
				if (item.previous === null) {
					// handle touch on first element
					if (item.next !== null) {
						this.first = item.next;
						this.first.previous = null;
						item.previous = this.last;
						item.next = null;
						this.last = item;
						item.previous.next = item;
					}
				} else if (item.next === null) {
					// handle touch on last element
				} else {
					// handle touch on any other element
					item.previous.next = item.next;
					item.next.previous = item.previous;
					item.previous = this.last;
					item.next = null;
					this.last = item;
					item.previous.next = item;
				}
			}
		}

		remove(node){
			let lruItem = this.items[node.id];
			if (lruItem) {
				if (this.elements === 1) {
					this.first = null;
					this.last = null;
				} else {
					if (!lruItem.previous) {
						this.first = lruItem.next;
						this.first.previous = null;
					}
					if (!lruItem.next) {
						this.last = lruItem.previous;
						this.last.next = null;
					}
					if (lruItem.previous && lruItem.next) {
						lruItem.previous.next = lruItem.next;
						lruItem.next.previous = lruItem.previous;
					}
				}

				delete this.items[node.id];
				this.elements--;
				this.numPoints -= node.numPoints;
			}
		}

		getLRUItem(){
			if (this.first === null) {
				return null;
			}
			let lru = this.first;

			return lru.node;
		}

		toString(){
			let string = '{ ';
			let curr = this.first;
			while (curr !== null) {
				string += curr.node.id;
				if (curr.next !== null) {
					string += ', ';
				}
				curr = curr.next;
			}
			string += '}';
			string += '(' + this.size() + ')';
			return string;
		}

		freeMemory(){
			if (this.elements <= 1) {
				return;
			}
			
			while (this.numPoints > Potree.pointLoadLimit) {
				let element = this.first;
				let node = element.node;
				this.remove(node);
				this.disposeDescendants(node);
			}
		}

		disposeDescendants(node){
			let stack = [];
			stack.push(node);
			while (stack.length > 0) {
				let current = stack.pop();

				current.dispose();
				this.remove(current);

				for (let key in current.children) {
					if (current.children.hasOwnProperty(key)) {
						let child = current.children[key];
						if (child.loaded) {
							stack.push(current.children[key]);
						}
					}
				}
			}
		}

	}

	class PointCloudTreeNode extends EventDispatcher{

		constructor(){
			super();
			this.needsTransformUpdate = true;
		}

		getChildren () {
			throw new Error('override function');
		}

		getBoundingBox () {
			throw new Error('override function');
		}

		isLoaded () {
			throw new Error('override function');
		}

		isGeometryNode () {
			throw new Error('override function');
		}

		isTreeNode () {
			throw new Error('override function');
		}

		getLevel () {
			throw new Error('override function');
		}

		getBoundingSphere () {
			throw new Error('override function');
		}
	};

	class PointCloudTree extends Object3D {
		constructor () {
			super();
		}

		initialized () {
			return this.root !== null;
		}
	};

	/**
	 * Some types of possible point attribute data formats
	 *
	 * @class
	 */
	const PointAttributeTypes = {
		DATA_TYPE_DOUBLE: {ordinal: 0, name: "double", size: 8},
		DATA_TYPE_FLOAT:  {ordinal: 1, name: "float",  size: 4},
		DATA_TYPE_INT8:   {ordinal: 2, name: "int8",   size: 1},
		DATA_TYPE_UINT8:  {ordinal: 3, name: "uint8",  size: 1},
		DATA_TYPE_INT16:  {ordinal: 4, name: "int16",  size: 2},
		DATA_TYPE_UINT16: {ordinal: 5, name: "uint16", size: 2},
		DATA_TYPE_INT32:  {ordinal: 6, name: "int32",  size: 4},
		DATA_TYPE_UINT32: {ordinal: 7, name: "uint32", size: 4},
		DATA_TYPE_INT64:  {ordinal: 8, name: "int64",  size: 8},
		DATA_TYPE_UINT64: {ordinal: 9, name: "uint64", size: 8}
	};

	let i = 0;
	for (let obj in PointAttributeTypes) {
		PointAttributeTypes[i] = PointAttributeTypes[obj];
		i++;
	}


	class PointAttribute{
		
		constructor(name, type, numElements){
			this.name = name;
			this.type = type;
			this.numElements = numElements;
			this.byteSize = this.numElements * this.type.size;
			this.description = "";
			this.range = [Infinity, -Infinity];
		}

	};

	PointAttribute.POSITION_CARTESIAN = new PointAttribute(
		"POSITION_CARTESIAN", PointAttributeTypes.DATA_TYPE_FLOAT, 3);

	PointAttribute.RGBA_PACKED = new PointAttribute(
		"COLOR_PACKED", PointAttributeTypes.DATA_TYPE_INT8, 4);

	PointAttribute.COLOR_PACKED = PointAttribute.RGBA_PACKED;

	PointAttribute.RGB_PACKED = new PointAttribute(
		"COLOR_PACKED", PointAttributeTypes.DATA_TYPE_INT8, 3);

	PointAttribute.NORMAL_FLOATS = new PointAttribute(
		"NORMAL_FLOATS", PointAttributeTypes.DATA_TYPE_FLOAT, 3);

	PointAttribute.INTENSITY = new PointAttribute(
		"INTENSITY", PointAttributeTypes.DATA_TYPE_UINT16, 1);

	PointAttribute.CLASSIFICATION = new PointAttribute(
		"CLASSIFICATION", PointAttributeTypes.DATA_TYPE_UINT8, 1);

	PointAttribute.NORMAL_SPHEREMAPPED = new PointAttribute(
		"NORMAL_SPHEREMAPPED", PointAttributeTypes.DATA_TYPE_UINT8, 2);

	PointAttribute.NORMAL_OCT16 = new PointAttribute(
		"NORMAL_OCT16", PointAttributeTypes.DATA_TYPE_UINT8, 2);

	PointAttribute.NORMAL = new PointAttribute(
		"NORMAL", PointAttributeTypes.DATA_TYPE_FLOAT, 3);
		
	PointAttribute.RETURN_NUMBER = new PointAttribute(
		"RETURN_NUMBER", PointAttributeTypes.DATA_TYPE_UINT8, 1);
		
	PointAttribute.NUMBER_OF_RETURNS = new PointAttribute(
		"NUMBER_OF_RETURNS", PointAttributeTypes.DATA_TYPE_UINT8, 1);
		
	PointAttribute.SOURCE_ID = new PointAttribute(
		"SOURCE_ID", PointAttributeTypes.DATA_TYPE_UINT16, 1);

	PointAttribute.INDICES = new PointAttribute(
		"INDICES", PointAttributeTypes.DATA_TYPE_UINT32, 1);

	PointAttribute.SPACING = new PointAttribute(
		"SPACING", PointAttributeTypes.DATA_TYPE_FLOAT, 1);

	PointAttribute.GPS_TIME = new PointAttribute(
		"GPS_TIME", PointAttributeTypes.DATA_TYPE_DOUBLE, 1);

	class PointAttributes{

		constructor(pointAttributes){
			this.attributes = [];
			this.byteSize = 0;
			this.size = 0;
			this.vectors = [];

			if (pointAttributes != null) {
				for (let i = 0; i < pointAttributes.length; i++) {
					let pointAttributeName = pointAttributes[i];
					let pointAttribute = PointAttribute[pointAttributeName];
					this.attributes.push(pointAttribute);
					this.byteSize += pointAttribute.byteSize;
					this.size++;
				}
			}
		}


		add(pointAttribute){
			this.attributes.push(pointAttribute);
			this.byteSize += pointAttribute.byteSize;
			this.size++;
		};

		addVector(vector){
			this.vectors.push(vector);
		}

		hasColors(){
			for (let name in this.attributes) {
				let pointAttribute = this.attributes[name];
				if (pointAttribute.name === PointAttributeNames.COLOR_PACKED) {
					return true;
				}
			}

			return false;
		};

		hasNormals(){
			for (let name in this.attributes) {
				let pointAttribute = this.attributes[name];
				if (
					pointAttribute === PointAttribute.NORMAL_SPHEREMAPPED ||
					pointAttribute === PointAttribute.NORMAL_FLOATS ||
					pointAttribute === PointAttribute.NORMAL ||
					pointAttribute === PointAttribute.NORMAL_OCT16) {
					return true;
				}
			}

			return false;
		};

	}

	class U {
		static toVector3(v, offset) {
			return new Vector3().fromArray(v, offset || 0);
		}

		static toBox3(b) {
			return new Box3(U.toVector3(b), U.toVector3(b, 3));
		};

		static findDim(schema, name) {
			var dim = schema.find((dim) => dim.name == name);
			if (!dim) throw new Error('Failed to find ' + name + ' in schema');
			return dim;
		}

		static sphereFrom(b) {
			return b.getBoundingSphere(new Sphere());
		}
	};

	class PointCloudEptGeometry {
		constructor(url, info) {
			let version = info.version;
			let schema = info.schema;
			let bounds = info.bounds;
			let boundsConforming = info.boundsConforming;

			let xyz = [
				U.findDim(schema, 'X'),
				U.findDim(schema, 'Y'),
				U.findDim(schema, 'Z')
			];
			let scale = xyz.map((d) => d.scale || 1);
			let offset = xyz.map((d) => d.offset || 0);
			this.eptScale = U.toVector3(scale);
			this.eptOffset = U.toVector3(offset);

			this.url = url;
			this.info = info;
			this.type = 'ept';

			this.schema = schema;
			this.span = info.span || info.ticks;
			this.boundingBox = U.toBox3(bounds);
			this.tightBoundingBox = U.toBox3(boundsConforming);
			this.offset = U.toVector3([0, 0, 0]);
			this.boundingSphere = U.sphereFrom(this.boundingBox);
			this.tightBoundingSphere = U.sphereFrom(this.tightBoundingBox);
			this.version = new Potree.Version('1.7');

			this.projection = null;
			this.fallbackProjection = null;

			if (info.srs && info.srs.horizontal) {
				this.projection = info.srs.authority + ':' + info.srs.horizontal;
			}

			if (info.srs.wkt) {
				if (!this.projection) this.projection = info.srs.wkt;
				else this.fallbackProjection = info.srs.wkt;
			}

			{ 
				// TODO [mschuetz]: named projections that proj4 can't handle seem to cause problems.
				// remove them for now

				try{
					proj4(this.projection);
				}catch(e){
					this.projection = null;
				}

			

			}

			
			{
				const attributes = new PointAttributes();

				attributes.add(PointAttribute.POSITION_CARTESIAN);
				attributes.add(new PointAttribute("rgba", PointAttributeTypes.DATA_TYPE_UINT8, 4));
				attributes.add(new PointAttribute("intensity", PointAttributeTypes.DATA_TYPE_UINT16, 1));
				attributes.add(new PointAttribute("classification", PointAttributeTypes.DATA_TYPE_UINT8, 1));
				attributes.add(new PointAttribute("gps-time", PointAttributeTypes.DATA_TYPE_DOUBLE, 1));
				attributes.add(new PointAttribute("returnNumber", PointAttributeTypes.DATA_TYPE_UINT8, 1));
				attributes.add(new PointAttribute("number of returns", PointAttributeTypes.DATA_TYPE_UINT8, 1));
				attributes.add(new PointAttribute("return number", PointAttributeTypes.DATA_TYPE_UINT8, 1));
				attributes.add(new PointAttribute("source id", PointAttributeTypes.DATA_TYPE_UINT16, 1));

				this.pointAttributes = attributes;
			}



			this.spacing =
				(this.boundingBox.max.x - this.boundingBox.min.x) / this.span;

			let hierarchyType = info.hierarchyType || 'json';

			const dataType = info.dataType;
			if (dataType == 'laszip') {
				this.loader = new Potree.EptLaszipLoader();
			}
			else if (dataType == 'binary') {
				this.loader = new Potree.EptBinaryLoader();
			}
			else if (dataType == 'zstandard') {
				this.loader = new Potree.EptZstandardLoader();
			}
			else {
				throw new Error('Could not read data type: ' + dataType);
			}
		}
	};

	class EptKey {
		constructor(ept, b, d, x, y, z) {
			this.ept = ept;
			this.b = b;
			this.d = d;
			this.x = x || 0;
			this.y = y || 0;
			this.z = z || 0;
		}

		name() {
			return this.d + '-' + this.x + '-' + this.y + '-' + this.z;
		}

		step(a, b, c) {
			let min = this.b.min.clone();
			let max = this.b.max.clone();
			let dst = new Vector3().subVectors(max, min);

			if (a)	min.x += dst.x / 2;
			else	max.x -= dst.x / 2;

			if (b)	min.y += dst.y / 2;
			else	max.y -= dst.y / 2;

			if (c)	min.z += dst.z / 2;
			else	max.z -= dst.z / 2;

			return new Potree.EptKey(
					this.ept,
					new Box3(min, max),
					this.d + 1,
					this.x * 2 + a,
					this.y * 2 + b,
					this.z * 2 + c);
		}

		children() {
			var result = [];
			for (var a = 0; a < 2; ++a) {
				for (var b = 0; b < 2; ++b) {
					for (var c = 0; c < 2; ++c) {
						var add = this.step(a, b, c).name();
						if (!result.includes(add)) result = result.concat(add);
					}
				}
			}
			return result;
		}
	}

	class PointCloudEptGeometryNode extends PointCloudTreeNode {
		constructor(ept, b, d, x, y, z) {
			super();

			this.ept = ept;
			this.key = new Potree.EptKey(
					this.ept,
					b || this.ept.boundingBox,
					d || 0,
					x,
					y,
					z);

			this.id = PointCloudEptGeometryNode.IDCount++;
			this.geometry = null;
			this.boundingBox = this.key.b;
			this.tightBoundingBox = this.boundingBox;
			this.spacing = this.ept.spacing / Math.pow(2, this.key.d);
			this.boundingSphere = U.sphereFrom(this.boundingBox);

			// These are set during hierarchy loading.
			this.hasChildren = false;
			this.children = { };
			this.numPoints = -1;

			this.level = this.key.d;
			this.loaded = false;
			this.loading = false;
			this.oneTimeDisposeHandlers = [];

			let k = this.key;
			this.name = this.toPotreeName(k.d, k.x, k.y, k.z);
			this.index = parseInt(this.name.charAt(this.name.length - 1));
		}

		isGeometryNode() { return true; }
		getLevel() { return this.level; }
		isTreeNode() { return false; }
		isLoaded() { return this.loaded; }
		getBoundingSphere() { return this.boundingSphere; }
		getBoundingBox() { return this.boundingBox; }
		url() { return this.ept.url + 'ept-data/' + this.filename(); }
		getNumPoints() { return this.numPoints; }

		filename() { return this.key.name(); }

		getChildren() {
			let children = [];

			for (let i = 0; i < 8; i++) {
				if (this.children[i]) {
					children.push(this.children[i]);
				}
			}

			return children;
		}

		addChild(child) {
			this.children[child.index] = child;
			child.parent = this;
		}

		load() {
			if (this.loaded || this.loading) return;
			if (Potree.numNodesLoading >= Potree.maxNodesLoading) return;

			this.loading = true;
			++Potree.numNodesLoading;

			if (this.numPoints == -1) this.loadHierarchy();
			this.loadPoints();
		}

		loadPoints(){
			this.ept.loader.load(this);
		}

		async loadHierarchy() {
			let nodes = { };
			nodes[this.filename()] = this;
			this.hasChildren = false;

			let eptHierarchyFile =
				`${this.ept.url}ept-hierarchy/${this.filename()}.json`;

			let response = await fetch(eptHierarchyFile);
			let hier = await response.json();

			// Since we want to traverse top-down, and 10 comes
			// lexicographically before 9 (for example), do a deep sort.
			var keys = Object.keys(hier).sort((a, b) => {
				let [da, xa, ya, za] = a.split('-').map((n) => parseInt(n, 10));
				let [db, xb, yb, zb] = b.split('-').map((n) => parseInt(n, 10));
				if (da < db) return -1; if (da > db) return 1;
				if (xa < xb) return -1; if (xa > xb) return 1;
				if (ya < yb) return -1; if (ya > yb) return 1;
				if (za < zb) return -1; if (za > zb) return 1;
				return 0;
			});

			keys.forEach((v) => {
				let [d, x, y, z] = v.split('-').map((n) => parseInt(n, 10));
				let a = x & 1, b = y & 1, c = z & 1;
				let parentName =
					(d - 1) + '-' + (x >> 1) + '-' + (y >> 1) + '-' + (z >> 1);

				let parentNode = nodes[parentName];
				if (!parentNode) return;
				parentNode.hasChildren = true;

				let key = parentNode.key.step(a, b, c);

				let node = new Potree.PointCloudEptGeometryNode(
						this.ept,
						key.b,
						key.d,
						key.x,
						key.y,
						key.z);

				node.level = d;
				node.numPoints = hier[v];

				parentNode.addChild(node);
				nodes[key.name()] = node;
			});
		}

		doneLoading(bufferGeometry, tightBoundingBox, np, mean) {
			bufferGeometry.boundingBox = this.boundingBox;
			this.geometry = bufferGeometry;
			this.tightBoundingBox = tightBoundingBox;
			this.numPoints = np;
			this.mean = mean;
			this.loaded = true;
			this.loading = false;
			--Potree.numNodesLoading;
		}

		toPotreeName(d, x, y, z) {
			var name = 'r';

			for (var i = 0; i < d; ++i) {
				var shift = d - i - 1;
				var mask = 1 << shift;
				var step = 0;

				if (x & mask) step += 4;
				if (y & mask) step += 2;
				if (z & mask) step += 1;

				name += step;
			}

			return name;
		}

		dispose() {
			if (this.geometry && this.parent != null) {
				this.geometry.dispose();
				this.geometry = null;
				this.loaded = false;

				// this.dispatchEvent( { type: 'dispose' } );
				for (let i = 0; i < this.oneTimeDisposeHandlers.length; i++) {
					let handler = this.oneTimeDisposeHandlers[i];
					handler();
				}
				this.oneTimeDisposeHandlers = [];
			}
		}
	}

	PointCloudEptGeometryNode.IDCount = 0;

	class PointCloudOctreeGeometry{

		constructor(){
			this.url = null;
			this.octreeDir = null;
			this.spacing = 0;
			this.boundingBox = null;
			this.root = null;
			this.nodes = null;
			this.pointAttributes = null;
			this.hierarchyStepSize = -1;
			this.loader = null;
		}
		
	}

	class PointCloudOctreeGeometryNode extends PointCloudTreeNode{

		constructor(name, pcoGeometry, boundingBox){
			super();

			this.id = PointCloudOctreeGeometryNode.IDCount++;
			this.name = name;
			this.index = parseInt(name.charAt(name.length - 1));
			this.pcoGeometry = pcoGeometry;
			this.geometry = null;
			this.boundingBox = boundingBox;
			this.boundingSphere = boundingBox.getBoundingSphere(new Sphere());
			this.children = {};
			this.numPoints = 0;
			this.level = null;
			this.loaded = false;
			this.oneTimeDisposeHandlers = [];
		}

		isGeometryNode(){
			return true;
		}

		getLevel(){
			return this.level;
		}

		isTreeNode(){
			return false;
		}

		isLoaded(){
			return this.loaded;
		}

		getBoundingSphere(){
			return this.boundingSphere;
		}

		getBoundingBox(){
			return this.boundingBox;
		}

		getChildren(){
			let children = [];

			for (let i = 0; i < 8; i++) {
				if (this.children[i]) {
					children.push(this.children[i]);
				}
			}

			return children;
		}

		getBoundingBox(){
			return this.boundingBox;
		}

		getURL(){
			let url = '';

			let version = this.pcoGeometry.loader.version;

			if (version.equalOrHigher('1.5')) {
				url = this.pcoGeometry.octreeDir + '/' + this.getHierarchyPath() + '/' + this.name;
			} else if (version.equalOrHigher('1.4')) {
				url = this.pcoGeometry.octreeDir + '/' + this.name;
			} else if (version.upTo('1.3')) {
				url = this.pcoGeometry.octreeDir + '/' + this.name;
			}

			return url;
		}

		getHierarchyPath(){
			let path = 'r/';

			let hierarchyStepSize = this.pcoGeometry.hierarchyStepSize;
			let indices = this.name.substr(1);

			let numParts = Math.floor(indices.length / hierarchyStepSize);
			for (let i = 0; i < numParts; i++) {
				path += indices.substr(i * hierarchyStepSize, hierarchyStepSize) + '/';
			}

			path = path.slice(0, -1);

			return path;
		}

		addChild(child) {
			this.children[child.index] = child;
			child.parent = this;
		}

		load(){
			if (this.loading === true || this.loaded === true || Potree.numNodesLoading >= Potree.maxNodesLoading) {
				return;
			}

			this.loading = true;

			Potree.numNodesLoading++;

			if (this.pcoGeometry.loader.version.equalOrHigher('1.5')) {
				if ((this.level % this.pcoGeometry.hierarchyStepSize) === 0 && this.hasChildren) {
					this.loadHierachyThenPoints();
				} else {
					this.loadPoints();
				}
			} else {
				this.loadPoints();
			}
		}

		loadPoints(){
			this.pcoGeometry.loader.load(this);
		}

		loadHierachyThenPoints(){
			let node = this;

			// load hierarchy
			let callback = function (node, hbuffer) {

				let tStart = performance.now();

				let view = new DataView(hbuffer);

				let stack = [];
				let children = view.getUint8(0);
				let numPoints = view.getUint32(1, true);
				node.numPoints = numPoints;
				stack.push({children: children, numPoints: numPoints, name: node.name});

				let decoded = [];

				let offset = 5;
				while (stack.length > 0) {
					let snode = stack.shift();
					let mask = 1;
					for (let i = 0; i < 8; i++) {
						if ((snode.children & mask) !== 0) {
							let childName = snode.name + i;

							let childChildren = view.getUint8(offset);
							let childNumPoints = view.getUint32(offset + 1, true);

							stack.push({children: childChildren, numPoints: childNumPoints, name: childName});

							decoded.push({children: childChildren, numPoints: childNumPoints, name: childName});

							offset += 5;
						}

						mask = mask * 2;
					}

					if (offset === hbuffer.byteLength) {
						break;
					}
				}

				// console.log(decoded);

				let nodes = {};
				nodes[node.name] = node;
				let pco = node.pcoGeometry;

				for (let i = 0; i < decoded.length; i++) {
					let name = decoded[i].name;
					let decodedNumPoints = decoded[i].numPoints;
					let index = parseInt(name.charAt(name.length - 1));
					let parentName = name.substring(0, name.length - 1);
					let parentNode = nodes[parentName];
					let level = name.length - 1;
					let boundingBox = Utils.createChildAABB(parentNode.boundingBox, index);

					let currentNode = new PointCloudOctreeGeometryNode(name, pco, boundingBox);
					currentNode.level = level;
					currentNode.numPoints = decodedNumPoints;
					currentNode.hasChildren = decoded[i].children > 0;
					currentNode.spacing = pco.spacing / Math.pow(2, level);
					parentNode.addChild(currentNode);
					nodes[name] = currentNode;
				}

				let duration = performance.now() - tStart;
				if(duration > 5){
					let msg = `duration: ${duration}ms, numNodes: ${decoded.length}`;
					console.log(msg);
				}

				node.loadPoints();
			};
			if ((node.level % node.pcoGeometry.hierarchyStepSize) === 0) {
				// let hurl = node.pcoGeometry.octreeDir + "/../hierarchy/" + node.name + ".hrc";
				let hurl = node.pcoGeometry.octreeDir + '/' + node.getHierarchyPath() + '/' + node.name + '.hrc';

				let xhr = XHRFactory.createXMLHttpRequest();
				xhr.open('GET', hurl, true);
				xhr.responseType = 'arraybuffer';
				xhr.overrideMimeType('text/plain; charset=x-user-defined');
				xhr.onreadystatechange = () => {
					if (xhr.readyState === 4) {
						if (xhr.status === 200 || xhr.status === 0) {
							let hbuffer = xhr.response;
							callback(node, hbuffer);
						} else {
							console.log('Failed to load file! HTTP status: ' + xhr.status + ', file: ' + hurl);
							Potree.numNodesLoading--;
						}
					}
				};
				try {
					xhr.send(null);
				} catch (e) {
					console.log('fehler beim laden der punktwolke: ' + e);
				}
			}
		}

		getNumPoints(){
			return this.numPoints;
		}

		dispose(){
			if (this.geometry && this.parent != null) {
				this.geometry.dispose();
				this.geometry = null;
				this.loaded = false;

				this.dispatchEvent( { type: 'dispose' } );
				
				for (let i = 0; i < this.oneTimeDisposeHandlers.length; i++) {
					let handler = this.oneTimeDisposeHandlers[i];
					handler();
				}
				this.oneTimeDisposeHandlers = [];
			}
		}
		
	}

	PointCloudOctreeGeometryNode.IDCount = 0;

	// -------------------------------------------
	// to get a ready to use gradient array from a chroma.js gradient:
	// http://gka.github.io/chroma.js/
	// -------------------------------------------
	//
	// let stops = [];
	// for(let i = 0; i <= 10; i++){
	//	let range = chroma.scale(['yellow', 'navy']).mode('lch').domain([10,0])(i)._rgb
	//		.slice(0, 3)
	//		.map(v => (v / 255).toFixed(4))
	//		.join(", ");
	//
	//	let line = `[${i / 10}, new THREE.Color(${range})],`;
	//
	//	stops.push(line);
	// }
	// stops.join("\n");
	//
	//
	//
	// -------------------------------------------
	// to get a ready to use gradient array from matplotlib:
	// -------------------------------------------
	// import matplotlib.pyplot as plt
	// import matplotlib.colors as colors
	//
	// norm = colors.Normalize(vmin=0,vmax=1)
	// cmap = plt.cm.viridis
	//
	// for i in range(0,11):
	//	u = i / 10
	//	rgb = cmap(norm(u))[0:3]
	//	rgb = ["{0:.3f}".format(v) for v in rgb]
	//	rgb = "[" + str(u) + ", new THREE.Color(" +  ", ".join(rgb) + ")],"
	//	print(rgb)

	let Gradients = {
		// From chroma spectral http://gka.github.io/chroma.js/
		SPECTRAL: [
			[0, new Color(0.3686, 0.3098, 0.6353)],
			[0.1, new Color(0.1961, 0.5333, 0.7412)],
			[0.2, new Color(0.4000, 0.7608, 0.6471)],
			[0.3, new Color(0.6706, 0.8667, 0.6431)],
			[0.4, new Color(0.9020, 0.9608, 0.5961)],
			[0.5, new Color(1.0000, 1.0000, 0.7490)],
			[0.6, new Color(0.9961, 0.8784, 0.5451)],
			[0.7, new Color(0.9922, 0.6824, 0.3804)],
			[0.8, new Color(0.9569, 0.4275, 0.2627)],
			[0.9, new Color(0.8353, 0.2431, 0.3098)],
			[1, new Color(0.6196, 0.0039, 0.2588)]
		],
		PLASMA: [
			[0.0, new Color(0.241, 0.015, 0.610)],
			[0.1, new Color(0.387, 0.001, 0.654)],
			[0.2, new Color(0.524, 0.025, 0.653)],
			[0.3, new Color(0.651, 0.125, 0.596)],
			[0.4, new Color(0.752, 0.227, 0.513)],
			[0.5, new Color(0.837, 0.329, 0.431)],
			[0.6, new Color(0.907, 0.435, 0.353)],
			[0.7, new Color(0.963, 0.554, 0.272)],
			[0.8, new Color(0.992, 0.681, 0.195)],
			[0.9, new Color(0.987, 0.822, 0.144)],
			[1.0, new Color(0.940, 0.975, 0.131)]
		],
		YELLOW_GREEN: [
			[0, new Color(0.1647, 0.2824, 0.3451)],
			[0.1, new Color(0.1338, 0.3555, 0.4227)],
			[0.2, new Color(0.0610, 0.4319, 0.4864)],
			[0.3, new Color(0.0000, 0.5099, 0.5319)],
			[0.4, new Color(0.0000, 0.5881, 0.5569)],
			[0.5, new Color(0.1370, 0.6650, 0.5614)],
			[0.6, new Color(0.2906, 0.7395, 0.5477)],
			[0.7, new Color(0.4453, 0.8099, 0.5201)],
			[0.8, new Color(0.6102, 0.8748, 0.4850)],
			[0.9, new Color(0.7883, 0.9323, 0.4514)],
			[1, new Color(0.9804, 0.9804, 0.4314)]
		],
		VIRIDIS: [
			[0.0, new Color(0.267, 0.005, 0.329)],
			[0.1, new Color(0.283, 0.141, 0.458)],
			[0.2, new Color(0.254, 0.265, 0.530)],
			[0.3, new Color(0.207, 0.372, 0.553)],
			[0.4, new Color(0.164, 0.471, 0.558)],
			[0.5, new Color(0.128, 0.567, 0.551)],
			[0.6, new Color(0.135, 0.659, 0.518)],
			[0.7, new Color(0.267, 0.749, 0.441)],
			[0.8, new Color(0.478, 0.821, 0.318)],
			[0.9, new Color(0.741, 0.873, 0.150)],
			[1.0, new Color(0.993, 0.906, 0.144)]
		],
		INFERNO: [
			[0.0, new Color(0.077, 0.042, 0.206)],
			[0.1, new Color(0.225, 0.036, 0.388)],
			[0.2, new Color(0.373, 0.074, 0.432)],
			[0.3, new Color(0.522, 0.128, 0.420)],
			[0.4, new Color(0.665, 0.182, 0.370)],
			[0.5, new Color(0.797, 0.255, 0.287)],
			[0.6, new Color(0.902, 0.364, 0.184)],
			[0.7, new Color(0.969, 0.516, 0.063)],
			[0.8, new Color(0.988, 0.683, 0.072)],
			[0.9, new Color(0.961, 0.859, 0.298)],
			[1.0, new Color(0.988, 0.998, 0.645)]
		],
		GRAYSCALE: [
			[0, new Color(0, 0, 0)],
			[1, new Color(1, 1, 1)]
		],
		// 16 samples of the TURBU color scheme
		// values taken from: https://gist.github.com/mikhailov-work/ee72ba4191942acecc03fe6da94fc73f
		// original file licensed under Apache-2.0
		TURBO: [
			[0.00, new Color(0.18995, 0.07176, 0.23217)],
			[0.07, new Color(0.25107, 0.25237, 0.63374)],
			[0.13, new Color(0.27628, 0.42118, 0.89123)],
			[0.20, new Color(0.25862, 0.57958, 0.99876)],
			[0.27, new Color(0.15844, 0.73551, 0.92305)],
			[0.33, new Color(0.09267, 0.86554, 0.7623)],
			[0.40, new Color(0.19659, 0.94901, 0.59466)],
			[0.47, new Color(0.42778, 0.99419, 0.38575)],
			[0.53, new Color(0.64362, 0.98999, 0.23356)],
			[0.60, new Color(0.80473, 0.92452, 0.20459)],
			[0.67, new Color(0.93301, 0.81236, 0.22667)],
			[0.73, new Color(0.99314, 0.67408, 0.20348)],
			[0.80, new Color(0.9836, 0.49291, 0.12849)],
			[0.87, new Color(0.92105, 0.31489, 0.05475)],
			[0.93, new Color(0.81608, 0.18462, 0.01809)],
			[1.00, new Color(0.66449, 0.08436, 0.00424)],
		],
		RAINBOW: [
			[0, new Color(0.278, 0, 0.714)],
			[1 / 6, new Color(0, 0, 1)],
			[2 / 6, new Color(0, 1, 1)],
			[3 / 6, new Color(0, 1, 0)],
			[4 / 6, new Color(1, 1, 0)],
			[5 / 6, new Color(1, 0.64, 0)],
			[1, new Color(1, 0, 0)]
		],
		CONTOUR: [
			[0.00, new Color(0, 0, 0)],
			[0.03, new Color(0, 0, 0)],
			[0.04, new Color(1, 1, 1)],
			[1.00, new Color(1, 1, 1)]
		],
	};

	let Shaders = {};

	Shaders["pointcloud.vs"] = `
precision highp float;
precision highp int;

#define max_clip_polygons 8
#define PI 3.141592653589793

attribute vec3 position;
attribute vec3 color;
attribute float intensity;
attribute float classification;
attribute float returnNumber;
attribute float numberOfReturns;
attribute float pointSourceID;
attribute vec4 indices;
attribute float spacing;
attribute float gpsTime;
attribute vec3 normal;
attribute float aExtra;

uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 uViewInv;

uniform float uScreenWidth;
uniform float uScreenHeight;
uniform float fov;
uniform float near;
uniform float far;

uniform bool uDebug;

uniform bool uUseOrthographicCamera;
uniform float uOrthoWidth;
uniform float uOrthoHeight;

#define CLIPTASK_NONE 0
#define CLIPTASK_HIGHLIGHT 1
#define CLIPTASK_SHOW_INSIDE 2
#define CLIPTASK_SHOW_OUTSIDE 3

#define CLIPMETHOD_INSIDE_ANY 0
#define CLIPMETHOD_INSIDE_ALL 1

uniform int clipTask;
uniform int clipMethod;
#if defined(num_clipboxes) && num_clipboxes > 0
	uniform mat4 clipBoxes[num_clipboxes];
#endif

#if defined(num_clipspheres) && num_clipspheres > 0
	uniform mat4 uClipSpheres[num_clipspheres];
#endif

#if defined(num_clippolygons) && num_clippolygons > 0
	uniform int uClipPolygonVCount[num_clippolygons];
	uniform vec3 uClipPolygonVertices[num_clippolygons * 8];
	uniform mat4 uClipPolygonWVP[num_clippolygons];
#endif


uniform float size;
uniform float minSize;
uniform float maxSize;

uniform float uPCIndex;
uniform float uOctreeSpacing;
uniform float uNodeSpacing;
uniform float uOctreeSize;
uniform vec3 uBBSize;
uniform float uLevel;
uniform float uVNStart;
uniform bool uIsLeafNode;

uniform vec3 uColor;
uniform float uOpacity;

uniform vec2 elevationRange;
uniform vec2 intensityRange;

uniform vec2 uFilterReturnNumberRange;
uniform vec2 uFilterNumberOfReturnsRange;
uniform vec2 uFilterPointSourceIDClipRange;
uniform vec2 uFilterGPSTimeClipRange;
uniform float uGpsScale;
uniform float uGpsOffset;

uniform vec2 uNormalizedGpsBufferRange;

uniform vec3 uIntensity_gbc;
uniform vec3 uRGB_gbc;
uniform vec3 uExtra_gbc;

uniform float uTransition;
uniform float wRGB;
uniform float wIntensity;
uniform float wElevation;
uniform float wClassification;
uniform float wReturnNumber;
uniform float wSourceID;

uniform vec2 uExtraNormalizedRange;
uniform vec2 uExtraRange;
uniform float uExtraScale;
uniform float uExtraOffset;

uniform vec3 uShadowColor;

uniform sampler2D visibleNodes;
uniform sampler2D gradient;
uniform sampler2D classificationLUT;

#if defined(color_type_matcap)
uniform sampler2D matcapTextureUniform;
#endif
uniform bool backfaceCulling;

#if defined(num_shadowmaps) && num_shadowmaps > 0
uniform sampler2D uShadowMap[num_shadowmaps];
uniform mat4 uShadowWorldView[num_shadowmaps];
uniform mat4 uShadowProj[num_shadowmaps];
#endif

varying vec3	vColor;
varying float	vLogDepth;
varying vec3	vViewPosition;
varying float 	vRadius;
varying float 	vPointSize;


float round(float number){
	return floor(number + 0.5);
}

// 
//    ###    ########     ###    ########  ######## #### ##     ## ########     ######  #### ######## ########  ######  
//   ## ##   ##     ##   ## ##   ##     ##    ##     ##  ##     ## ##          ##    ##  ##       ##  ##       ##    ## 
//  ##   ##  ##     ##  ##   ##  ##     ##    ##     ##  ##     ## ##          ##        ##      ##   ##       ##       
// ##     ## ##     ## ##     ## ########     ##     ##  ##     ## ######       ######   ##     ##    ######    ######  
// ######### ##     ## ######### ##           ##     ##   ##   ##  ##                ##  ##    ##     ##             ## 
// ##     ## ##     ## ##     ## ##           ##     ##    ## ##   ##          ##    ##  ##   ##      ##       ##    ## 
// ##     ## ########  ##     ## ##           ##    ####    ###    ########     ######  #### ######## ########  ######  
// 																			


// ---------------------
// OCTREE
// ---------------------

#if (defined(adaptive_point_size) || defined(color_type_level_of_detail)) && defined(tree_type_octree)
/**
 * number of 1-bits up to inclusive index position
 * number is treated as if it were an integer in the range 0-255
 *
 */
int numberOfOnes(int number, int index){
	int numOnes = 0;
	int tmp = 128;
	for(int i = 7; i >= 0; i--){
		
		if(number >= tmp){
			number = number - tmp;

			if(i <= index){
				numOnes++;
			}
		}
		
		tmp = tmp / 2;
	}

	return numOnes;
}


/**
 * checks whether the bit at index is 1
 * number is treated as if it were an integer in the range 0-255
 *
 */
bool isBitSet(int number, int index){

	// weird multi else if due to lack of proper array, int and bitwise support in WebGL 1.0
	int powi = 1;
	if(index == 0){
		powi = 1;
	}else if(index == 1){
		powi = 2;
	}else if(index == 2){
		powi = 4;
	}else if(index == 3){
		powi = 8;
	}else if(index == 4){
		powi = 16;
	}else if(index == 5){
		powi = 32;
	}else if(index == 6){
		powi = 64;
	}else if(index == 7){
		powi = 128;
	}else{
		return false;
	}

	int ndp = number / powi;

	return mod(float(ndp), 2.0) != 0.0;
}


/**
 * find the LOD at the point position
 */
float getLOD(){
	
	vec3 offset = vec3(0.0, 0.0, 0.0);
	int iOffset = int(uVNStart);
	float depth = uLevel;
	for(float i = 0.0; i <= 30.0; i++){
		float nodeSizeAtLevel = uOctreeSize / pow(2.0, i + uLevel + 0.0);
		
		vec3 index3d = (position-offset) / nodeSizeAtLevel;
		index3d = floor(index3d + 0.5);
		int index = int(round(4.0 * index3d.x + 2.0 * index3d.y + index3d.z));
		
		vec4 value = texture2D(visibleNodes, vec2(float(iOffset) / 2048.0, 0.0));
		int mask = int(round(value.r * 255.0));

		if(isBitSet(mask, index)){
			// there are more visible child nodes at this position
			int advanceG = int(round(value.g * 255.0)) * 256;
			int advanceB = int(round(value.b * 255.0));
			int advanceChild = numberOfOnes(mask, index - 1);
			int advance = advanceG + advanceB + advanceChild;

			iOffset = iOffset + advance;
			
			depth++;
		}else{
			// no more visible child nodes at this position
			//return value.a * 255.0;

			float lodOffset = (255.0 * value.a) / 10.0 - 10.0;

			return depth  + lodOffset;
		}
		
		offset = offset + (vec3(1.0, 1.0, 1.0) * nodeSizeAtLevel * 0.5) * index3d;
	}
		
	return depth;
}

float getSpacing(){
	vec3 offset = vec3(0.0, 0.0, 0.0);
	int iOffset = int(uVNStart);
	float depth = uLevel;
	float spacing = uNodeSpacing;
	for(float i = 0.0; i <= 30.0; i++){
		float nodeSizeAtLevel = uOctreeSize / pow(2.0, i + uLevel + 0.0);
		
		vec3 index3d = (position-offset) / nodeSizeAtLevel;
		index3d = floor(index3d + 0.5);
		int index = int(round(4.0 * index3d.x + 2.0 * index3d.y + index3d.z));
		
		vec4 value = texture2D(visibleNodes, vec2(float(iOffset) / 2048.0, 0.0));
		int mask = int(round(value.r * 255.0));
		float spacingFactor = value.a;

		if(i > 0.0){
			spacing = spacing / (255.0 * spacingFactor);
		}
		

		if(isBitSet(mask, index)){
			// there are more visible child nodes at this position
			int advanceG = int(round(value.g * 255.0)) * 256;
			int advanceB = int(round(value.b * 255.0));
			int advanceChild = numberOfOnes(mask, index - 1);
			int advance = advanceG + advanceB + advanceChild;

			iOffset = iOffset + advance;

			//spacing = spacing / (255.0 * spacingFactor);
			//spacing = spacing / 3.0;
			
			depth++;
		}else{
			// no more visible child nodes at this position
			return spacing;
		}
		
		offset = offset + (vec3(1.0, 1.0, 1.0) * nodeSizeAtLevel * 0.5) * index3d;
	}
		
	return spacing;
}

float getPointSizeAttenuation(){
	return pow(2.0, getLOD());
}


#endif


// ---------------------
// KD-TREE
// ---------------------

#if (defined(adaptive_point_size) || defined(color_type_level_of_detail)) && defined(tree_type_kdtree)

float getLOD(){
	vec3 offset = vec3(0.0, 0.0, 0.0);
	float iOffset = 0.0;
	float depth = 0.0;
		
		
	vec3 size = uBBSize;	
	vec3 pos = position;
		
	for(float i = 0.0; i <= 1000.0; i++){
		
		vec4 value = texture2D(visibleNodes, vec2(iOffset / 2048.0, 0.0));
		
		int children = int(value.r * 255.0);
		float next = value.g * 255.0;
		int split = int(value.b * 255.0);
		
		if(next == 0.0){
		 	return depth;
		}
		
		vec3 splitv = vec3(0.0, 0.0, 0.0);
		if(split == 1){
			splitv.x = 1.0;
		}else if(split == 2){
		 	splitv.y = 1.0;
		}else if(split == 4){
		 	splitv.z = 1.0;
		}
		
		iOffset = iOffset + next;
		
		float factor = length(pos * splitv / size);
		if(factor < 0.5){
			// left
		if(children == 0 || children == 2){
				return depth;
			}
		}else{
			// right
			pos = pos - size * splitv * 0.5;
			if(children == 0 || children == 1){
				return depth;
			}
			if(children == 3){
				iOffset = iOffset + 1.0;
			}
		}
		size = size * ((1.0 - (splitv + 1.0) / 2.0) + 0.5);
		
		depth++;
	}
		
		
	return depth;	
}

float getPointSizeAttenuation(){
	return 0.5 * pow(1.3, getLOD());
}

#endif



// 
//    ###    ######## ######## ########  #### ########  ##     ## ######## ########  ######  
//   ## ##      ##       ##    ##     ##  ##  ##     ## ##     ##    ##    ##       ##    ## 
//  ##   ##     ##       ##    ##     ##  ##  ##     ## ##     ##    ##    ##       ##       
// ##     ##    ##       ##    ########   ##  ########  ##     ##    ##    ######    ######  
// #########    ##       ##    ##   ##    ##  ##     ## ##     ##    ##    ##             ## 
// ##     ##    ##       ##    ##    ##   ##  ##     ## ##     ##    ##    ##       ##    ## 
// ##     ##    ##       ##    ##     ## #### ########   #######     ##    ########  ######                                                                               
// 



// formula adapted from: http://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-5-contrast-adjustment/
float getContrastFactor(float contrast){
	return (1.0158730158730156 * (contrast + 1.0)) / (1.0158730158730156 - contrast);
}

vec3 getRGB(){
	vec3 rgb = color;
	
	rgb = pow(rgb, vec3(uRGB_gbc.x));
	rgb = rgb + uRGB_gbc.y;
	rgb = (rgb - 0.5) * getContrastFactor(uRGB_gbc.z) + 0.5;
	rgb = clamp(rgb, 0.0, 1.0);

	return rgb;
}

float getIntensity(){
	float w = (intensity - intensityRange.x) / (intensityRange.y - intensityRange.x);
	w = pow(w, uIntensity_gbc.x);
	w = w + uIntensity_gbc.y;
	w = (w - 0.5) * getContrastFactor(uIntensity_gbc.z) + 0.5;
	w = clamp(w, 0.0, 1.0);

	return w;
}

vec3 getGpsTime(){

	float w = (gpsTime + uGpsOffset) * uGpsScale;


	vec3 c = texture2D(gradient, vec2(w, 1.0 - w)).rgb;


	// vec2 r = uNormalizedGpsBufferRange;
	// float w = gpsTime * (r.y - r.x) + r.x;
	// w = clamp(w, 0.0, 1.0);
	// vec3 c = texture2D(gradient, vec2(w,1.0-w)).rgb;
	
	return c;
}

vec3 getElevation(){
	vec4 world = modelMatrix * vec4( position, 1.0 );
	float w = (world.z - elevationRange.x) / (elevationRange.y - elevationRange.x);
	vec3 cElevation = texture2D(gradient, vec2(w,1.0-w)).rgb;
	
	return cElevation;
}

vec4 getClassification(){
	vec2 uv = vec2(classification / 255.0, 0.5);
	vec4 classColor = texture2D(classificationLUT, uv);
	
	return classColor;
}

vec3 getReturns(){

	// 0b 00_000_111
	float rn = mod(returnNumber, 8.0);
	// 0b 00_111_000
	float nr = mod(returnNumber / 8.0, 8.0);

	if(nr <= 1.0){
		return vec3(1.0, 0.0, 0.0);
	}else{
		return vec3(0.0, 1.0, 0.0);
	}

	// return vec3(nr / 4.0, 0.0, 0.0);

	// if(nr == 1.0){
	// 	return vec3(1.0, 1.0, 0.0);
	// }else{
	// 	if(rn == 1.0){
	// 		return vec3(1.0, 0.0, 0.0);
	// 	}else if(rn == nr){
	// 		return vec3(0.0, 0.0, 1.0);
	// 	}else{
	// 		return vec3(0.0, 1.0, 0.0);
	// 	}
	// }

	// if(numberOfReturns == 1.0){
	// 	return vec3(1.0, 1.0, 0.0);
	// }else{
	// 	if(returnNumber == 1.0){
	// 		return vec3(1.0, 0.0, 0.0);
	// 	}else if(returnNumber == numberOfReturns){
	// 		return vec3(0.0, 0.0, 1.0);
	// 	}else{
	// 		return vec3(0.0, 1.0, 0.0);
	// 	}
	// }
}

vec3 getReturnNumber(){
	if(numberOfReturns == 1.0){
		return vec3(1.0, 1.0, 0.0);
	}else{
		if(returnNumber == 1.0){
			return vec3(1.0, 0.0, 0.0);
		}else if(returnNumber == numberOfReturns){
			return vec3(0.0, 0.0, 1.0);
		}else{
			return vec3(0.0, 1.0, 0.0);
		}
	}
}

vec3 getNumberOfReturns(){
	float value = numberOfReturns;

	float w = value / 6.0;

	vec3 color = texture2D(gradient, vec2(w, 1.0 - w)).rgb;

	return color;
}

vec3 getSourceID(){
	float w = mod(pointSourceID, 10.0) / 10.0;
	return texture2D(gradient, vec2(w,1.0 - w)).rgb;
}

vec3 getCompositeColor(){
	vec3 c;
	float w;

	c += wRGB * getRGB();
	w += wRGB;
	
	c += wIntensity * getIntensity() * vec3(1.0, 1.0, 1.0);
	w += wIntensity;
	
	c += wElevation * getElevation();
	w += wElevation;
	
	c += wReturnNumber * getReturnNumber();
	w += wReturnNumber;
	
	c += wSourceID * getSourceID();
	w += wSourceID;
	
	vec4 cl = wClassification * getClassification();
	c += cl.a * cl.rgb;
	w += wClassification * cl.a;

	c = c / w;
	
	if(w == 0.0){
		//c = color;
		gl_Position = vec4(100.0, 100.0, 100.0, 0.0);
	}
	
	return c;
}


vec3 getNormal(){
	//vec3 n_hsv = vec3( modelMatrix * vec4( normal, 0.0 )) * 0.5 + 0.5; // (n_world.xyz + vec3(1.,1.,1.)) / 2.;
	vec3 n_view = normalize( vec3(modelViewMatrix * vec4( normal, 0.0 )) );
	return n_view;
}
bool applyBackfaceCulling() {
	// Black not facing vertices / Backface culling
	vec3 e = normalize(vec3(modelViewMatrix * vec4( position, 1. )));
	vec3 n = getNormal(); // normalize( vec3(modelViewMatrix * vec4( normal, 0.0 )) );

	if((uUseOrthographicCamera && n.z <= 0.) || (!uUseOrthographicCamera && dot( n, e ) >= 0.)) { 
		return true;
	} else {
		return false;
	}
}

#if defined(color_type_matcap)
// Matcap Material
vec3 getMatcap(){ 
	vec3 eye = normalize( vec3( modelViewMatrix * vec4( position, 1. ) ) ); 
	if(uUseOrthographicCamera) { 
		eye = vec3(0., 0., -1.);
	}
	vec3 r_en = reflect( eye, getNormal() ); // or r_en = e - 2. * dot( n, e ) * n;
	float m = 2. * sqrt(pow( r_en.x, 2. ) + pow( r_en.y, 2. ) + pow( r_en.z + 1., 2. ));
	vec2 vN = r_en.xy / m + .5;
	return texture2D(matcapTextureUniform, vN).rgb; 
}
#endif

vec3 getExtra(){

	float w = (aExtra + uExtraOffset) * uExtraScale;
	w = clamp(w, 0.0, 1.0);

	vec3 color = texture2D(gradient, vec2(w,1.0-w)).rgb;

	// vec2 r = uExtraNormalizedRange;

	// float w = aExtra * (r.y - r.x) + r.x;

	// w = (w - uExtraRange.x) / (uExtraRange.y - uExtraRange.x);

	// w = clamp(w, 0.0, 1.0);

	// vec3 color = texture2D(gradient, vec2(w,1.0-w)).rgb;

	return color;
}

vec3 getColor(){
	vec3 color;
	
	#ifdef color_type_rgba
		color = getRGB();
	#elif defined color_type_height || defined color_type_elevation
		color = getElevation();
	#elif defined color_type_rgb_height
		vec3 cHeight = getElevation();
		color = (1.0 - uTransition) * getRGB() + uTransition * cHeight;
	#elif defined color_type_depth
		float linearDepth = gl_Position.w;
		float expDepth = (gl_Position.z / gl_Position.w) * 0.5 + 0.5;
		color = vec3(linearDepth, expDepth, 0.0);
		//color = vec3(1.0, 0.5, 0.3);
	#elif defined color_type_intensity
		float w = getIntensity();
		color = vec3(w, w, w);
	#elif defined color_type_gps_time
		color = getGpsTime();
	#elif defined color_type_intensity_gradient
		float w = getIntensity();
		color = texture2D(gradient, vec2(w,1.0-w)).rgb;
	#elif defined color_type_color
		color = uColor;
	#elif defined color_type_level_of_detail
		float depth = getLOD();
		float w = depth / 10.0;
		color = texture2D(gradient, vec2(w,1.0-w)).rgb;
	#elif defined color_type_indices
		color = indices.rgb;
	#elif defined color_type_classification
		vec4 cl = getClassification(); 
		color = cl.rgb;
	#elif defined color_type_return_number
		color = getReturnNumber();
	#elif defined color_type_returns
		color = getReturns();
	#elif defined color_type_number_of_returns
		color = getNumberOfReturns();
	#elif defined color_type_source_id
		color = getSourceID();
	#elif defined color_type_point_source_id
		color = getSourceID();
	#elif defined color_type_normal
		color = (modelMatrix * vec4(normal, 0.0)).xyz;
	#elif defined color_type_phong
		color = color;
	#elif defined color_type_composite
		color = getCompositeColor();
	#elif defined color_type_matcap
		color = getMatcap();
	#else 
		color = getExtra();
	#endif
	
	if (backfaceCulling && applyBackfaceCulling()) {
		color = vec3(0.);
	}

	return color;
}

float getPointSize(){
	float pointSize = 1.0;
	
	float slope = tan(fov / 2.0);
	float projFactor = -0.5 * uScreenHeight / (slope * vViewPosition.z);

	float scale = length(
		modelViewMatrix * vec4(0, 0, 0, 1) - 
		modelViewMatrix * vec4(uOctreeSpacing, 0, 0, 1)
	) / uOctreeSpacing;
	projFactor = projFactor * scale;
	
	float r = uOctreeSpacing * 1.7;
	vRadius = r;
	#if defined fixed_point_size
		pointSize = size;
	#elif defined attenuated_point_size
		if(uUseOrthographicCamera){
			pointSize = size;
		}else{
			pointSize = size * spacing * projFactor;
			//pointSize = pointSize * projFactor;
		}
	#elif defined adaptive_point_size
		if(uUseOrthographicCamera) {
			float worldSpaceSize = 1.0 * size * r / getPointSizeAttenuation();
			pointSize = (worldSpaceSize / uOrthoWidth) * uScreenWidth;
		} else {
			float worldSpaceSize = 1.0 * size * r / getPointSizeAttenuation();
			pointSize = worldSpaceSize * projFactor;
		}
	#endif

	pointSize = max(minSize, pointSize);
	pointSize = min(maxSize, pointSize);
	
	vRadius = pointSize / projFactor;

	return pointSize;
}

#if defined(num_clippolygons) && num_clippolygons > 0
bool pointInClipPolygon(vec3 point, int polyIdx) {

	mat4 wvp = uClipPolygonWVP[polyIdx];
	//vec4 screenClipPos = uClipPolygonVP[polyIdx] * modelMatrix * vec4(point, 1.0);
	//screenClipPos.xy = screenClipPos.xy / screenClipPos.w * 0.5 + 0.5;

	vec4 pointNDC = wvp * vec4(point, 1.0);
	pointNDC.xy = pointNDC.xy / pointNDC.w;

	int j = uClipPolygonVCount[polyIdx] - 1;
	bool c = false;
	for(int i = 0; i < 8; i++) {
		if(i == uClipPolygonVCount[polyIdx]) {
			break;
		}

		//vec4 verti = wvp * vec4(uClipPolygonVertices[polyIdx * 8 + i], 1);
		//vec4 vertj = wvp * vec4(uClipPolygonVertices[polyIdx * 8 + j], 1);

		//verti.xy = verti.xy / verti.w;
		//vertj.xy = vertj.xy / vertj.w;

		//verti.xy = verti.xy / verti.w * 0.5 + 0.5;
		//vertj.xy = vertj.xy / vertj.w * 0.5 + 0.5;

		vec3 verti = uClipPolygonVertices[polyIdx * 8 + i];
		vec3 vertj = uClipPolygonVertices[polyIdx * 8 + j];

		if( ((verti.y > pointNDC.y) != (vertj.y > pointNDC.y)) && 
			(pointNDC.x < (vertj.x-verti.x) * (pointNDC.y-verti.y) / (vertj.y-verti.y) + verti.x) ) {
			c = !c;
		}
		j = i;
	}

	return c;
}
#endif

void doClipping(){

	{
		vec4 cl = getClassification(); 
		if(cl.a == 0.0){
			gl_Position = vec4(100.0, 100.0, 100.0, 0.0);
			
			return;
		}
	}

	#if defined(clip_return_number_enabled)
	{ // return number filter
		vec2 range = uFilterReturnNumberRange;
		if(returnNumber < range.x || returnNumber > range.y){
			gl_Position = vec4(100.0, 100.0, 100.0, 0.0);
			
			return;
		}
	}
	#endif

	#if defined(clip_number_of_returns_enabled)
	{ // number of return filter
		vec2 range = uFilterNumberOfReturnsRange;
		if(numberOfReturns < range.x || numberOfReturns > range.y){
			gl_Position = vec4(100.0, 100.0, 100.0, 0.0);
			
			return;
		}
	}
	#endif

	#if defined(clip_gps_enabled)
	{ // GPS time filter
		float time = (gpsTime + uGpsOffset) * uGpsScale;
		vec2 range = uFilterGPSTimeClipRange;

		if(time < range.x || time > range.y){
			gl_Position = vec4(100.0, 100.0, 100.0, 0.0);
			
			return;
		}
	}
	#endif

	#if defined(clip_point_source_id_enabled)
	{ // point source id filter
		vec2 range = uFilterPointSourceIDClipRange;
		if(pointSourceID < range.x || pointSourceID > range.y){
			gl_Position = vec4(100.0, 100.0, 100.0, 0.0);
			
			return;
		}
	}
	#endif

	int clipVolumesCount = 0;
	int insideCount = 0;

	#if defined(num_clipboxes) && num_clipboxes > 0
		for(int i = 0; i < num_clipboxes; i++){
			vec4 clipPosition = clipBoxes[i] * modelMatrix * vec4( position, 1.0 );
			bool inside = -0.5 <= clipPosition.x && clipPosition.x <= 0.5;
			inside = inside && -0.5 <= clipPosition.y && clipPosition.y <= 0.5;
			inside = inside && -0.5 <= clipPosition.z && clipPosition.z <= 0.5;

			insideCount = insideCount + (inside ? 1 : 0);
			clipVolumesCount++;
		}	
	#endif

	#if defined(num_clippolygons) && num_clippolygons > 0
		for(int i = 0; i < num_clippolygons; i++) {
			bool inside = pointInClipPolygon(position, i);

			insideCount = insideCount + (inside ? 1 : 0);
			clipVolumesCount++;
		}
	#endif

	bool insideAny = insideCount > 0;
	bool insideAll = (clipVolumesCount > 0) && (clipVolumesCount == insideCount);

	if(clipMethod == CLIPMETHOD_INSIDE_ANY){
		if(insideAny && clipTask == CLIPTASK_HIGHLIGHT){
			vColor.r += 0.5;
		}else if(!insideAny && clipTask == CLIPTASK_SHOW_INSIDE){
			gl_Position = vec4(100.0, 100.0, 100.0, 1.0);
		}else if(insideAny && clipTask == CLIPTASK_SHOW_OUTSIDE){
			gl_Position = vec4(100.0, 100.0, 100.0, 1.0);
		}
	}else if(clipMethod == CLIPMETHOD_INSIDE_ALL){
		if(insideAll && clipTask == CLIPTASK_HIGHLIGHT){
			vColor.r += 0.5;
		}else if(!insideAll && clipTask == CLIPTASK_SHOW_INSIDE){
			gl_Position = vec4(100.0, 100.0, 100.0, 1.0);
		}else if(insideAll && clipTask == CLIPTASK_SHOW_OUTSIDE){
			gl_Position = vec4(100.0, 100.0, 100.0, 1.0);
		}
	}
}



// 
// ##     ##    ###    #### ##    ## 
// ###   ###   ## ##    ##  ###   ## 
// #### ####  ##   ##   ##  ####  ## 
// ## ### ## ##     ##  ##  ## ## ## 
// ##     ## #########  ##  ##  #### 
// ##     ## ##     ##  ##  ##   ### 
// ##     ## ##     ## #### ##    ## 
//

void main() {
	vec4 mvPosition = modelViewMatrix * vec4(position, 1.0 );
	vViewPosition = mvPosition.xyz;
	gl_Position = projectionMatrix * mvPosition;
	vLogDepth = log2(-mvPosition.z);

	//gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
	//gl_PointSize = 5.0;

	// POINT SIZE
	float pointSize = getPointSize();
	//float pointSize = 2.0;
	gl_PointSize = pointSize;
	vPointSize = pointSize;

	// COLOR
	vColor = getColor();
	// vColor = vec3(1.0, 0.0, 0.0);

	//gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
	//gl_Position = vec4(position.xzy / 1000.0, 1.0 );

	//gl_PointSize = 5.0;
	//vColor = vec3(1.0, 1.0, 1.0);

	// only for "replacing" approaches
	// if(getLOD() != uLevel){
	// 	gl_Position = vec4(10.0, 10.0, 10.0, 1.0);
	// }


	#if defined hq_depth_pass
		float originalDepth = gl_Position.w;
		float adjustedDepth = originalDepth + 2.0 * vRadius;
		float adjust = adjustedDepth / originalDepth;

		mvPosition.xyz = mvPosition.xyz * adjust;
		gl_Position = projectionMatrix * mvPosition;
	#endif


	// CLIPPING
	doClipping();

	#if defined(num_clipspheres) && num_clipspheres > 0
		for(int i = 0; i < num_clipspheres; i++){
			vec4 sphereLocal = uClipSpheres[i] * mvPosition;

			float distance = length(sphereLocal.xyz);

			if(distance < 1.0){
				float w = distance;
				vec3 cGradient = texture2D(gradient, vec2(w, 1.0 - w)).rgb;
				
				vColor = cGradient;
				//vColor = cGradient * 0.7 + vColor * 0.3;
			}
		}
	#endif

	#if defined(num_shadowmaps) && num_shadowmaps > 0

		const float sm_near = 0.1;
		const float sm_far = 10000.0;

		for(int i = 0; i < num_shadowmaps; i++){
			vec3 viewPos = (uShadowWorldView[i] * vec4(position, 1.0)).xyz;
			float distanceToLight = abs(viewPos.z);
			
			vec4 projPos = uShadowProj[i] * uShadowWorldView[i] * vec4(position, 1);
			vec3 nc = projPos.xyz / projPos.w;
			
			float u = nc.x * 0.5 + 0.5;
			float v = nc.y * 0.5 + 0.5;

			vec2 sampleStep = vec2(1.0 / (2.0*1024.0), 1.0 / (2.0*1024.0)) * 1.5;
			vec2 sampleLocations[9];
			sampleLocations[0] = vec2(0.0, 0.0);
			sampleLocations[1] = sampleStep;
			sampleLocations[2] = -sampleStep;
			sampleLocations[3] = vec2(sampleStep.x, -sampleStep.y);
			sampleLocations[4] = vec2(-sampleStep.x, sampleStep.y);

			sampleLocations[5] = vec2(0.0, sampleStep.y);
			sampleLocations[6] = vec2(0.0, -sampleStep.y);
			sampleLocations[7] = vec2(sampleStep.x, 0.0);
			sampleLocations[8] = vec2(-sampleStep.x, 0.0);

			float visibleSamples = 0.0;
			float numSamples = 0.0;

			float bias = vRadius * 2.0;

			for(int j = 0; j < 9; j++){
				vec4 depthMapValue = texture2D(uShadowMap[i], vec2(u, v) + sampleLocations[j]);

				float linearDepthFromSM = depthMapValue.x + bias;
				float linearDepthFromViewer = distanceToLight;

				if(linearDepthFromSM > linearDepthFromViewer){
					visibleSamples += 1.0;
				}

				numSamples += 1.0;
			}

			float visibility = visibleSamples / numSamples;

			if(u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0 || nc.x < -1.0 || nc.x > 1.0 || nc.y < -1.0 || nc.y > 1.0 || nc.z < -1.0 || nc.z > 1.0){
				//vColor = vec3(0.0, 0.0, 0.2);
			}else{
				//vColor = vec3(1.0, 1.0, 1.0) * visibility + vec3(1.0, 1.0, 1.0) * vec3(0.5, 0.0, 0.0) * (1.0 - visibility);
				vColor = vColor * visibility + vColor * uShadowColor * (1.0 - visibility);
			}


		}

	#endif
}
`;

	Shaders["pointcloud.fs"] = `
#if defined paraboloid_point_shape
	#extension GL_EXT_frag_depth : enable
#endif

precision highp float;
precision highp int;

uniform mat4 viewMatrix;
uniform mat4 uViewInv;
uniform mat4 uProjInv;
uniform vec3 cameraPosition;


uniform mat4 projectionMatrix;
uniform float uOpacity;

uniform float blendHardness;
uniform float blendDepthSupplement;
uniform float fov;
uniform float uSpacing;
uniform float near;
uniform float far;
uniform float uPCIndex;
uniform float uScreenWidth;
uniform float uScreenHeight;

varying vec3	vColor;
varying float	vLogDepth;
varying vec3	vViewPosition;
varying float	vRadius;
varying float 	vPointSize;
varying vec3 	vPosition;


float specularStrength = 1.0;

void main() {

	// gl_FragColor = vec4(vColor, 1.0);

	vec3 color = vColor;
	float depth = gl_FragCoord.z;

	#if defined(circle_point_shape) || defined(paraboloid_point_shape) 
		float u = 2.0 * gl_PointCoord.x - 1.0;
		float v = 2.0 * gl_PointCoord.y - 1.0;
	#endif
	
	#if defined(circle_point_shape) 
		float cc = u*u + v*v;
		if(cc > 1.0){
			discard;
		}
	#endif
		
	#if defined color_type_indices
		gl_FragColor = vec4(color, uPCIndex / 255.0);
	#else
		gl_FragColor = vec4(color, uOpacity);
	#endif

	#if defined paraboloid_point_shape
		float wi = 0.0 - ( u*u + v*v);
		vec4 pos = vec4(vViewPosition, 1.0);
		pos.z += wi * vRadius;
		float linearDepth = -pos.z;
		pos = projectionMatrix * pos;
		pos = pos / pos.w;
		float expDepth = pos.z;
		depth = (pos.z + 1.0) / 2.0;
		gl_FragDepthEXT = depth;
		
		#if defined(color_type_depth)
			color.r = linearDepth;
			color.g = expDepth;
		#endif
		
		#if defined(use_edl)
			gl_FragColor.a = log2(linearDepth);
		#endif
		
	#else
		#if defined(use_edl)
			gl_FragColor.a = vLogDepth;
		#endif
	#endif

	#if defined(weighted_splats)
		float distance = 2.0 * length(gl_PointCoord.xy - 0.5);
		float weight = max(0.0, 1.0 - distance);
		weight = pow(weight, 1.5);

		gl_FragColor.a = weight;
		gl_FragColor.xyz = gl_FragColor.xyz * weight;
	#endif

	//gl_FragColor = vec4(0.0, 0.7, 0.0, 1.0);
	
}


`;

	Shaders["pointcloud_sm.vs"] = `
precision mediump float;
precision mediump int;

attribute vec3 position;
attribute vec3 color;

uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;

uniform float uScreenWidth;
uniform float uScreenHeight;
uniform float near;
uniform float far;

uniform float uSpacing;
uniform float uOctreeSize;
uniform float uLevel;
uniform float uVNStart;

uniform sampler2D visibleNodes;

varying float vLinearDepth;
varying vec3 vColor;

#define PI 3.141592653589793



// ---------------------
// OCTREE
// ---------------------

#if defined(adaptive_point_size)
/**
 * number of 1-bits up to inclusive index position
 * number is treated as if it were an integer in the range 0-255
 *
 */
float numberOfOnes(float number, float index){
	float tmp = mod(number, pow(2.0, index + 1.0));
	float numOnes = 0.0;
	for(float i = 0.0; i < 8.0; i++){
		if(mod(tmp, 2.0) != 0.0){
			numOnes++;
		}
		tmp = floor(tmp / 2.0);
	}
	return numOnes;
}


/**
 * checks whether the bit at index is 1
 * number is treated as if it were an integer in the range 0-255
 *
 */
bool isBitSet(float number, float index){
	return mod(floor(number / pow(2.0, index)), 2.0) != 0.0;
}


/**
 * find the LOD at the point position
 */
float getLOD(){
	
	vec3 offset = vec3(0.0, 0.0, 0.0);
	float iOffset = uVNStart;
	float depth = uLevel;
	for(float i = 0.0; i <= 30.0; i++){
		float nodeSizeAtLevel = uOctreeSize  / pow(2.0, i + uLevel + 0.0);
		
		vec3 index3d = (position-offset) / nodeSizeAtLevel;
		index3d = floor(index3d + 0.5);
		float index = 4.0 * index3d.x + 2.0 * index3d.y + index3d.z;
		
		vec4 value = texture2D(visibleNodes, vec2(iOffset / 2048.0, 0.0));
		float mask = value.r * 255.0;
		if(isBitSet(mask, index)){
			// there are more visible child nodes at this position
			iOffset = iOffset + value.g * 255.0 * 256.0 + value.b * 255.0 + numberOfOnes(mask, index - 1.0);
			depth++;
		}else{
			// no more visible child nodes at this position
			return depth;
		}
		
		offset = offset + (vec3(1.0, 1.0, 1.0) * nodeSizeAtLevel * 0.5) * index3d;
	}
		
	return depth;
}

#endif

float getPointSize(){
	float pointSize = 1.0;
	
	float slope = tan(fov / 2.0);
	float projFactor =  -0.5 * uScreenHeight / (slope * vViewPosition.z);
	
	float r = uOctreeSpacing * 1.5;
	vRadius = r;
	#if defined fixed_point_size
		pointSize = size;
	#elif defined attenuated_point_size
		if(uUseOrthographicCamera){
			pointSize = size;			
		}else{
			pointSize = pointSize * projFactor;
		}
	#elif defined adaptive_point_size
		if(uUseOrthographicCamera) {
			float worldSpaceSize = 1.5 * size * r / getPointSizeAttenuation();
			pointSize = (worldSpaceSize / uOrthoWidth) * uScreenWidth;
		} else {
			float worldSpaceSize = 1.5 * size * r / getPointSizeAttenuation();
			pointSize = worldSpaceSize * projFactor;
		}
	#endif

	pointSize = max(minSize, pointSize);
	pointSize = min(maxSize, pointSize);
	
	vRadius = pointSize / projFactor;

	return pointSize;
}


void main() {

	vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
	vLinearDepth = gl_Position.w;

	float pointSize = getPointSize();
	gl_PointSize = pointSize;

}
`;

	Shaders["pointcloud_sm.fs"] = `
precision mediump float;
precision mediump int;

varying vec3 vColor;
varying float vLinearDepth;

void main() {

	//gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
	//gl_FragColor = vec4(vColor, 1.0);
	//gl_FragColor = vec4(vLinearDepth, pow(vLinearDepth, 2.0), 0.0, 1.0);
	gl_FragColor = vec4(vLinearDepth, vLinearDepth / 30.0, vLinearDepth / 30.0, 1.0);
	
}


`;

	Shaders["normalize.vs"] = `
precision mediump float;
precision mediump int;

attribute vec3 position;
attribute vec2 uv;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

varying vec2 vUv;

void main() {
	vUv = uv;
	gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}`;

	Shaders["normalize.fs"] = `
#extension GL_EXT_frag_depth : enable

precision mediump float;
precision mediump int;

uniform sampler2D uWeightMap;
uniform sampler2D uDepthMap;

varying vec2 vUv;

void main() {
	float depth = texture2D(uDepthMap, vUv).r;
	
	if(depth >= 1.0){
		discard;
	}

	gl_FragColor = vec4(depth, 1.0, 0.0, 1.0);

	vec4 color = texture2D(uWeightMap, vUv); 
	color = color / color.w;
	
	gl_FragColor = vec4(color.xyz, 1.0); 
	
	gl_FragDepthEXT = depth;


}`;

	Shaders["normalize_and_edl.fs"] = `
#extension GL_EXT_frag_depth : enable

// 
// adapted from the EDL shader code from Christian Boucheny in cloud compare:
// https://github.com/cloudcompare/trunk/tree/master/plugins/qEDL/shaders/EDL
//

precision mediump float;
precision mediump int;

uniform sampler2D uWeightMap;
uniform sampler2D uEDLMap;
uniform sampler2D uDepthMap;

uniform float screenWidth;
uniform float screenHeight;
uniform vec2 neighbours[NEIGHBOUR_COUNT];
uniform float edlStrength;
uniform float radius;

varying vec2 vUv;

float response(float depth){
	vec2 uvRadius = radius / vec2(screenWidth, screenHeight);
	
	float sum = 0.0;
	
	for(int i = 0; i < NEIGHBOUR_COUNT; i++){
		vec2 uvNeighbor = vUv + uvRadius * neighbours[i];
		
		float neighbourDepth = texture2D(uEDLMap, uvNeighbor).a;

		if(neighbourDepth != 0.0){
			if(depth == 0.0){
				sum += 100.0;
			}else{
				sum += max(0.0, depth - neighbourDepth);
			}
		}
	}
	
	return sum / float(NEIGHBOUR_COUNT);
}

void main() {

	float edlDepth = texture2D(uEDLMap, vUv).a;
	float res = response(edlDepth);
	float shade = exp(-res * 300.0 * edlStrength);

	float depth = texture2D(uDepthMap, vUv).r;
	if(depth >= 1.0 && res == 0.0){
		discard;
	}
	
	vec4 color = texture2D(uWeightMap, vUv); 
	color = color / color.w;
	color = color * shade;

	gl_FragColor = vec4(color.xyz, 1.0); 

	gl_FragDepthEXT = depth;
}`;

	Shaders["edl.vs"] = `
precision mediump float;
precision mediump int;

attribute vec3 position;
attribute vec2 uv;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

varying vec2 vUv;

void main() {
	vUv = uv;
	
	vec4 mvPosition = modelViewMatrix * vec4(position,1.0);

	gl_Position = projectionMatrix * mvPosition;
}`;

	Shaders["edl.fs"] = `
#extension GL_EXT_frag_depth : enable

// 
// adapted from the EDL shader code from Christian Boucheny in cloud compare:
// https://github.com/cloudcompare/trunk/tree/master/plugins/qEDL/shaders/EDL
//

precision mediump float;
precision mediump int;

uniform float screenWidth;
uniform float screenHeight;
uniform vec2 neighbours[NEIGHBOUR_COUNT];
uniform float edlStrength;
uniform float radius;
uniform float opacity;

uniform float uNear;
uniform float uFar;

uniform mat4 uProj;

uniform sampler2D uEDLColor;
uniform sampler2D uEDLDepth;

varying vec2 vUv;

float response(float depth){
	vec2 uvRadius = radius / vec2(screenWidth, screenHeight);
	
	float sum = 0.0;
	
	for(int i = 0; i < NEIGHBOUR_COUNT; i++){
		vec2 uvNeighbor = vUv + uvRadius * neighbours[i];
		
		float neighbourDepth = texture2D(uEDLColor, uvNeighbor).a;
		neighbourDepth = (neighbourDepth == 1.0) ? 0.0 : neighbourDepth;

		if(neighbourDepth != 0.0){
			if(depth == 0.0){
				sum += 100.0;
			}else{
				sum += max(0.0, depth - neighbourDepth);
			}
		}
	}
	
	return sum / float(NEIGHBOUR_COUNT);
}

void main(){
	vec4 cEDL = texture2D(uEDLColor, vUv);
	
	float depth = cEDL.a;
	depth = (depth == 1.0) ? 0.0 : depth;
	float res = response(depth);
	float shade = exp(-res * 300.0 * edlStrength);

	gl_FragColor = vec4(cEDL.rgb * shade, opacity);

	{ // write regular hyperbolic depth values to depth buffer
		float dl = pow(2.0, depth);

		vec4 dp = uProj * vec4(0.0, 0.0, -dl, 1.0);
		float pz = dp.z / dp.w;
		float fragDepth = (pz + 1.0) / 2.0;

		gl_FragDepthEXT = fragDepth;
	}

	if(depth == 0.0){
		discard;
	}

}
`;

	Shaders["blur.vs"] = `
varying vec2 vUv;

void main() {
	vUv = uv;

	gl_Position =   projectionMatrix * modelViewMatrix * vec4(position,1.0);
}`;

	Shaders["blur.fs"] = `
uniform mat4 projectionMatrix;

uniform float screenWidth;
uniform float screenHeight;
uniform float near;
uniform float far;

uniform sampler2D map;

varying vec2 vUv;

void main() {

	float dx = 1.0 / screenWidth;
	float dy = 1.0 / screenHeight;

	vec3 color = vec3(0.0, 0.0, 0.0);
	color += texture2D(map, vUv + vec2(-dx, -dy)).rgb;
	color += texture2D(map, vUv + vec2(  0, -dy)).rgb;
	color += texture2D(map, vUv + vec2(+dx, -dy)).rgb;
	color += texture2D(map, vUv + vec2(-dx,   0)).rgb;
	color += texture2D(map, vUv + vec2(  0,   0)).rgb;
	color += texture2D(map, vUv + vec2(+dx,   0)).rgb;
	color += texture2D(map, vUv + vec2(-dx,  dy)).rgb;
	color += texture2D(map, vUv + vec2(  0,  dy)).rgb;
	color += texture2D(map, vUv + vec2(+dx,  dy)).rgb;

	color = color / 9.0;
	
	gl_FragColor = vec4(color, 1.0);
}`;

	const ClassificationScheme = {

		DEFAULT: {
			0:       { visible: true, name: 'never classified'  , color: [0.5,  0.5,  0.5,  1.0] },
			1:       { visible: true, name: 'unclassified'      , color: [0.5,  0.5,  0.5,  1.0] },
			2:       { visible: true, name: 'ground'            , color: [0.63, 0.32, 0.18, 1.0] },
			3:       { visible: true, name: 'low vegetation'    , color: [0.0,  1.0,  0.0,  1.0] },
			4:       { visible: true, name: 'medium vegetation' , color: [0.0,  0.8,  0.0,  1.0] },
			5:       { visible: true, name: 'high vegetation'   , color: [0.0,  0.6,  0.0,  1.0] },
			6:       { visible: true, name: 'building'          , color: [1.0,  0.66, 0.0,  1.0] },
			7:       { visible: true, name: 'low point(noise)'  , color: [1.0,  0.0,  1.0,  1.0] },
			8:       { visible: true, name: 'key-point'         , color: [1.0,  0.0,  0.0,  1.0] },
			9:       { visible: true, name: 'water'             , color: [0.0,  0.0,  1.0,  1.0] },
			12:      { visible: true, name: 'overlap'           , color: [1.0,  1.0,  0.0,  1.0] },
			DEFAULT: { visible: true, name: 'default'           , color: [0.3,  0.6,  0.6,  0.5] },
		}
	};

	Object.defineProperty(ClassificationScheme, 'RANDOM', {
		get: function() { 

			let scheme = {};

			for(let i = 0; i <= 255; i++){
				scheme[i] = new Vector4(Math.random(), Math.random(), Math.random());
			}

			scheme["DEFAULT"] = new Vector4(Math.random(), Math.random(), Math.random());

			return scheme;
		}
	});

	//
	// how to calculate the radius of a projected sphere in screen space
	// http://stackoverflow.com/questions/21648630/radius-of-projected-sphere-in-screen-space
	// http://stackoverflow.com/questions/3717226/radius-of-projected-sphere
	//


	class PointCloudMaterial$1 extends RawShaderMaterial {
		constructor (parameters = {}) {
			super();

			this.visibleNodesTexture = Utils.generateDataTexture(2048, 1, new Color(0xffffff));
			this.visibleNodesTexture.minFilter = NearestFilter;
			this.visibleNodesTexture.magFilter = NearestFilter;

			let getValid = (a, b) => {
				if(a !== undefined){
					return a;
				}else {
					return b;
				}
			};

			let pointSize = getValid(parameters.size, 1.0);
			let minSize = getValid(parameters.minSize, 1.0);
			let maxSize = getValid(parameters.maxSize, 50.0);
			let treeType = getValid(parameters.treeType, TreeType.OCTREE);

			this._pointSizeType = PointSizeType.FIXED;
			this._shape = PointShape.SQUARE;
			this._useClipBox = false;
			this.clipBoxes = [];
			this.clipPolygons = [];
			this._weighted = false;
			this._gradient = Gradients.SPECTRAL;
			this.gradientTexture = PointCloudMaterial$1.generateGradientTexture(this._gradient);
			this._matcap = "matcap.jpg";
			this.matcapTexture = Potree.PointCloudMaterial.generateMatcapTexture(this._matcap);
			this.lights = false;
			this.fog = false;
			this._treeType = treeType;
			this._useEDL = false;
			this.defines = new Map();

			this.ranges = new Map();

			this._activeAttributeName = null;

			this._defaultIntensityRangeChanged = false;
			this._defaultElevationRangeChanged = false;

			{
				const [width, height] = [256, 1];
				let data = new Uint8Array(width * 4);
				let texture = new DataTexture(data, width, height, RGBAFormat);
				texture.magFilter = NearestFilter;
				texture.needsUpdate = true;

				this.classificationTexture = texture;
			}

			this.attributes = {
				position: { type: 'fv', value: [] },
				color: { type: 'fv', value: [] },
				normal: { type: 'fv', value: [] },
				intensity: { type: 'f', value: [] },
				classification: { type: 'f', value: [] },
				returnNumber: { type: 'f', value: [] },
				numberOfReturns: { type: 'f', value: [] },
				pointSourceID: { type: 'f', value: [] },
				indices: { type: 'fv', value: [] }
			};

			this.uniforms = {
				level:				{ type: "f", value: 0.0 },
				vnStart:			{ type: "f", value: 0.0 },
				spacing:			{ type: "f", value: 1.0 },
				blendHardness:		{ type: "f", value: 2.0 },
				blendDepthSupplement:	{ type: "f", value: 0.0 },
				fov:				{ type: "f", value: 1.0 },
				screenWidth:		{ type: "f", value: 1.0 },
				screenHeight:		{ type: "f", value: 1.0 },
				near:				{ type: "f", value: 0.1 },
				far:				{ type: "f", value: 1.0 },
				uColor:				{ type: "c", value: new Color( 0xffffff ) },
				uOpacity:			{ type: "f", value: 1.0 },
				size:				{ type: "f", value: pointSize },
				minSize:			{ type: "f", value: minSize },
				maxSize:			{ type: "f", value: maxSize },
				octreeSize:			{ type: "f", value: 0 },
				bbSize:				{ type: "fv", value: [0, 0, 0] },
				elevationRange:		{ type: "2fv", value: [0, 0] },

				clipBoxCount:		{ type: "f", value: 0 },
				//clipSphereCount:	{ type: "f", value: 0 },
				clipPolygonCount:	{ type: "i", value: 0 },
				clipBoxes:			{ type: "Matrix4fv", value: [] },
				//clipSpheres:		{ type: "Matrix4fv", value: [] },
				clipPolygons:		{ type: "3fv", value: [] },
				clipPolygonVCount:	{ type: "iv", value: [] },
				clipPolygonVP:		{ type: "Matrix4fv", value: [] },

				visibleNodes:		{ type: "t", value: this.visibleNodesTexture },
				pcIndex:			{ type: "f", value: 0 },
				gradient:			{ type: "t", value: this.gradientTexture },
				classificationLUT:	{ type: "t", value: this.classificationTexture },
				uHQDepthMap:		{ type: "t", value: null },
				toModel:			{ type: "Matrix4f", value: [] },
				diffuse:			{ type: "fv", value: [1, 1, 1] },
				transition:			{ type: "f", value: 0.5 },

				 intensityRange:		{ type: "fv", value: [Infinity, -Infinity] },

				intensity_gbc: 		{ type: "fv", value: [1, 0, 0]},
				uRGB_gbc:	 		{ type: "fv", value: [1, 0, 0]},
				// intensityGamma:		{ type: "f", value: 1 },
				// intensityContrast:	{ type: "f", value: 0 },
				// intensityBrightness:{ type: "f", value: 0 },
				// rgbGamma:			{ type: "f", value: 1 },
				// rgbContrast:		{ type: "f", value: 0 },
				// rgbBrightness:		{ type: "f", value: 0 },
				wRGB:				{ type: "f", value: 1 },
				wIntensity:			{ type: "f", value: 0 },
				wElevation:			{ type: "f", value: 0 },
				wClassification:	{ type: "f", value: 0 },
				wReturnNumber:		{ type: "f", value: 0 },
				wSourceID:			{ type: "f", value: 0 },
				useOrthographicCamera: { type: "b", value: false },
				elevationGradientRepat: { type: "i", value: ElevationGradientRepeat.CLAMP },
				clipTask:			{ type: "i", value: 1 },
				clipMethod:			{ type: "i", value: 1 },
				uShadowColor:		{ type: "3fv", value: [0, 0, 0] },

				uExtraScale:		{ type: "f", value: 1},
				uExtraOffset:		{ type: "f", value: 0},
				uExtraRange:		{ type: "2fv", value: [0, 1] },
				uExtraGammaBrightContr:	{ type: "3fv", value: [1, 0, 0] },

				uFilterReturnNumberRange:		{ type: "fv", value: [0, 7]},
				uFilterNumberOfReturnsRange:	{ type: "fv", value: [0, 7]},
				uFilterGPSTimeClipRange:		{ type: "fv", value: [0, 7]},
				uFilterPointSourceIDClipRange:		{ type: "fv", value: [0, 65535]},
				matcapTextureUniform: 	{ type: "t", value: this.matcapTexture },
				backfaceCulling: { type: "b", value: false },
			};

			this.classification = ClassificationScheme.DEFAULT;

			this.defaultAttributeValues.normal = [0, 0, 0];
			this.defaultAttributeValues.classification = [0, 0, 0];
			this.defaultAttributeValues.indices = [0, 0, 0, 0];

			this.vertexShader = Shaders['pointcloud.vs'];
			this.fragmentShader = Shaders['pointcloud.fs'];
			
			this.vertexColors = VertexColors;

			this.updateShaderSource();
		}

		setDefine(key, value){
			if(value !== undefined && value !== null){
				if(this.defines.get(key) !== value){
					this.defines.set(key, value);
					this.updateShaderSource();
				}
			}else {
				this.removeDefine(key);
			}
		}

		removeDefine(key){
			this.defines.delete(key);
		}

		updateShaderSource () {

			let vs = Shaders['pointcloud.vs'];
			let fs = Shaders['pointcloud.fs'];
			let definesString = this.getDefines();

			let vsVersionIndex = vs.indexOf("#version ");
			let fsVersionIndex = fs.indexOf("#version ");

			if(vsVersionIndex >= 0){
				vs = vs.replace(/(#version .*)/, `$1\n${definesString}`);
			}else {
				vs = `${definesString}\n${vs}`;
			}

			if(fsVersionIndex >= 0){
				fs = fs.replace(/(#version .*)/, `$1\n${definesString}`);
			}else {
				fs = `${definesString}\n${fs}`;
			}

			this.vertexShader = vs;
			this.fragmentShader = fs;

			if (this.opacity === 1.0) {
				this.blending = NoBlending;
				this.transparent = false;
				this.depthTest = true;
				this.depthWrite = true;
				this.depthFunc = LessEqualDepth;
			} else if (this.opacity < 1.0 && !this.useEDL) {
				this.blending = AdditiveBlending;
				this.transparent = true;
				this.depthTest = false;
				this.depthWrite = true;
				this.depthFunc = AlwaysDepth;
			}

			if (this.weighted) {
				this.blending = AdditiveBlending;
				this.transparent = true;
				this.depthTest = true;
				this.depthWrite = false;
			}

			this.needsUpdate = true;
		}

		getDefines () {
			let defines = [];

			if (this.pointSizeType === PointSizeType.FIXED) {
				defines.push('#define fixed_point_size');
			} else if (this.pointSizeType === PointSizeType.ATTENUATED) {
				defines.push('#define attenuated_point_size');
			} else if (this.pointSizeType === PointSizeType.ADAPTIVE) {
				defines.push('#define adaptive_point_size');
			}

			if (this.shape === PointShape.SQUARE) {
				defines.push('#define square_point_shape');
			} else if (this.shape === PointShape.CIRCLE) {
				defines.push('#define circle_point_shape');
			} else if (this.shape === PointShape.PARABOLOID) {
				defines.push('#define paraboloid_point_shape');
			}

			if (this._useEDL) {
				defines.push('#define use_edl');
			}

			if(this.activeAttributeName){
				let attributeName = this.activeAttributeName.replace(/[^a-zA-Z0-9]/g, '_');

				defines.push(`#define color_type_${attributeName}`);
			}
			
			if(this._treeType === TreeType.OCTREE){
				defines.push('#define tree_type_octree');
			}else if(this._treeType === TreeType.KDTREE){
				defines.push('#define tree_type_kdtree');
			}

			if (this.weighted) {
				defines.push('#define weighted_splats');
			}

			for(let [key, value] of this.defines){
				defines.push(value);
			}

			return defines.join("\n");
		}

		setClipBoxes (clipBoxes) {
			if (!clipBoxes) {
				return;
			}

			let doUpdate = (this.clipBoxes.length !== clipBoxes.length) && (clipBoxes.length === 0 || this.clipBoxes.length === 0);

			this.uniforms.clipBoxCount.value = this.clipBoxes.length;
			this.clipBoxes = clipBoxes;

			if (doUpdate) {
				this.updateShaderSource();
			}

			this.uniforms.clipBoxes.value = new Float32Array(this.clipBoxes.length * 16);

			for (let i = 0; i < this.clipBoxes.length; i++) {
				let box = clipBoxes[i];

				this.uniforms.clipBoxes.value.set(box.inverse.elements, 16 * i);
			}

			for (let i = 0; i < this.uniforms.clipBoxes.value.length; i++) {
				if (Number.isNaN(this.uniforms.clipBoxes.value[i])) {
					this.uniforms.clipBoxes.value[i] = Infinity;
				}
			}
		}

		setClipPolygons(clipPolygons, maxPolygonVertices) {
			if(!clipPolygons){
				return;
			}

			this.clipPolygons = clipPolygons;

			let doUpdate = (this.clipPolygons.length !== clipPolygons.length);

			if(doUpdate){
				this.updateShaderSource();
			}
		}
		
		get gradient(){
			return this._gradient;
		}

		set gradient (value) {
			if (this._gradient !== value) {
				this._gradient = value;
				this.gradientTexture = PointCloudMaterial$1.generateGradientTexture(this._gradient);
				this.uniforms.gradient.value = this.gradientTexture;
			}
		}

		get matcap(){
			return this._matcap;
		}

		set matcap (value) {
			if (this._matcap !== value) {
				this._matcap = value;
				this.matcapTexture = Potree.PointCloudMaterial.generateMatcapTexture(this._matcap);
				this.uniforms.matcapTextureUniform.value = this.matcapTexture;
			}
		}
		get useOrthographicCamera() {
			return this.uniforms.useOrthographicCamera.value;
		}

		set useOrthographicCamera(value) {
			if(this.uniforms.useOrthographicCamera.value !== value){
				this.uniforms.useOrthographicCamera.value = value;
			}
		}
		get backfaceCulling() {
			return this.uniforms.backfaceCulling.value;
		}

		set backfaceCulling(value) {
			if(this.uniforms.backfaceCulling.value !== value){
				this.uniforms.backfaceCulling.value = value;
				this.dispatchEvent({type: 'backface_changed', target: this});
			}
		}

		recomputeClassification () {
			const classification = this.classification;
			const data = this.classificationTexture.image.data;

			let width = 256;
			const black = [1, 1, 1, 1];

			let valuesChanged = false;

			for (let i = 0; i < width; i++) {

				let color;
				let visible = true;

				if (classification[i]) {
					color = classification[i].color;
					visible = classification[i].visible;
				} else if (classification[i % 32]) {
					color = classification[i % 32].color;
					visible = classification[i % 32].visible;
				} else if(classification.DEFAULT) {
					color = classification.DEFAULT.color;
					visible = classification.DEFAULT.visible;
				}else {
					color = black;
				}

				const r = parseInt(255 * color[0]);
				const g = parseInt(255 * color[1]);
				const b = parseInt(255 * color[2]);
				const a = visible ? parseInt(255 * color[3]) : 0;


				if(data[4 * i + 0] !== r){
					data[4 * i + 0] = r;
					valuesChanged = true;
				}

				if(data[4 * i + 1] !== g){
					data[4 * i + 1] = g;
					valuesChanged = true;
				}

				if(data[4 * i + 2] !== b){
					data[4 * i + 2] = b;
					valuesChanged = true;
				}

				if(data[4 * i + 3] !== a){
					data[4 * i + 3] = a;
					valuesChanged = true;
				}
			}

			if(valuesChanged){
				this.classificationTexture.needsUpdate = true;

				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get spacing () {
			return this.uniforms.spacing.value;
		}

		set spacing (value) {
			if (this.uniforms.spacing.value !== value) {
				this.uniforms.spacing.value = value;
			}
		}

		get useClipBox () {
			return this._useClipBox;
		}

		set useClipBox (value) {
			if (this._useClipBox !== value) {
				this._useClipBox = value;
				this.updateShaderSource();
			}
		}

		get clipTask(){
			return this.uniforms.clipTask.value;
		}

		set clipTask(mode){
			this.uniforms.clipTask.value = mode;
		}

		get elevationGradientRepat(){
			return this.uniforms.elevationGradientRepat.value;
		}

		set elevationGradientRepat(mode){
			this.uniforms.elevationGradientRepat.value = mode;
		}

		get clipMethod(){
			return this.uniforms.clipMethod.value;
		}

		set clipMethod(mode){
			this.uniforms.clipMethod.value = mode;
		}

		get weighted(){
			return this._weighted;
		}

		set weighted (value) {
			if (this._weighted !== value) {
				this._weighted = value;
				this.updateShaderSource();
			}
		}

		get fov () {
			return this.uniforms.fov.value;
		}

		set fov (value) {
			if (this.uniforms.fov.value !== value) {
				this.uniforms.fov.value = value;
				// this.updateShaderSource();
			}
		}

		get screenWidth () {
			return this.uniforms.screenWidth.value;
		}

		set screenWidth (value) {
			if (this.uniforms.screenWidth.value !== value) {
				this.uniforms.screenWidth.value = value;
				// this.updateShaderSource();
			}
		}

		get screenHeight () {
			return this.uniforms.screenHeight.value;
		}

		set screenHeight (value) {
			if (this.uniforms.screenHeight.value !== value) {
				this.uniforms.screenHeight.value = value;
				// this.updateShaderSource();
			}
		}

		get near () {
			return this.uniforms.near.value;
		}

		set near (value) {
			if (this.uniforms.near.value !== value) {
				this.uniforms.near.value = value;
			}
		}

		get far () {
			return this.uniforms.far.value;
		}

		set far (value) {
			if (this.uniforms.far.value !== value) {
				this.uniforms.far.value = value;
			}
		}
		
		get opacity(){
			return this.uniforms.uOpacity.value;
		}

		set opacity (value) {
			if (this.uniforms && this.uniforms.uOpacity) {
				if (this.uniforms.uOpacity.value !== value) {
					this.uniforms.uOpacity.value = value;
					this.updateShaderSource();
					this.dispatchEvent({
						type: 'opacity_changed',
						target: this
					});
					this.dispatchEvent({
						type: 'material_property_changed',
						target: this
					});
				}
			}
		}

		get activeAttributeName(){
			return this._activeAttributeName;
		}

		set activeAttributeName(value){
			if (this._activeAttributeName !== value) {
				this._activeAttributeName = value;

				this.updateShaderSource();
				this.dispatchEvent({
					type: 'active_attribute_changed',
					target: this
				});

				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get pointSizeType () {
			return this._pointSizeType;
		}

		set pointSizeType (value) {
			if (this._pointSizeType !== value) {
				this._pointSizeType = value;
				this.updateShaderSource();
				this.dispatchEvent({
					type: 'point_size_type_changed',
					target: this
				});
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get useEDL(){
			return this._useEDL;
		}

		set useEDL (value) {
			if (this._useEDL !== value) {
				this._useEDL = value;
				this.updateShaderSource();
			}
		}

		get color () {
			return this.uniforms.uColor.value;
		}

		set color (value) {
			if (!this.uniforms.uColor.value.equals(value)) {
				this.uniforms.uColor.value.copy(value);
				
				this.dispatchEvent({
					type: 'color_changed',
					target: this
				});
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get shape () {
			return this._shape;
		}

		set shape (value) {
			if (this._shape !== value) {
				this._shape = value;
				this.updateShaderSource();
				this.dispatchEvent({type: 'point_shape_changed', target: this});
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get treeType () {
			return this._treeType;
		}

		set treeType (value) {
			if (this._treeType !== value) {
				this._treeType = value;
				this.updateShaderSource();
			}
		}

		get bbSize () {
			return this.uniforms.bbSize.value;
		}

		set bbSize (value) {
			this.uniforms.bbSize.value = value;
		}

		get size () {
			return this.uniforms.size.value;
		}

		set size (value) {
			if (this.uniforms.size.value !== value) {
				this.uniforms.size.value = value;

				this.dispatchEvent({
					type: 'point_size_changed',
					target: this
				});
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get minSize(){
			return this.uniforms.minSize.value;
		}

		set minSize(value){
			if (this.uniforms.minSize.value !== value) {
				this.uniforms.minSize.value = value;

				this.dispatchEvent({
					type: 'point_size_changed',
					target: this
				});
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get elevationRange () {
			return this.uniforms.elevationRange.value;
		}

		set elevationRange (value) {
			let changed = this.uniforms.elevationRange.value[0] !== value[0]
				|| this.uniforms.elevationRange.value[1] !== value[1];

			if(changed){
				this.uniforms.elevationRange.value = value;

				this._defaultElevationRangeChanged = true;

				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get heightMin () {
			return this.uniforms.elevationRange.value[0];
		}

		set heightMin (value) {
			this.elevationRange = [value, this.elevationRange[1]];
		}

		get heightMax () {
			return this.uniforms.elevationRange.value[1];
		}

		set heightMax (value) {
			this.elevationRange = [this.elevationRange[0], value];
		}

		get transition () {
			return this.uniforms.transition.value;
		}

		set transition (value) {
			this.uniforms.transition.value = value;
		}

		get intensityRange () {
			return this.uniforms.intensityRange.value;
		}

		set intensityRange (value) {
			if (!(value instanceof Array && value.length === 2)) {
				return;
			}

			if (value[0] === this.uniforms.intensityRange.value[0] &&
				value[1] === this.uniforms.intensityRange.value[1]) {
				return;
			}

			this.uniforms.intensityRange.value = value;

			this._defaultIntensityRangeChanged = true;

			this.dispatchEvent({
				type: 'material_property_changed',
				target: this
			});
		}

		get intensityGamma () {
			return this.uniforms.intensity_gbc.value[0];
		}

		set intensityGamma (value) {
			if (this.uniforms.intensity_gbc.value[0] !== value) {
				this.uniforms.intensity_gbc.value[0] = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get intensityContrast () {
			return this.uniforms.intensity_gbc.value[2];
		}

		set intensityContrast (value) {
			if (this.uniforms.intensity_gbc.value[2] !== value) {
				this.uniforms.intensity_gbc.value[2] = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get intensityBrightness () {
			return this.uniforms.intensity_gbc.value[1];
		}

		set intensityBrightness (value) {
			if (this.uniforms.intensity_gbc.value[1] !== value) {
				this.uniforms.intensity_gbc.value[1] = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get rgbGamma () {
			return this.uniforms.uRGB_gbc.value[0];
		}

		set rgbGamma (value) {
			if (this.uniforms.uRGB_gbc.value[0] !== value) {
				this.uniforms.uRGB_gbc.value[0] = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get rgbContrast () {
			return this.uniforms.uRGB_gbc.value[2];
		}

		set rgbContrast (value) {
			if (this.uniforms.uRGB_gbc.value[2] !== value) {
				this.uniforms.uRGB_gbc.value[2] = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get rgbBrightness () {
			return this.uniforms.uRGB_gbc.value[1];
		}

		set rgbBrightness (value) {
			if (this.uniforms.uRGB_gbc.value[1] !== value) {
				this.uniforms.uRGB_gbc.value[1] = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		
		get extraGamma () {
			return this.uniforms.uExtraGammaBrightContr.value[0];
		}

		set extraGamma (value) {
			if (this.uniforms.uExtraGammaBrightContr.value[0] !== value) {
				this.uniforms.uExtraGammaBrightContr.value[0] = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get extraBrightness () {
			return this.uniforms.uExtraGammaBrightContr.value[1];
		}

		set extraBrightness (value) {
			if (this.uniforms.uExtraGammaBrightContr.value[1] !== value) {
				this.uniforms.uExtraGammaBrightContr.value[1] = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get extraContrast () {
			return this.uniforms.uExtraGammaBrightContr.value[2];
		}

		set extraContrast (value) {
			if (this.uniforms.uExtraGammaBrightContr.value[2] !== value) {
				this.uniforms.uExtraGammaBrightContr.value[2] = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		getRange(attributeName){
			return this.ranges.get(attributeName);
		}

		setRange(attributeName, newRange){

			let rangeChanged = false;

			let oldRange = this.ranges.get(attributeName);

			if(oldRange != null && newRange != null){
				rangeChanged = oldRange[0] !== newRange[0] || oldRange[1] !== newRange[1];
			}else {
				rangeChanged = true;
			}

			this.ranges.set(attributeName, newRange);

			if(rangeChanged){
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get extraRange () {
			return this.uniforms.uExtraRange.value;
		}

		set extraRange (value) {
			if (!(value instanceof Array && value.length === 2)) {
				return;
			}

			if (value[0] === this.uniforms.uExtraRange.value[0] &&
				value[1] === this.uniforms.uExtraRange.value[1]) {
				return;
			}

			this.uniforms.uExtraRange.value = value;

			this._defaultExtraRangeChanged = true;

			this.dispatchEvent({
				type: 'material_property_changed',
				target: this
			});
		}

		get weightRGB () {
			return this.uniforms.wRGB.value;
		}

		set weightRGB (value) {
			if(this.uniforms.wRGB.value !== value){
				this.uniforms.wRGB.value = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get weightIntensity () {
			return this.uniforms.wIntensity.value;
		}

		set weightIntensity (value) {
			if(this.uniforms.wIntensity.value !== value){
				this.uniforms.wIntensity.value = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get weightElevation () {
			return this.uniforms.wElevation.value;
		}

		set weightElevation (value) {
			if(this.uniforms.wElevation.value !== value){
				this.uniforms.wElevation.value = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get weightClassification () {
			return this.uniforms.wClassification.value;
		}

		set weightClassification (value) {
			if(this.uniforms.wClassification.value !== value){
				this.uniforms.wClassification.value = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get weightReturnNumber () {
			return this.uniforms.wReturnNumber.value;
		}

		set weightReturnNumber (value) {
			if(this.uniforms.wReturnNumber.value !== value){
				this.uniforms.wReturnNumber.value = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		get weightSourceID () {
			return this.uniforms.wSourceID.value;
		}

		set weightSourceID (value) {
			if(this.uniforms.wSourceID.value !== value){
				this.uniforms.wSourceID.value = value;
				this.dispatchEvent({
					type: 'material_property_changed',
					target: this
				});
			}
		}

		static generateGradientTexture (gradient) {
			let size = 64;

			// create canvas
			let canvas = document.createElement('canvas');
			canvas.width = size;
			canvas.height = size;

			// get context
			let context = canvas.getContext('2d');

			// draw gradient
			context.rect(0, 0, size, size);
			let ctxGradient = context.createLinearGradient(0, 0, size, size);

			for (let i = 0; i < gradient.length; i++) {
				let step = gradient[i];

				ctxGradient.addColorStop(step[0], '#' + step[1].getHexString());
			}

			context.fillStyle = ctxGradient;
			context.fill();
			
			//let texture = new THREE.Texture(canvas);
			let texture = new CanvasTexture(canvas);
			texture.needsUpdate = true;
			
			texture.minFilter = LinearFilter;
			texture.wrap = RepeatWrapping;
			texture.repeat = 2;
			// textureImage = texture.image;

			return texture;
		}
		
		static generateMatcapTexture (matcap) {
		var url = new URL(Potree.resourcePath + "/textures/matcap/" + matcap).href;
		let texture = new TextureLoader().load( url );
			texture.magFilter = texture.minFilter = LinearFilter; 
			texture.needsUpdate = true;
			// PotreeConverter_1.6_2018_07_29_windows_x64\PotreeConverter.exe autzen_xyzrgbXYZ_ascii.xyz -f xyzrgbXYZ -a RGB NORMAL -o autzen_xyzrgbXYZ_ascii_a -p index --overwrite
			// Switch matcap texture on the fly : viewer.scene.pointclouds[0].material.matcap = 'matcap1.jpg'; 
			// For non power of 2, use LinearFilter and dont generate mipmaps, For power of 2, use NearestFilter and generate mipmaps : matcap2.jpg 1 2 8 11 12 13
			return texture; 
		}

		static generateMatcapTexture (matcap) {
		var url = new URL(Potree.resourcePath + "/textures/matcap/" + matcap).href;
		let texture = new TextureLoader().load( url );
			texture.magFilter = texture.minFilter = LinearFilter; 
			texture.needsUpdate = true;
			// PotreeConverter_1.6_2018_07_29_windows_x64\PotreeConverter.exe autzen_xyzrgbXYZ_ascii.xyz -f xyzrgbXYZ -a RGB NORMAL -o autzen_xyzrgbXYZ_ascii_a -p index --overwrite
			// Switch matcap texture on the fly : viewer.scene.pointclouds[0].material.matcap = 'matcap1.jpg'; 
			// For non power of 2, use LinearFilter and dont generate mipmaps, For power of 2, use NearestFilter and generate mipmaps : matcap2.jpg 1 2 8 11 12 13
			return texture; 
		}

		disableEvents(){
			if(this._hiddenListeners === undefined){
				this._hiddenListeners = this._listeners;
				this._listeners = {};
			}
		};

		enableEvents(){
			this._listeners = this._hiddenListeners;
			this._hiddenListeners = undefined;
		};

		// copyFrom(from){

		// 	var a = 10;

		// 	for(let name of Object.keys(this.uniforms)){
		// 		this.uniforms[name].value = from.uniforms[name].value;
		// 	}
		// }

		// copy(from){
		// 	this.copyFrom(from);
		// }

	}

	class PointCloudOctreeNode extends PointCloudTreeNode {
		constructor () {
			super();

			//this.children = {};
			this.children = [];
			this.sceneNode = null;
			this.octree = null;
		}

		getNumPoints () {
			return this.geometryNode.numPoints;
		}

		isLoaded () {
			return true;
		}

		isTreeNode () {
			return true;
		}

		isGeometryNode () {
			return false;
		}

		getLevel () {
			return this.geometryNode.level;
		}

		getBoundingSphere () {
			return this.geometryNode.boundingSphere;
		}

		getBoundingBox () {
			return this.geometryNode.boundingBox;
		}

		getChildren () {
			let children = [];

			for (let i = 0; i < 8; i++) {
				if (this.children[i]) {
					children.push(this.children[i]);
				}
			}

			return children;
		}

		getPointsInBox(boxNode){

			if(!this.sceneNode){
				return null;
			}

			let buffer = this.geometryNode.buffer;

			let posOffset = buffer.offset("position");
			let stride = buffer.stride;
			let view = new DataView(buffer.data);

			let worldToBox = boxNode.matrixWorld.clone().invert();
			let objectToBox = new Matrix4().multiplyMatrices(worldToBox, this.sceneNode.matrixWorld);

			let inBox = [];

			let pos = new Vector4();
			for(let i = 0; i < buffer.numElements; i++){
				let x = view.getFloat32(i * stride + posOffset + 0, true);
				let y = view.getFloat32(i * stride + posOffset + 4, true);
				let z = view.getFloat32(i * stride + posOffset + 8, true);

				pos.set(x, y, z, 1);
				pos.applyMatrix4(objectToBox);

				if(-0.5 < pos.x && pos.x < 0.5){
					if(-0.5 < pos.y && pos.y < 0.5){
						if(-0.5 < pos.z && pos.z < 0.5){
							pos.set(x, y, z, 1).applyMatrix4(this.sceneNode.matrixWorld);
							inBox.push(new Vector3(pos.x, pos.y, pos.z));
						}
					}
				}
			}

			return inBox;
		}

		get name () {
			return this.geometryNode.name;
		}
	};

	class PointCloudOctree extends PointCloudTree {
		constructor (geometry, material) {
			super();

			this.pointBudget = Infinity;
			this.pcoGeometry = geometry;
			this.boundingBox = this.pcoGeometry.boundingBox;
			this.boundingSphere = this.boundingBox.getBoundingSphere(new Sphere());
			this.material = material || new PointCloudMaterial$1();
			this.visiblePointsTarget = 2 * 1000 * 1000;
			this.minimumNodePixelSize = 150;
			this.level = 0;
			this.position.copy(geometry.offset);
			this.updateMatrix();

			{

				let priorityQueue = ["rgba", "rgb", "intensity", "classification"];
				let selected = "rgba";

				for(let attributeName of priorityQueue){
					let attribute = this.pcoGeometry.pointAttributes.attributes.find(a => a.name === attributeName);

					if(!attribute){
						continue;
					}

					let min = attribute.range[0].constructor.name === "Array" ? attribute.range[0] : [attribute.range[0]];
					let max = attribute.range[1].constructor.name === "Array" ? attribute.range[1] : [attribute.range[1]];

					let range_min = new Vector3(...min);
					let range_max = new Vector3(...max);
					let range = range_min.distanceTo(range_max);

					if(range === 0){
						continue;
					}

					selected = attributeName;
					break;
				}

				this.material.activeAttributeName = selected;
			}

			this.showBoundingBox = false;
			this.boundingBoxNodes = [];
			this.loadQueue = [];
			this.visibleBounds = new Box3();
			this.visibleNodes = [];
			this.visibleGeometry = [];
			this.generateDEM = false;
			this.profileRequests = [];
			this.name = '';
			this._visible = true;

			{
				let box = [this.pcoGeometry.tightBoundingBox, this.getBoundingBoxWorld()]
					.find(v => v !== undefined);

				this.updateMatrixWorld(true);
				box = Utils.computeTransformedBoundingBox(box, this.matrixWorld);

				let bMin = box.min.z;
				let bMax = box.max.z;
				this.material.heightMin = bMin;
				this.material.heightMax = bMax;
			}

			// TODO read projection from file instead
			this.projection = geometry.projection;
			this.fallbackProjection = geometry.fallbackProjection;

			this.root = this.pcoGeometry.root;
		}

		setName (name) {
			if (this.name !== name) {
				this.name = name;
				this.dispatchEvent({type: 'name_changed', name: name, pointcloud: this});
			}
		}

		getName () {
			return this.name;
		}

		getAttribute(name){

			const attribute = this.pcoGeometry.pointAttributes.attributes.find(a => a.name === name);

			if(attribute){
				return attribute;
			}else {
				return null;
			}
		}

		getAttributes(){
			return this.pcoGeometry.pointAttributes;
		}

		toTreeNode (geometryNode, parent) {
			let node = new PointCloudOctreeNode();

			// if(geometryNode.name === "r40206"){
			//	console.log("creating node for r40206");
			// }
			let sceneNode = new Points(geometryNode.geometry, this.material);
			sceneNode.name = geometryNode.name;
			sceneNode.position.copy(geometryNode.boundingBox.min);
			sceneNode.frustumCulled = false;
			sceneNode.onBeforeRender = (_this, scene, camera, geometry, material, group) => {
				if (material.program) {
					_this.getContext().useProgram(material.program.program);

					if (material.program.getUniforms().map.level) {
						let level = geometryNode.getLevel();
						material.uniforms.level.value = level;
						material.program.getUniforms().map.level.setValue(_this.getContext(), level);
					}

					if (this.visibleNodeTextureOffsets && material.program.getUniforms().map.vnStart) {
						let vnStart = this.visibleNodeTextureOffsets.get(node);
						material.uniforms.vnStart.value = vnStart;
						material.program.getUniforms().map.vnStart.setValue(_this.getContext(), vnStart);
					}

					if (material.program.getUniforms().map.pcIndex) {
						let i = node.pcIndex ? node.pcIndex : this.visibleNodes.indexOf(node);
						material.uniforms.pcIndex.value = i;
						material.program.getUniforms().map.pcIndex.setValue(_this.getContext(), i);
					}
				}
			};

			// { // DEBUG
			//	let sg = new THREE.SphereGeometry(1, 16, 16);
			//	let sm = new THREE.MeshNormalMaterial();
			//	let s = new THREE.Mesh(sg, sm);
			//	s.scale.set(5, 5, 5);
			//	s.position.copy(geometryNode.mean)
			//		.add(this.position)
			//		.add(geometryNode.boundingBox.min);
			//
			//	viewer.scene.scene.add(s);
			// }

			node.geometryNode = geometryNode;
			node.sceneNode = sceneNode;
			node.pointcloud = this;
			node.children = [];
			//for (let key in geometryNode.children) {
			//	node.children[key] = geometryNode.children[key];
			//}
			for(let i = 0; i < 8; i++){
				node.children[i] = geometryNode.children[i];
			}

			if (!parent) {
				this.root = node;
				this.add(sceneNode);
			} else {
				let childIndex = parseInt(geometryNode.name[geometryNode.name.length - 1]);
				parent.sceneNode.add(sceneNode);
				parent.children[childIndex] = node;
			}

			let disposeListener = function () {
				let childIndex = parseInt(geometryNode.name[geometryNode.name.length - 1]);
				parent.sceneNode.remove(node.sceneNode);
				parent.children[childIndex] = geometryNode;
			};
			geometryNode.oneTimeDisposeHandlers.push(disposeListener);

			return node;
		}

		updateVisibleBounds () {
			let leafNodes = [];
			for (let i = 0; i < this.visibleNodes.length; i++) {
				let node = this.visibleNodes[i];
				let isLeaf = true;

				for (let j = 0; j < node.children.length; j++) {
					let child = node.children[j];
					if (child instanceof PointCloudOctreeNode) {
						isLeaf = isLeaf && !child.sceneNode.visible;
					} else if (child instanceof PointCloudOctreeGeometryNode) {
						isLeaf = true;
					}
				}

				if (isLeaf) {
					leafNodes.push(node);
				}
			}

			this.visibleBounds.min = new Vector3(Infinity, Infinity, Infinity);
			this.visibleBounds.max = new Vector3(-Infinity, -Infinity, -Infinity);
			for (let i = 0; i < leafNodes.length; i++) {
				let node = leafNodes[i];

				this.visibleBounds.expandByPoint(node.getBoundingBox().min);
				this.visibleBounds.expandByPoint(node.getBoundingBox().max);
			}
		}

		updateMaterial (material, visibleNodes, camera, renderer) {
			material.fov = camera.fov * (Math.PI / 180);
			material.screenWidth = renderer.domElement.clientWidth;
			material.screenHeight = renderer.domElement.clientHeight;
			material.spacing = this.pcoGeometry.spacing; // * Math.max(this.scale.x, this.scale.y, this.scale.z);
			material.near = camera.near;
			material.far = camera.far;
			material.uniforms.octreeSize.value = this.pcoGeometry.boundingBox.getSize(new Vector3()).x;
		}

		computeVisibilityTextureData(nodes, camera){

			if(Potree.measureTimings) performance.mark("computeVisibilityTextureData-start");

			let data = new Uint8Array(nodes.length * 4);
			let visibleNodeTextureOffsets = new Map();

			// copy array
			nodes = nodes.slice();

			// sort by level and index, e.g. r, r0, r3, r4, r01, r07, r30, ...
			let sort = function (a, b) {
				let na = a.geometryNode.name;
				let nb = b.geometryNode.name;
				if (na.length !== nb.length) return na.length - nb.length;
				if (na < nb) return -1;
				if (na > nb) return 1;
				return 0;
			};
			nodes.sort(sort);

			let worldDir = new Vector3();

			let nodeMap = new Map();
			let offsetsToChild = new Array(nodes.length).fill(Infinity);

			for(let i = 0; i < nodes.length; i++){
				let node = nodes[i];

				nodeMap.set(node.name, node);
				visibleNodeTextureOffsets.set(node, i);

				if(i > 0){
					let index = parseInt(node.name.slice(-1));
					let parentName = node.name.slice(0, -1);
					let parent = nodeMap.get(parentName);
					let parentOffset = visibleNodeTextureOffsets.get(parent);

					let parentOffsetToChild = (i - parentOffset);

					offsetsToChild[parentOffset] = Math.min(offsetsToChild[parentOffset], parentOffsetToChild);

					data[parentOffset * 4 + 0] = data[parentOffset * 4 + 0] | (1 << index);
					data[parentOffset * 4 + 1] = (offsetsToChild[parentOffset] >> 8);
					data[parentOffset * 4 + 2] = (offsetsToChild[parentOffset] % 256);
				}

				let density = node.geometryNode.density;
				
				if(typeof density === "number"){
					let lodOffset = Math.log2(density) / 2 - 1.5;

					let offsetUint8 = (lodOffset + 10) * 10;

					data[i * 4 + 3] = offsetUint8;
				}else {
					data[i * 4 + 3] = 100;
				}

			}

			if(Potree.measureTimings){
				performance.mark("computeVisibilityTextureData-end");
				performance.measure("render.computeVisibilityTextureData", "computeVisibilityTextureData-start", "computeVisibilityTextureData-end");
			}

			return {
				data: data,
				offsets: visibleNodeTextureOffsets
			};
		}

		nodeIntersectsProfile (node, profile) {
			let bbWorld = node.boundingBox.clone().applyMatrix4(this.matrixWorld);
			let bsWorld = bbWorld.getBoundingSphere(new Sphere());

			let intersects = false;

			for (let i = 0; i < profile.points.length - 1; i++) {

				let start = new Vector3(profile.points[i + 0].x, profile.points[i + 0].y, bsWorld.center.z);
				let end = new Vector3(profile.points[i + 1].x, profile.points[i + 1].y, bsWorld.center.z);

				let closest = new Line3(start, end).closestPointToPoint(bsWorld.center, true, new Vector3());
				let distance = closest.distanceTo(bsWorld.center);

				intersects = intersects || (distance < (bsWorld.radius + profile.width));
			}

			//console.log(`${node.name}: ${intersects}`);

			return intersects;
		}

		deepestNodeAt(position){
			
			const toObjectSpace = this.matrixWorld.clone().invert();

			const objPos = position.clone().applyMatrix4(toObjectSpace);

			let current = this.root;
			while(true){

				let containingChild = null;

				for(const child of current.children){

					if(child !== undefined){
						if(child.getBoundingBox().containsPoint(objPos)){
							containingChild = child;
						}
					}
				}

				if(containingChild !== null && containingChild instanceof PointCloudOctreeNode){
					current = containingChild;
				}else {
					break;
				}
			}

			const deepest = current;

			return deepest;
		}

		nodesOnRay (nodes, ray) {
			let nodesOnRay = [];

			let _ray = ray.clone();
			for (let i = 0; i < nodes.length; i++) {
				let node = nodes[i];
				let sphere = node.getBoundingSphere().clone().applyMatrix4(this.matrixWorld);

				if (_ray.intersectsSphere(sphere)) {
					nodesOnRay.push(node);
				}
			}

			return nodesOnRay;
		}

		updateMatrixWorld (force) {
			if (this.matrixAutoUpdate === true) this.updateMatrix();

			if (this.matrixWorldNeedsUpdate === true || force === true) {
				if (!this.parent) {
					this.matrixWorld.copy(this.matrix);
				} else {
					this.matrixWorld.multiplyMatrices(this.parent.matrixWorld, this.matrix);
				}

				this.matrixWorldNeedsUpdate = false;

				force = true;
			}
		}

		hideDescendants (object) {
			let stack = [];
			for (let i = 0; i < object.children.length; i++) {
				let child = object.children[i];
				if (child.visible) {
					stack.push(child);
				}
			}

			while (stack.length > 0) {
				let object = stack.shift();

				object.visible = false;

				for (let i = 0; i < object.children.length; i++) {
					let child = object.children[i];
					if (child.visible) {
						stack.push(child);
					}
				}
			}
		}

		moveToOrigin () {
			this.position.set(0, 0, 0);
			this.updateMatrixWorld(true);
			let box = this.boundingBox;
			let transform = this.matrixWorld;
			let tBox = Utils.computeTransformedBoundingBox(box, transform);
			this.position.set(0, 0, 0).sub(tBox.getCenter(new Vector3()));
		};

		moveToGroundPlane () {
			this.updateMatrixWorld(true);
			let box = this.boundingBox;
			let transform = this.matrixWorld;
			let tBox = Utils.computeTransformedBoundingBox(box, transform);
			this.position.y += -tBox.min.y;
		};

		getBoundingBoxWorld () {
			this.updateMatrixWorld(true);
			let box = this.boundingBox;
			let transform = this.matrixWorld;
			let tBox = Utils.computeTransformedBoundingBox(box, transform);

			return tBox;
		};

		/**
		 * returns points inside the profile points
		 *
		 * maxDepth:		search points up to the given octree depth
		 *
		 *
		 * The return value is an array with all segments of the profile path
		 *	let segment = {
		 *		start:	THREE.Vector3,
		 *		end:	THREE.Vector3,
		 *		points: {}
		 *		project: function()
		 *	};
		 *
		 * The project() function inside each segment can be used to transform
		 * that segments point coordinates to line up along the x-axis.
		 *
		 *
		 */
		getPointsInProfile (profile, maxDepth, callback) {
			if (callback) {
				let request = new Potree.ProfileRequest(this, profile, maxDepth, callback);
				this.profileRequests.push(request);

				return request;
			}

			let points = {
				segments: [],
				boundingBox: new Box3(),
				projectedBoundingBox: new Box2()
			};

			// evaluate segments
			for (let i = 0; i < profile.points.length - 1; i++) {
				let start = profile.points[i];
				let end = profile.points[i + 1];
				let ps = this.getProfile(start, end, profile.width, maxDepth);

				let segment = {
					start: start,
					end: end,
					points: ps,
					project: null
				};

				points.segments.push(segment);

				points.boundingBox.expandByPoint(ps.boundingBox.min);
				points.boundingBox.expandByPoint(ps.boundingBox.max);
			}

			// add projection functions to the segments
			let mileage = new Vector3();
			for (let i = 0; i < points.segments.length; i++) {
				let segment = points.segments[i];
				let start = segment.start;
				let end = segment.end;

				let project = (function (_start, _end, _mileage, _boundingBox) {
					let start = _start;
					let end = _end;
					let mileage = _mileage;
					let boundingBox = _boundingBox;

					let xAxis = new Vector3(1, 0, 0);
					let dir = new Vector3().subVectors(end, start);
					dir.y = 0;
					dir.normalize();
					let alpha = Math.acos(xAxis.dot(dir));
					if (dir.z > 0) {
						alpha = -alpha;
					}

					return function (position) {
						let toOrigin = new Matrix4().makeTranslation(-start.x, -boundingBox.min.y, -start.z);
						let alignWithX = new Matrix4().makeRotationY(-alpha);
						let applyMileage = new Matrix4().makeTranslation(mileage.x, 0, 0);

						let pos = position.clone();
						pos.applyMatrix4(toOrigin);
						pos.applyMatrix4(alignWithX);
						pos.applyMatrix4(applyMileage);

						return pos;
					};
				}(start, end, mileage.clone(), points.boundingBox.clone()));

				segment.project = project;

				mileage.x += new Vector3(start.x, 0, start.z).distanceTo(new Vector3(end.x, 0, end.z));
				mileage.y += end.y - start.y;
			}

			points.projectedBoundingBox.min.x = 0;
			points.projectedBoundingBox.min.y = points.boundingBox.min.y;
			points.projectedBoundingBox.max.x = mileage.x;
			points.projectedBoundingBox.max.y = points.boundingBox.max.y;

			return points;
		}

		/**
		 * returns points inside the given profile bounds.
		 *
		 * start:
		 * end:
		 * width:
		 * depth:		search points up to the given octree depth
		 * callback:	if specified, points are loaded before searching
		 *
		 *
		 */
		getProfile (start, end, width, depth, callback) {
			let request = new Potree.ProfileRequest(start, end, width, depth, callback);
			this.profileRequests.push(request);
		};

		getVisibleExtent () {
			return this.visibleBounds.applyMatrix4(this.matrixWorld);
		};

		intersectsPoint(position){

			let rootAvailable = this.pcoGeometry.root && this.pcoGeometry.root.geometry;

			if(!rootAvailable){
				return false;
			}

			if(typeof this.signedDistanceField === "undefined"){

				const resolution = 32;
				const field = new Float32Array(resolution ** 3).fill(Infinity);

				const positions = this.pcoGeometry.root.geometry.attributes.position;
				const boundingBox = this.boundingBox;

				const n = positions.count;

				for(let i = 0; i < n; i = i + 3){
					const x = positions.array[3 * i + 0];
					const y = positions.array[3 * i + 1];
					const z = positions.array[3 * i + 2];

					const ix = parseInt(Math.min(resolution * (x / boundingBox.max.x), resolution - 1));
					const iy = parseInt(Math.min(resolution * (y / boundingBox.max.y), resolution - 1));
					const iz = parseInt(Math.min(resolution * (z / boundingBox.max.z), resolution - 1));

					const index = ix + iy * resolution + iz * resolution * resolution;

					field[index] = 0;
				}

				const sdf = {
					resolution: resolution,
					field: field,
				};

				this.signedDistanceField = sdf;
			}


			{
				const sdf = this.signedDistanceField;
				const boundingBox = this.boundingBox;

				const toObjectSpace = this.matrixWorld.clone().invert();

				const objPos = position.clone().applyMatrix4(toObjectSpace);

				const resolution = sdf.resolution;
				const ix = parseInt(resolution * (objPos.x / boundingBox.max.x));
				const iy = parseInt(resolution * (objPos.y / boundingBox.max.y));
				const iz = parseInt(resolution * (objPos.z / boundingBox.max.z));

				if(ix < 0 || iy < 0 || iz < 0){
					return false;
				}
				if(ix >= resolution || iy >= resolution || iz >= resolution){
					return false;
				}

				const index = ix + iy * resolution + iz * resolution * resolution;

				const value = sdf.field[index];

				if(value === 0){
					return true;
				}

			}

			return false;

		}

		/**
		 *
		 *
		 *
		 * params.pickWindowSize:	Look for points inside a pixel window of this size.
		 *							Use odd values: 1, 3, 5, ...
		 *
		 *
		 * TODO: only draw pixels that are actually read with readPixels().
		 *
		 */
		pick(viewer, camera, ray, params = {}){

			let renderer = viewer.renderer;
			let pRenderer = viewer.pRenderer;

			performance.mark("pick-start");

			let getVal = (a, b) => a !== undefined ? a : b;

			let pickWindowSize = getVal(params.pickWindowSize, 65);
			let pickOutsideClipRegion = getVal(params.pickOutsideClipRegion, false);

			let size = renderer.getSize(new Vector2());

			let width = Math.ceil(getVal(params.width, size.width));
			let height = Math.ceil(getVal(params.height, size.height));

			let pointSizeType = getVal(params.pointSizeType, this.material.pointSizeType);
			let pointSize = getVal(params.pointSize, this.material.size);

			let nodes = this.nodesOnRay(this.visibleNodes, ray);

			if (nodes.length === 0) {
				return null;
			}

			if (!this.pickState) {
				let scene = new Scene();

				let material = new Potree.PointCloudMaterial();
				material.activeAttributeName = "indices";

				let renderTarget = new WebGLRenderTarget(
					1, 1,
					{ minFilter: LinearFilter,
						magFilter: NearestFilter,
						format: RGBAFormat }
				);

				this.pickState = {
					renderTarget: renderTarget,
					material: material,
					scene: scene
				};
			};

			let pickState = this.pickState;
			let pickMaterial = pickState.material;

			{ // update pick material
				pickMaterial.pointSizeType = pointSizeType;
				//pickMaterial.shape = this.material.shape;
				pickMaterial.shape = Potree.PointShape.PARABOLOID;

				pickMaterial.uniforms.uFilterReturnNumberRange.value = this.material.uniforms.uFilterReturnNumberRange.value;
				pickMaterial.uniforms.uFilterNumberOfReturnsRange.value = this.material.uniforms.uFilterNumberOfReturnsRange.value;
				pickMaterial.uniforms.uFilterGPSTimeClipRange.value = this.material.uniforms.uFilterGPSTimeClipRange.value;
				pickMaterial.uniforms.uFilterPointSourceIDClipRange.value = this.material.uniforms.uFilterPointSourceIDClipRange.value;

				pickMaterial.activeAttributeName = "indices";

				pickMaterial.size = pointSize;
				pickMaterial.uniforms.minSize.value = this.material.uniforms.minSize.value;
				pickMaterial.uniforms.maxSize.value = this.material.uniforms.maxSize.value;
				pickMaterial.classification = this.material.classification;
				pickMaterial.recomputeClassification();

				if(params.pickClipped){
					pickMaterial.clipBoxes = this.material.clipBoxes;
					pickMaterial.uniforms.clipBoxes = this.material.uniforms.clipBoxes;
					if(this.material.clipTask === Potree.ClipTask.HIGHLIGHT){
						pickMaterial.clipTask = Potree.ClipTask.NONE;
					}else {
						pickMaterial.clipTask = this.material.clipTask;
					}
					pickMaterial.clipMethod = this.material.clipMethod;
				}else {
					pickMaterial.clipBoxes = [];
				}

				this.updateMaterial(pickMaterial, nodes, camera, renderer);
			}

			pickState.renderTarget.setSize(width, height);

			let pixelPos = new Vector2(params.x, params.y);

			let gl = renderer.getContext();
			gl.enable(gl.SCISSOR_TEST);
			gl.scissor(
				parseInt(pixelPos.x - (pickWindowSize - 1) / 2),
				parseInt(pixelPos.y - (pickWindowSize - 1) / 2),
				parseInt(pickWindowSize), parseInt(pickWindowSize));


			renderer.state.buffers.depth.setTest(pickMaterial.depthTest);
			renderer.state.buffers.depth.setMask(pickMaterial.depthWrite);
			renderer.state.setBlending(NoBlending);

			{ // RENDER
				renderer.setRenderTarget(pickState.renderTarget);
				gl.clearColor(0, 0, 0, 0);
				renderer.clear(true, true, true);

				let tmp = this.material;
				this.material = pickMaterial;

				pRenderer.renderOctree(this, nodes, camera, pickState.renderTarget);

				this.material = tmp;
			}

			let clamp = (number, min, max) => Math.min(Math.max(min, number), max);

			let x = parseInt(clamp(pixelPos.x - (pickWindowSize - 1) / 2, 0, width));
			let y = parseInt(clamp(pixelPos.y - (pickWindowSize - 1) / 2, 0, height));
			let w = parseInt(Math.min(x + pickWindowSize, width) - x);
			let h = parseInt(Math.min(y + pickWindowSize, height) - y);

			let pixelCount = w * h;
			let buffer = new Uint8Array(4 * pixelCount);

			gl.readPixels(x, y, pickWindowSize, pickWindowSize, gl.RGBA, gl.UNSIGNED_BYTE, buffer);

			renderer.setRenderTarget(null);
			renderer.state.reset();
			renderer.setScissorTest(false);
			gl.disable(gl.SCISSOR_TEST);

			let pixels = buffer;
			let ibuffer = new Uint32Array(buffer.buffer);

			// find closest hit inside pixelWindow boundaries
			let min = Number.MAX_VALUE;
			let hits = [];
			for (let u = 0; u < pickWindowSize; u++) {
				for (let v = 0; v < pickWindowSize; v++) {
					let offset = (u + v * pickWindowSize);
					let distance = Math.pow(u - (pickWindowSize - 1) / 2, 2) + Math.pow(v - (pickWindowSize - 1) / 2, 2);

					let pcIndex = pixels[4 * offset + 3];
					pixels[4 * offset + 3] = 0;
					let pIndex = ibuffer[offset];

					if(!(pcIndex === 0 && pIndex === 0) && (pcIndex !== undefined) && (pIndex !== undefined)){
						let hit = {
							pIndex: pIndex,
							pcIndex: pcIndex,
							distanceToCenter: distance
						};

						if(params.all){
							hits.push(hit);
						}else {
							if(hits.length > 0){
								if(distance < hits[0].distanceToCenter){
									hits[0] = hit;
								}
							}else {
								hits.push(hit);
							}
						}


					}
				}
			}

			
			// { // DEBUG: show panel with pick image
			// 	let img = Utils.pixelsArrayToImage(buffer, w, h);
			// 	let screenshot = img.src;
			
			// 	if(!this.debugDIV){
			// 		this.debugDIV = $(`
			// 			<div id="pickDebug"
			// 			style="position: absolute;
			// 			right: 400px; width: 300px;
			// 			bottom: 44px; width: 300px;
			// 			z-index: 1000;
			// 			"></div>`);
			// 		$(document.body).append(this.debugDIV);
			// 	}
			
			// 	this.debugDIV.empty();
			// 	this.debugDIV.append($(`<img src="${screenshot}"
			// 		style="transform: scaleY(-1); width: 300px"/>`));
			// 	//$(this.debugWindow.document).append($(`<img src="${screenshot}"/>`));
			// 	//this.debugWindow.document.write('<img src="'+screenshot+'"/>');
			// }


			for(let hit of hits){
				let point = {};

				if (!nodes[hit.pcIndex]) {
					return null;
				}

				let node = nodes[hit.pcIndex];
				let pc = node.sceneNode;
				let geometry = node.geometryNode.geometry;

				for(let attributeName in geometry.attributes){
					let attribute = geometry.attributes[attributeName];

					if (attributeName === 'position') {
						let x = attribute.array[3 * hit.pIndex + 0];
						let y = attribute.array[3 * hit.pIndex + 1];
						let z = attribute.array[3 * hit.pIndex + 2];

						let position = new Vector3(x, y, z);
						position.applyMatrix4(pc.matrixWorld);

						point[attributeName] = position;
					} else if (attributeName === 'indices') {

					} else {

						let values = attribute.array.slice(attribute.itemSize * hit.pIndex, attribute.itemSize * (hit.pIndex + 1)) ;

						if(attribute.potree){
							const {scale, offset} = attribute.potree;
							values = values.map(v => v / scale + offset);
						}

						point[attributeName] = values;

						//debugger;
						//if (values.itemSize === 1) {
						//	point[attribute.name] = values.array[hit.pIndex];
						//} else {
						//	let value = [];
						//	for (let j = 0; j < values.itemSize; j++) {
						//		value.push(values.array[values.itemSize * hit.pIndex + j]);
						//	}
						//	point[attribute.name] = value;
						//}
					}

				}

				hit.point = point;
			}

			performance.mark("pick-end");
			performance.measure("pick", "pick-start", "pick-end");

			if(params.all){
				return hits.map(hit => hit.point);
			}else {
				if(hits.length === 0){
					return null;
				}else {
					return hits[0].point;
					//let sorted = hits.sort( (a, b) => a.distanceToCenter - b.distanceToCenter);

					//return sorted[0].point;
				}
			}

		};

		* getFittedBoxGen(boxNode){
			let start = performance.now();

			let shrinkedLocalBounds = new Box3();
			let worldToBox = boxNode.matrixWorld.clone().invert();

			for(let node of this.visibleNodes){
				if(!node.sceneNode){
					continue;
				}

				let buffer = node.geometryNode.buffer;

				let posOffset = buffer.offset("position");
				let stride = buffer.stride;
				let view = new DataView(buffer.data);

				let objectToBox = new Matrix4().multiplyMatrices(worldToBox, node.sceneNode.matrixWorld);

				let pos = new Vector4();
				for(let i = 0; i < buffer.numElements; i++){
					let x = view.getFloat32(i * stride + posOffset + 0, true);
					let y = view.getFloat32(i * stride + posOffset + 4, true);
					let z = view.getFloat32(i * stride + posOffset + 8, true);

					pos.set(x, y, z, 1);
					pos.applyMatrix4(objectToBox);

					if(-0.5 < pos.x && pos.x < 0.5){
						if(-0.5 < pos.y && pos.y < 0.5){
							if(-0.5 < pos.z && pos.z < 0.5){
								shrinkedLocalBounds.expandByPoint(pos);
							}
						}
					}
				}

				yield;
			}

			let fittedPosition = shrinkedLocalBounds.getCenter(new Vector3()).applyMatrix4(boxNode.matrixWorld);

			let fitted = new Object3D();
			fitted.position.copy(fittedPosition);
			fitted.scale.copy(boxNode.scale);
			fitted.rotation.copy(boxNode.rotation);

			let ds = new Vector3().subVectors(shrinkedLocalBounds.max, shrinkedLocalBounds.min);
			fitted.scale.multiply(ds);

			let duration = performance.now() - start;
			console.log("duration: ", duration);

			yield fitted;
		}

		getFittedBox(boxNode, maxLevel = Infinity){

			maxLevel = Infinity;

			let start = performance.now();

			let shrinkedLocalBounds = new Box3();
			let worldToBox = boxNode.matrixWorld.clone().invert();

			for(let node of this.visibleNodes){
				if(!node.sceneNode || node.getLevel() > maxLevel){
					continue;
				}

				let buffer = node.geometryNode.buffer;

				let posOffset = buffer.offset("position");
				let stride = buffer.stride;
				let view = new DataView(buffer.data);

				let objectToBox = new Matrix4().multiplyMatrices(worldToBox, node.sceneNode.matrixWorld);

				let pos = new Vector4();
				for(let i = 0; i < buffer.numElements; i++){
					let x = view.getFloat32(i * stride + posOffset + 0, true);
					let y = view.getFloat32(i * stride + posOffset + 4, true);
					let z = view.getFloat32(i * stride + posOffset + 8, true);

					pos.set(x, y, z, 1);
					pos.applyMatrix4(objectToBox);

					if(-0.5 < pos.x && pos.x < 0.5){
						if(-0.5 < pos.y && pos.y < 0.5){
							if(-0.5 < pos.z && pos.z < 0.5){
								shrinkedLocalBounds.expandByPoint(pos);
							}
						}
					}
				}
			}

			let fittedPosition = shrinkedLocalBounds.getCenter(new Vector3()).applyMatrix4(boxNode.matrixWorld);

			let fitted = new Object3D();
			fitted.position.copy(fittedPosition);
			fitted.scale.copy(boxNode.scale);
			fitted.rotation.copy(boxNode.rotation);

			let ds = new Vector3().subVectors(shrinkedLocalBounds.max, shrinkedLocalBounds.min);
			fitted.scale.multiply(ds);

			let duration = performance.now() - start;
			console.log("duration: ", duration);

			return fitted;
		}

		get progress () {
			return this.visibleNodes.length / this.visibleGeometry.length;
		}

		find(name){
			let node = null;
			for(let char of name){
				if(char === "r"){
					node = this.root;
				}else {
					node = node.children[char];
				}
			}

			return node;
		}

		get visible(){
			return this._visible;
		}

		set visible(value){

			if(value !== this._visible){
				this._visible = value;

				this.dispatchEvent({type: 'visibility_changed', pointcloud: this});
			}

		}

	}

	class Points$1 {
		
		constructor () {
			this.boundingBox = new Box3();
			this.numPoints = 0;
			this.data = {};
		}

		add (points) {
			let currentSize = this.numPoints;
			let additionalSize = points.numPoints;
			let newSize = currentSize + additionalSize;

			let thisAttributes = Object.keys(this.data);
			let otherAttributes = Object.keys(points.data);
			let attributes = new Set([...thisAttributes, ...otherAttributes]);

			for (let attribute of attributes) {
				if (thisAttributes.includes(attribute) && otherAttributes.includes(attribute)) {
					// attribute in both, merge
					let Type = this.data[attribute].constructor;
					let merged = new Type(this.data[attribute].length + points.data[attribute].length);
					merged.set(this.data[attribute], 0);
					merged.set(points.data[attribute], this.data[attribute].length);
					this.data[attribute] = merged;
				} else if (thisAttributes.includes(attribute) && !otherAttributes.includes(attribute)) {
					// attribute only in this; take over this and expand to new size
					let elementsPerPoint = this.data[attribute].length / this.numPoints;
					let Type = this.data[attribute].constructor;
					let expanded = new Type(elementsPerPoint * newSize);
					expanded.set(this.data[attribute], 0);
					this.data[attribute] = expanded;
				} else if (!thisAttributes.includes(attribute) && otherAttributes.includes(attribute)) {
					// attribute only in points to be added; take over new points and expand to new size
					let elementsPerPoint = points.data[attribute].length / points.numPoints;
					let Type = points.data[attribute].constructor;
					let expanded = new Type(elementsPerPoint * newSize);
					expanded.set(points.data[attribute], elementsPerPoint * currentSize);
					this.data[attribute] = expanded;
				}
			}

			this.numPoints = newSize;

			this.boundingBox.union(points.boundingBox);
		}
	}

	/**
	 *
	 * code adapted from three.js BoxHelper.js
	 * https://github.com/mrdoob/three.js/blob/dev/src/helpers/BoxHelper.js
	 *
	 * @author mrdoob / http://mrdoob.com/
	 * @author Mugen87 / http://github.com/Mugen87
	 * @author mschuetz / http://potree.org
	 */

	class Box3Helper$1 extends LineSegments {
		constructor (box, color) {
			if (color === undefined) color = 0xffff00;

			let indices = new Uint16Array([ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ]);
			let positions = new Float32Array([
				box.min.x, box.min.y, box.min.z,
				box.max.x, box.min.y, box.min.z,
				box.max.x, box.min.y, box.max.z,
				box.min.x, box.min.y, box.max.z,
				box.min.x, box.max.y, box.min.z,
				box.max.x, box.max.y, box.min.z,
				box.max.x, box.max.y, box.max.z,
				box.min.x, box.max.y, box.max.z
			]);

			let geometry = new BufferGeometry();
			geometry.setIndex(new BufferAttribute(indices, 1));
			geometry.setAttribute('position', new BufferAttribute(positions, 3));

			let material = new LineBasicMaterial({ color: color });

			super(geometry, material);
		}
	}

	function updatePointClouds(pointclouds, camera, renderer){

		for (let pointcloud of pointclouds) {
			let start = performance.now();

			for (let profileRequest of pointcloud.profileRequests) {
				profileRequest.update();

				let duration = performance.now() - start;
				if(duration > 5){
					break;
				}
			}

			let duration = performance.now() - start;
		}

		let result = updateVisibility(pointclouds, camera, renderer);

		for (let pointcloud of pointclouds) {
			pointcloud.updateMaterial(pointcloud.material, pointcloud.visibleNodes, camera, renderer);
			pointcloud.updateVisibleBounds();
		}

		exports.lru.freeMemory();

		return result;
	};



	function updateVisibilityStructures(pointclouds, camera, renderer) {
		let frustums = [];
		let camObjPositions = [];
		let priorityQueue = new BinaryHeap(function (x) { return 1 / x.weight; });

		for (let i = 0; i < pointclouds.length; i++) {
			let pointcloud = pointclouds[i];

			if (!pointcloud.initialized()) {
				continue;
			}

			pointcloud.numVisibleNodes = 0;
			pointcloud.numVisiblePoints = 0;
			pointcloud.deepestVisibleLevel = 0;
			pointcloud.visibleNodes = [];
			pointcloud.visibleGeometry = [];

			// frustum in object space
			camera.updateMatrixWorld();
			let frustum = new Frustum();
			let viewI = camera.matrixWorldInverse;
			let world = pointcloud.matrixWorld;
			
			// use close near plane for frustum intersection
			let frustumCam = camera.clone();
			frustumCam.near = Math.min(camera.near, 0.1);
			frustumCam.updateProjectionMatrix();
			let proj = camera.projectionMatrix;

			let fm = new Matrix4().multiply(proj).multiply(viewI).multiply(world);
			frustum.setFromProjectionMatrix(fm);
			frustums.push(frustum);

			// camera position in object space
			let view = camera.matrixWorld;
			let worldI = world.clone().invert();
			let camMatrixObject = new Matrix4().multiply(worldI).multiply(view);
			let camObjPos = new Vector3().setFromMatrixPosition(camMatrixObject);
			camObjPositions.push(camObjPos);

			if (pointcloud.visible && pointcloud.root !== null) {
				priorityQueue.push({pointcloud: i, node: pointcloud.root, weight: Number.MAX_VALUE});
			}

			// hide all previously visible nodes
			// if(pointcloud.root instanceof PointCloudOctreeNode){
			//	pointcloud.hideDescendants(pointcloud.root.sceneNode);
			// }
			if (pointcloud.root.isTreeNode()) {
				pointcloud.hideDescendants(pointcloud.root.sceneNode);
			}

			for (let j = 0; j < pointcloud.boundingBoxNodes.length; j++) {
				pointcloud.boundingBoxNodes[j].visible = false;
			}
		}

		return {
			'frustums': frustums,
			'camObjPositions': camObjPositions,
			'priorityQueue': priorityQueue
		};
	};


	function updateVisibility(pointclouds, camera, renderer){

		let numVisibleNodes = 0;
		let numVisiblePoints = 0;

		let numVisiblePointsInPointclouds = new Map(pointclouds.map(pc => [pc, 0]));

		let visibleNodes = [];
		let visibleGeometry = [];
		let unloadedGeometry = [];

		let lowestSpacing = Infinity;

		// calculate object space frustum and cam pos and setup priority queue
		let s = updateVisibilityStructures(pointclouds, camera, renderer);
		let frustums = s.frustums;
		let camObjPositions = s.camObjPositions;
		let priorityQueue = s.priorityQueue;

		let loadedToGPUThisFrame = 0;
		
		let domWidth = renderer.domElement.clientWidth;
		let domHeight = renderer.domElement.clientHeight;

		// check if pointcloud has been transformed
		// some code will only be executed if changes have been detected
		if(!Potree._pointcloudTransformVersion){
			Potree._pointcloudTransformVersion = new Map();
		}
		let pointcloudTransformVersion = Potree._pointcloudTransformVersion;
		for(let pointcloud of pointclouds){

			if(!pointcloud.visible){
				continue;
			}

			pointcloud.updateMatrixWorld();

			if(!pointcloudTransformVersion.has(pointcloud)){
				pointcloudTransformVersion.set(pointcloud, {number: 0, transform: pointcloud.matrixWorld.clone()});
			}else {
				let version = pointcloudTransformVersion.get(pointcloud);

				if(!version.transform.equals(pointcloud.matrixWorld)){
					version.number++;
					version.transform.copy(pointcloud.matrixWorld);

					pointcloud.dispatchEvent({
						type: "transformation_changed",
						target: pointcloud
					});
				}
			}
		}

		while (priorityQueue.size() > 0) {
			let element = priorityQueue.pop();
			let node = element.node;
			let parent = element.parent;
			let pointcloud = pointclouds[element.pointcloud];

			// { // restrict to certain nodes for debugging
			//	let allowedNodes = ["r", "r0", "r4"];
			//	if(!allowedNodes.includes(node.name)){
			//		continue;
			//	}
			// }

			let box = node.getBoundingBox();
			let frustum = frustums[element.pointcloud];
			let camObjPos = camObjPositions[element.pointcloud];

			let insideFrustum = frustum.intersectsBox(box);
			let maxLevel = pointcloud.maxLevel || Infinity;
			let level = node.getLevel();
			let visible = insideFrustum;
			visible = visible && !(numVisiblePoints + node.getNumPoints() > Potree.pointBudget);
			visible = visible && !(numVisiblePointsInPointclouds.get(pointcloud) + node.getNumPoints() > pointcloud.pointBudget);
			visible = visible && level < maxLevel;
			visible = visible || node.getLevel() <= 2;

			let clipBoxes = pointcloud.material.clipBoxes;
			if(true && clipBoxes.length > 0){

				//node.debug = false;

				let numIntersecting = 0;
				let numIntersectionVolumes = 0;

				//if(node.name === "r60"){
				//	var a = 10;
				//}

				for(let clipBox of clipBoxes){

					let pcWorldInverse = pointcloud.matrixWorld.clone().invert();
					let toPCObject = pcWorldInverse.multiply(clipBox.box.matrixWorld);

					let px = new Vector3(+0.5, 0, 0).applyMatrix4(pcWorldInverse);
					let nx = new Vector3(-0.5, 0, 0).applyMatrix4(pcWorldInverse);
					let py = new Vector3(0, +0.5, 0).applyMatrix4(pcWorldInverse);
					let ny = new Vector3(0, -0.5, 0).applyMatrix4(pcWorldInverse);
					let pz = new Vector3(0, 0, +0.5).applyMatrix4(pcWorldInverse);
					let nz = new Vector3(0, 0, -0.5).applyMatrix4(pcWorldInverse);

					let pxN = new Vector3().subVectors(nx, px).normalize();
					let nxN = pxN.clone().multiplyScalar(-1);
					let pyN = new Vector3().subVectors(ny, py).normalize();
					let nyN = pyN.clone().multiplyScalar(-1);
					let pzN = new Vector3().subVectors(nz, pz).normalize();
					let nzN = pzN.clone().multiplyScalar(-1);

					let pxPlane = new Plane().setFromNormalAndCoplanarPoint(pxN, px);
					let nxPlane = new Plane().setFromNormalAndCoplanarPoint(nxN, nx);
					let pyPlane = new Plane().setFromNormalAndCoplanarPoint(pyN, py);
					let nyPlane = new Plane().setFromNormalAndCoplanarPoint(nyN, ny);
					let pzPlane = new Plane().setFromNormalAndCoplanarPoint(pzN, pz);
					let nzPlane = new Plane().setFromNormalAndCoplanarPoint(nzN, nz);

					//if(window.debugdraw !== undefined && window.debugdraw === true && node.name === "r60"){

					//	Potree.utils.debugPlane(viewer.scene.scene, pxPlane, 1, 0xFF0000);
					//	Potree.utils.debugPlane(viewer.scene.scene, nxPlane, 1, 0x990000);
					//	Potree.utils.debugPlane(viewer.scene.scene, pyPlane, 1, 0x00FF00);
					//	Potree.utils.debugPlane(viewer.scene.scene, nyPlane, 1, 0x009900);
					//	Potree.utils.debugPlane(viewer.scene.scene, pzPlane, 1, 0x0000FF);
					//	Potree.utils.debugPlane(viewer.scene.scene, nzPlane, 1, 0x000099);

					//	Potree.utils.debugBox(viewer.scene.scene, box, new THREE.Matrix4(), 0x00FF00);
					//	Potree.utils.debugBox(viewer.scene.scene, box, pointcloud.matrixWorld, 0xFF0000);
					//	Potree.utils.debugBox(viewer.scene.scene, clipBox.box.boundingBox, clipBox.box.matrixWorld, 0xFF0000);

					//	window.debugdraw = false;
					//}

					let frustum = new Frustum(pxPlane, nxPlane, pyPlane, nyPlane, pzPlane, nzPlane);
					let intersects = frustum.intersectsBox(box);

					if(intersects){
						numIntersecting++;
					}
					numIntersectionVolumes++;
				}

				let insideAny = numIntersecting > 0;
				let insideAll = numIntersecting === numIntersectionVolumes;

				if(pointcloud.material.clipTask === ClipTask.SHOW_INSIDE){
					if(pointcloud.material.clipMethod === ClipMethod.INSIDE_ANY && insideAny){
						//node.debug = true
					}else if(pointcloud.material.clipMethod === ClipMethod.INSIDE_ALL && insideAll){
						//node.debug = true;
					}else {
						visible = false;
					}
				} else if(pointcloud.material.clipTask === ClipTask.SHOW_OUTSIDE){
					//if(pointcloud.material.clipMethod === ClipMethod.INSIDE_ANY && !insideAny){
					//	//visible = true;
					//	let a = 10;
					//}else if(pointcloud.material.clipMethod === ClipMethod.INSIDE_ALL && !insideAll){
					//	//visible = true;
					//	let a = 20;
					//}else{
					//	visible = false;
					//}
				}
				

			}

			// visible = ["r", "r0", "r06", "r060"].includes(node.name);
			// visible = ["r"].includes(node.name);

			if (node.spacing) {
				lowestSpacing = Math.min(lowestSpacing, node.spacing);
			} else if (node.geometryNode && node.geometryNode.spacing) {
				lowestSpacing = Math.min(lowestSpacing, node.geometryNode.spacing);
			}

			if (numVisiblePoints + node.getNumPoints() > Potree.pointBudget) {
				break;
			}

			if (!visible) {
				/*if(node.isTreeNode()){
					node.geometryNode.dispose();
					console.log("Disposing Node ");
				}*/
				continue;
			}

			// TODO: not used, same as the declaration?
			// numVisibleNodes++;
			numVisiblePoints += node.getNumPoints();
			let numVisiblePointsInPointcloud = numVisiblePointsInPointclouds.get(pointcloud);
			numVisiblePointsInPointclouds.set(pointcloud, numVisiblePointsInPointcloud + node.getNumPoints());

			pointcloud.numVisibleNodes++;
			pointcloud.numVisiblePoints += node.getNumPoints();

			if (node.isGeometryNode() && (!parent || parent.isTreeNode())) {
				if (node.isLoaded() && loadedToGPUThisFrame < 2) {
					node = pointcloud.toTreeNode(node, parent);
					loadedToGPUThisFrame++;
				} else {
					unloadedGeometry.push(node);
					visibleGeometry.push(node);
				}
			}

			if (node.isTreeNode()) {
				exports.lru.touch(node.geometryNode);
				node.sceneNode.visible = true;
				node.sceneNode.material = pointcloud.material;

				visibleNodes.push(node);
				pointcloud.visibleNodes.push(node);

				if(node._transformVersion === undefined){
					node._transformVersion = -1;
				}
				let transformVersion = pointcloudTransformVersion.get(pointcloud);
				if(node._transformVersion !== transformVersion.number){
					node.sceneNode.updateMatrix();
					node.sceneNode.matrixWorld.multiplyMatrices(pointcloud.matrixWorld, node.sceneNode.matrix);	
					node._transformVersion = transformVersion.number;
				}

				if (pointcloud.showBoundingBox && !node.boundingBoxNode && node.getBoundingBox) {
					let boxHelper = new Box3Helper$1(node.getBoundingBox());
					boxHelper.matrixAutoUpdate = false;
					pointcloud.boundingBoxNodes.push(boxHelper);
					node.boundingBoxNode = boxHelper;
					node.boundingBoxNode.matrix.copy(pointcloud.matrixWorld);
				} else if (pointcloud.showBoundingBox) {
					node.boundingBoxNode.visible = true;
					node.boundingBoxNode.matrix.copy(pointcloud.matrixWorld);
				} else if (!pointcloud.showBoundingBox && node.boundingBoxNode) {
					node.boundingBoxNode.visible = false;
				}

				// if(node.boundingBoxNode !== undefined && exports.debug.allowedNodes !== undefined){
				// 	if(!exports.debug.allowedNodes.includes(node.name)){
				// 		node.boundingBoxNode.visible = false;
				// 	}
				// }
			}

			// add child nodes to priorityQueue
			let children = node.getChildren();
			for (let i = 0; i < children.length; i++) {
				let child = children[i];

				let weight = 0; 
				if(camera.isPerspectiveCamera){
					let sphere = child.getBoundingSphere();
					let center = sphere.center;
					//let distance = sphere.center.distanceTo(camObjPos);
					
					let dx = camObjPos.x - center.x;
					let dy = camObjPos.y - center.y;
					let dz = camObjPos.z - center.z;
					
					let dd = dx * dx + dy * dy + dz * dz;
					let distance = Math.sqrt(dd);
					
					
					let radius = sphere.radius;
					
					let fov = (camera.fov * Math.PI) / 180;
					let slope = Math.tan(fov / 2);
					let projFactor = (0.5 * domHeight) / (slope * distance);
					let screenPixelRadius = radius * projFactor;
					
					if(screenPixelRadius < pointcloud.minimumNodePixelSize){
						continue;
					}
				
					weight = screenPixelRadius;

					if(distance - radius < 0){
						weight = Number.MAX_VALUE;
					}
				} else {
					// TODO ortho visibility
					let bb = child.getBoundingBox();				
					let distance = child.getBoundingSphere().center.distanceTo(camObjPos);
					let diagonal = bb.max.clone().sub(bb.min).length();
					//weight = diagonal / distance;

					weight = diagonal;
				}

				priorityQueue.push({pointcloud: element.pointcloud, node: child, parent: node, weight: weight});
			}
		}// end priority queue loop

		{ // update DEM
			let maxDEMLevel = 4;
			let candidates = pointclouds
				.filter(p => (p.generateDEM && p.dem instanceof Potree.DEM));
			for (let pointcloud of candidates) {
				let updatingNodes = pointcloud.visibleNodes.filter(n => n.getLevel() <= maxDEMLevel);
				pointcloud.dem.update(updatingNodes);
			}
		}

		for (let i = 0; i < Math.min(Potree.maxNodesLoading, unloadedGeometry.length); i++) {
			unloadedGeometry[i].load();
		}

		return {
			visibleNodes: visibleNodes,
			numVisiblePoints: numVisiblePoints,
			lowestSpacing: lowestSpacing
		};
	};

	class PointCloudArena4DNode extends PointCloudTreeNode {
		constructor () {
			super();

			this.left = null;
			this.right = null;
			this.sceneNode = null;
			this.kdtree = null;
		}

		getNumPoints () {
			return this.geometryNode.numPoints;
		}

		isLoaded () {
			return true;
		}

		isTreeNode () {
			return true;
		}

		isGeometryNode () {
			return false;
		}

		getLevel () {
			return this.geometryNode.level;
		}

		getBoundingSphere () {
			return this.geometryNode.boundingSphere;
		}

		getBoundingBox () {
			return this.geometryNode.boundingBox;
		}

		toTreeNode (child) {
			let geometryNode = null;

			if (this.left === child) {
				geometryNode = this.left;
			} else if (this.right === child) {
				geometryNode = this.right;
			}

			if (!geometryNode.loaded) {
				return;
			}

			let node = new PointCloudArena4DNode();
			let sceneNode = PointCloud(geometryNode.geometry, this.kdtree.material);
			sceneNode.visible = false;

			node.kdtree = this.kdtree;
			node.geometryNode = geometryNode;
			node.sceneNode = sceneNode;
			node.parent = this;
			node.left = this.geometryNode.left;
			node.right = this.geometryNode.right;
		}

		getChildren () {
			let children = [];

			if (this.left) {
				children.push(this.left);
			}

			if (this.right) {
				children.push(this.right);
			}

			return children;
		}
	};

	class PointCloudArena4D extends PointCloudTree{
		constructor (geometry) {
			super();

			this.root = null;
			if (geometry.root) {
				this.root = geometry.root;
			} else {
				geometry.addEventListener('hierarchy_loaded', () => {
					this.root = geometry.root;
				});
			}

			this.visiblePointsTarget = 2 * 1000 * 1000;
			this.minimumNodePixelSize = 150;

			this.position.sub(geometry.offset);
			this.updateMatrix();

			this.numVisibleNodes = 0;
			this.numVisiblePoints = 0;

			this.boundingBoxNodes = [];
			this.loadQueue = [];
			this.visibleNodes = [];

			this.pcoGeometry = geometry;
			this.boundingBox = this.pcoGeometry.boundingBox;
			this.boundingSphere = this.pcoGeometry.boundingSphere;
			this.material = new PointCloudMaterial$1({vertexColors: VertexColors, size: 0.05, treeType: TreeType.KDTREE});
			this.material.sizeType = PointSizeType.ATTENUATED;
			this.material.size = 0.05;
			this.profileRequests = [];
			this.name = '';
		}

		getBoundingBoxWorld () {
			this.updateMatrixWorld(true);
			let box = this.boundingBox;
			let transform = this.matrixWorld;
			let tBox = Utils.computeTransformedBoundingBox(box, transform);

			return tBox;
		};

		setName (name) {
			if (this.name !== name) {
				this.name = name;
				this.dispatchEvent({type: 'name_changed', name: name, pointcloud: this});
			}
		}

		getName () {
			return this.name;
		}

		getLevel () {
			return this.level;
		}

		toTreeNode (geometryNode, parent) {
			let node = new PointCloudArena4DNode();
			let sceneNode = new Points(geometryNode.geometry, this.material);

			sceneNode.frustumCulled = false;
			sceneNode.onBeforeRender = (_this, scene, camera, geometry, material, group) => {
				if (material.program) {
					_this.getContext().useProgram(material.program.program);

					if (material.program.getUniforms().map.level) {
						let level = geometryNode.getLevel();
						material.uniforms.level.value = level;
						material.program.getUniforms().map.level.setValue(_this.getContext(), level);
					}

					if (this.visibleNodeTextureOffsets && material.program.getUniforms().map.vnStart) {
						let vnStart = this.visibleNodeTextureOffsets.get(node);
						material.uniforms.vnStart.value = vnStart;
						material.program.getUniforms().map.vnStart.setValue(_this.getContext(), vnStart);
					}

					if (material.program.getUniforms().map.pcIndex) {
						let i = node.pcIndex ? node.pcIndex : this.visibleNodes.indexOf(node);
						material.uniforms.pcIndex.value = i;
						material.program.getUniforms().map.pcIndex.setValue(_this.getContext(), i);
					}
				}
			};

			node.geometryNode = geometryNode;
			node.sceneNode = sceneNode;
			node.pointcloud = this;
			node.left = geometryNode.left;
			node.right = geometryNode.right;

			if (!parent) {
				this.root = node;
				this.add(sceneNode);
			} else {
				parent.sceneNode.add(sceneNode);

				if (parent.left === geometryNode) {
					parent.left = node;
				} else if (parent.right === geometryNode) {
					parent.right = node;
				}
			}

			let disposeListener = function () {
				parent.sceneNode.remove(node.sceneNode);

				if (parent.left === node) {
					parent.left = geometryNode;
				} else if (parent.right === node) {
					parent.right = geometryNode;
				}
			};
			geometryNode.oneTimeDisposeHandlers.push(disposeListener);

			return node;
		}

		updateMaterial (material, visibleNodes, camera, renderer) {
			material.fov = camera.fov * (Math.PI / 180);
			material.screenWidth = renderer.domElement.clientWidth;
			material.screenHeight = renderer.domElement.clientHeight;
			material.spacing = this.pcoGeometry.spacing;
			material.near = camera.near;
			material.far = camera.far;

			// reduce shader source updates by setting maxLevel slightly higher than actually necessary
			if (this.maxLevel > material.levels) {
				material.levels = this.maxLevel + 2;
			}

			// material.uniforms.octreeSize.value = this.boundingBox.size().x;
			let bbSize = this.boundingBox.getSize(new Vector3());
			material.bbSize = [bbSize.x, bbSize.y, bbSize.z];
		}

		updateVisibleBounds () {

		}

		hideDescendants (object) {
			let stack = [];
			for (let i = 0; i < object.children.length; i++) {
				let child = object.children[i];
				if (child.visible) {
					stack.push(child);
				}
			}

			while (stack.length > 0) {
				let child = stack.shift();

				child.visible = false;
				if (child.boundingBoxNode) {
					child.boundingBoxNode.visible = false;
				}

				for (let i = 0; i < child.children.length; i++) {
					let childOfChild = child.children[i];
					if (childOfChild.visible) {
						stack.push(childOfChild);
					}
				}
			}
		}

		updateMatrixWorld (force) {
			// node.matrixWorld.multiplyMatrices( node.parent.matrixWorld, node.matrix );

			if (this.matrixAutoUpdate === true) this.updateMatrix();

			if (this.matrixWorldNeedsUpdate === true || force === true) {
				if (this.parent === undefined) {
					this.matrixWorld.copy(this.matrix);
				} else {
					this.matrixWorld.multiplyMatrices(this.parent.matrixWorld, this.matrix);
				}

				this.matrixWorldNeedsUpdate = false;

				force = true;
			}
		}

		nodesOnRay (nodes, ray) {
			let nodesOnRay = [];

			let _ray = ray.clone();
			for (let i = 0; i < nodes.length; i++) {
				let node = nodes[i];
				let sphere = node.getBoundingSphere().clone().applyMatrix4(node.sceneNode.matrixWorld);
				// TODO Unused: let box = node.getBoundingBox().clone().applyMatrix4(node.sceneNode.matrixWorld);

				if (_ray.intersectsSphere(sphere)) {
					nodesOnRay.push(node);
				}
				// if(_ray.isIntersectionBox(box)){
				//	nodesOnRay.push(node);
				// }
			}

			return nodesOnRay;
		}

		pick(viewer, camera, ray, params = {}){

			let renderer = viewer.renderer;
			let pRenderer = viewer.pRenderer;

			performance.mark("pick-start");

			let getVal = (a, b) => a !== undefined ? a : b;

			let pickWindowSize = getVal(params.pickWindowSize, 17);
			let pickOutsideClipRegion = getVal(params.pickOutsideClipRegion, false);

			let size = renderer.getSize(new Vector2());

			let width = Math.ceil(getVal(params.width, size.width));
			let height = Math.ceil(getVal(params.height, size.height));

			let pointSizeType = getVal(params.pointSizeType, this.material.pointSizeType);
			let pointSize = getVal(params.pointSize, this.material.size);

			let nodes = this.nodesOnRay(this.visibleNodes, ray);

			if (nodes.length === 0) {
				return null;
			}

			if (!this.pickState) {
				let scene = new Scene();

				let material = new PointCloudMaterial$1();
				material.activeAttributeName = "indices";

				let renderTarget = new WebGLRenderTarget(
					1, 1,
					{ minFilter: LinearFilter,
						magFilter: NearestFilter,
						format: RGBAFormat }
				);

				this.pickState = {
					renderTarget: renderTarget,
					material: material,
					scene: scene
				};
			};

			let pickState = this.pickState;
			let pickMaterial = pickState.material;

			{ // update pick material
				pickMaterial.pointSizeType = pointSizeType;
				pickMaterial.shape = this.material.shape;

				pickMaterial.size = pointSize;
				pickMaterial.uniforms.minSize.value = this.material.uniforms.minSize.value;
				pickMaterial.uniforms.maxSize.value = this.material.uniforms.maxSize.value;
				pickMaterial.classification = this.material.classification;
				if(params.pickClipped){
					pickMaterial.clipBoxes = this.material.clipBoxes;
					if(this.material.clipTask === ClipTask.HIGHLIGHT){
						pickMaterial.clipTask = ClipTask.NONE;
					}else {
						pickMaterial.clipTask = this.material.clipTask;
					}
				}else {
					pickMaterial.clipBoxes = [];
				}
				
				this.updateMaterial(pickMaterial, nodes, camera, renderer);
			}

			pickState.renderTarget.setSize(width, height);

			let pixelPos = new Vector2(params.x, params.y);
			
			let gl = renderer.getContext();
			gl.enable(gl.SCISSOR_TEST);
			gl.scissor(
				parseInt(pixelPos.x - (pickWindowSize - 1) / 2),
				parseInt(pixelPos.y - (pickWindowSize - 1) / 2),
				parseInt(pickWindowSize), parseInt(pickWindowSize));


			renderer.state.buffers.depth.setTest(pickMaterial.depthTest);
			renderer.state.buffers.depth.setMask(pickMaterial.depthWrite);
			renderer.state.setBlending(NoBlending);

			renderer.clearTarget(pickState.renderTarget, true, true, true);

			{ // RENDER
				renderer.setRenderTarget(pickState.renderTarget);
				gl.clearColor(0, 0, 0, 0);
				renderer.clearTarget( pickState.renderTarget, true, true, true );
				
				let tmp = this.material;
				this.material = pickMaterial;
				
				pRenderer.renderOctree(this, nodes, camera, pickState.renderTarget);
				
				this.material = tmp;
			}

			let clamp = (number, min, max) => Math.min(Math.max(min, number), max);

			let x = parseInt(clamp(pixelPos.x - (pickWindowSize - 1) / 2, 0, width));
			let y = parseInt(clamp(pixelPos.y - (pickWindowSize - 1) / 2, 0, height));
			let w = parseInt(Math.min(x + pickWindowSize, width) - x);
			let h = parseInt(Math.min(y + pickWindowSize, height) - y);

			let pixelCount = w * h;
			let buffer = new Uint8Array(4 * pixelCount);
			
			gl.readPixels(x, y, pickWindowSize, pickWindowSize, gl.RGBA, gl.UNSIGNED_BYTE, buffer); 
			
			renderer.setRenderTarget(null);
			renderer.state.reset();
			renderer.setScissorTest(false);
			gl.disable(gl.SCISSOR_TEST);
			
			let pixels = buffer;
			let ibuffer = new Uint32Array(buffer.buffer);

			// find closest hit inside pixelWindow boundaries
			let min = Number.MAX_VALUE;
			let hits = [];
			for (let u = 0; u < pickWindowSize; u++) {
				for (let v = 0; v < pickWindowSize; v++) {
					let offset = (u + v * pickWindowSize);
					let distance = Math.pow(u - (pickWindowSize - 1) / 2, 2) + Math.pow(v - (pickWindowSize - 1) / 2, 2);

					let pcIndex = pixels[4 * offset + 3];
					pixels[4 * offset + 3] = 0;
					let pIndex = ibuffer[offset];

					if(!(pcIndex === 0 && pIndex === 0) && (pcIndex !== undefined) && (pIndex !== undefined)){
						let hit = {
							pIndex: pIndex,
							pcIndex: pcIndex,
							distanceToCenter: distance
						};

						if(params.all){
							hits.push(hit);
						}else {
							if(hits.length > 0){
								if(distance < hits[0].distanceToCenter){
									hits[0] = hit;
								}
							}else {
								hits.push(hit);
							}
						}

						
					}
				}
			}



			for(let hit of hits){
				let point = {};
			
				if (!nodes[hit.pcIndex]) {
					return null;
				}
			
				let node = nodes[hit.pcIndex];
				let pc = node.sceneNode;
				let geometry = node.geometryNode.geometry;
				
				for(let attributeName in geometry.attributes){
					let attribute = geometry.attributes[attributeName];
			
					if (attributeName === 'position') {
						let x = attribute.array[3 * hit.pIndex + 0];
						let y = attribute.array[3 * hit.pIndex + 1];
						let z = attribute.array[3 * hit.pIndex + 2];
						
						let position = new Vector3(x, y, z);
						position.applyMatrix4(pc.matrixWorld);
			
						point[attributeName] = position;
					} else if (attributeName === 'indices') {
			
					} else {
						//if (values.itemSize === 1) {
						//	point[attribute.name] = values.array[hit.pIndex];
						//} else {
						//	let value = [];
						//	for (let j = 0; j < values.itemSize; j++) {
						//		value.push(values.array[values.itemSize * hit.pIndex + j]);
						//	}
						//	point[attribute.name] = value;
						//}
					}
					
				}

				hit.point = point;
			}

			performance.mark("pick-end");
			performance.measure("pick", "pick-start", "pick-end");

			if(params.all){
				return hits.map(hit => hit.point);
			}else {
				if(hits.length === 0){
					return null;
				}else {
					return hits[0].point;
				}
			}
		}

		computeVisibilityTextureData(nodes){

			if(exports.measureTimings) performance.mark("computeVisibilityTextureData-start");

			let data = new Uint8Array(nodes.length * 3);
			let visibleNodeTextureOffsets = new Map();

			// copy array
			nodes = nodes.slice();

			// sort by level and number
			let sort = function (a, b) {
				let la = a.geometryNode.level;
				let lb = b.geometryNode.level;
				let na = a.geometryNode.number;
				let nb = b.geometryNode.number;
				if (la !== lb) return la - lb;
				if (na < nb) return -1;
				if (na > nb) return 1;
				return 0;
			};
			nodes.sort(sort);

			let visibleNodeNames = [];
			for (let i = 0; i < nodes.length; i++) {
				visibleNodeNames.push(nodes[i].geometryNode.number);
			}

			for (let i = 0; i < nodes.length; i++) {
				let node = nodes[i];

				visibleNodeTextureOffsets.set(node, i);

				let b1 = 0;	// children
				let b2 = 0;	// offset to first child
				let b3 = 0;	// split

				if (node.geometryNode.left && visibleNodeNames.indexOf(node.geometryNode.left.number) > 0) {
					b1 += 1;
					b2 = visibleNodeNames.indexOf(node.geometryNode.left.number) - i;
				}
				if (node.geometryNode.right && visibleNodeNames.indexOf(node.geometryNode.right.number) > 0) {
					b1 += 2;
					b2 = (b2 === 0) ? visibleNodeNames.indexOf(node.geometryNode.right.number) - i : b2;
				}

				if (node.geometryNode.split === 'X') {
					b3 = 1;
				} else if (node.geometryNode.split === 'Y') {
					b3 = 2;
				} else if (node.geometryNode.split === 'Z') {
					b3 = 4;
				}

				data[i * 3 + 0] = b1;
				data[i * 3 + 1] = b2;
				data[i * 3 + 2] = b3;
			}

			if(exports.measureTimings){
				performance.mark("computeVisibilityTextureData-end");
				performance.measure("render.computeVisibilityTextureData", "computeVisibilityTextureData-start", "computeVisibilityTextureData-end");
			}

			return {
				data: data,
				offsets: visibleNodeTextureOffsets
			};
		}

		get progress () {
			if (this.pcoGeometry.root) {
				return exports.numNodesLoading > 0 ? 0 : 1;
			} else {
				return 0;
			}
		}
	};

	// Copied from three.js: WebGLRenderer.js
	function paramThreeToGL(_gl, p) {

		let extension;

		if (p === RepeatWrapping) return _gl.REPEAT;
		if (p === ClampToEdgeWrapping) return _gl.CLAMP_TO_EDGE;
		if (p === MirroredRepeatWrapping) return _gl.MIRRORED_REPEAT;

		if (p === NearestFilter) return _gl.NEAREST;
		if (p === NearestMipMapNearestFilter) return _gl.NEAREST_MIPMAP_NEAREST;
		if (p === NearestMipMapLinearFilter) return _gl.NEAREST_MIPMAP_LINEAR;

		if (p === LinearFilter) return _gl.LINEAR;
		if (p === LinearMipMapNearestFilter) return _gl.LINEAR_MIPMAP_NEAREST;
		if (p === LinearMipMapLinearFilter) return _gl.LINEAR_MIPMAP_LINEAR;

		if (p === UnsignedByteType) return _gl.UNSIGNED_BYTE;
		if (p === UnsignedShort4444Type) return _gl.UNSIGNED_SHORT_4_4_4_4;
		if (p === UnsignedShort5551Type) return _gl.UNSIGNED_SHORT_5_5_5_1;
		if (p === UnsignedShort565Type) return _gl.UNSIGNED_SHORT_5_6_5;

		if (p === ByteType) return _gl.BYTE;
		if (p === ShortType) return _gl.SHORT;
		if (p === UnsignedShortType) return _gl.UNSIGNED_SHORT;
		if (p === IntType) return _gl.INT;
		if (p === UnsignedIntType) return _gl.UNSIGNED_INT;
		if (p === FloatType) return _gl.FLOAT;

		if (p === HalfFloatType) {

			extension = extensions.get('OES_texture_half_float');

			if (extension !== null) return extension.HALF_FLOAT_OES;

		}

		if (p === AlphaFormat) return _gl.ALPHA;
		if (p === RGBFormat) return _gl.RGB;
		if (p === RGBAFormat) return _gl.RGBA;
		if (p === LuminanceFormat) return _gl.LUMINANCE;
		if (p === LuminanceAlphaFormat) return _gl.LUMINANCE_ALPHA;
		if (p === DepthFormat) return _gl.DEPTH_COMPONENT;
		if (p === DepthStencilFormat) return _gl.DEPTH_STENCIL;

		if (p === AddEquation) return _gl.FUNC_ADD;
		if (p === SubtractEquation) return _gl.FUNC_SUBTRACT;
		if (p === ReverseSubtractEquation) return _gl.FUNC_REVERSE_SUBTRACT;

		if (p === ZeroFactor) return _gl.ZERO;
		if (p === OneFactor) return _gl.ONE;
		if (p === SrcColorFactor) return _gl.SRC_COLOR;
		if (p === OneMinusSrcColorFactor) return _gl.ONE_MINUS_SRC_COLOR;
		if (p === SrcAlphaFactor) return _gl.SRC_ALPHA;
		if (p === OneMinusSrcAlphaFactor) return _gl.ONE_MINUS_SRC_ALPHA;
		if (p === DstAlphaFactor) return _gl.DST_ALPHA;
		if (p === OneMinusDstAlphaFactor) return _gl.ONE_MINUS_DST_ALPHA;

		if (p === DstColorFactor) return _gl.DST_COLOR;
		if (p === OneMinusDstColorFactor) return _gl.ONE_MINUS_DST_COLOR;
		if (p === SrcAlphaSaturateFactor) return _gl.SRC_ALPHA_SATURATE;

		if (p === RGB_S3TC_DXT1_Format || p === RGBA_S3TC_DXT1_Format ||
			p === RGBA_S3TC_DXT3_Format || p === RGBA_S3TC_DXT5_Format) {

			extension = extensions.get('WEBGL_compressed_texture_s3tc');

			if (extension !== null) {

				if (p === RGB_S3TC_DXT1_Format) return extension.COMPRESSED_RGB_S3TC_DXT1_EXT;
				if (p === RGBA_S3TC_DXT1_Format$1) return extension.COMPRESSED_RGBA_S3TC_DXT1_EXT;
				if (p === RGBA_S3TC_DXT3_Format) return extension.COMPRESSED_RGBA_S3TC_DXT3_EXT;
				if (p === RGBA_S3TC_DXT5_Format$1) return extension.COMPRESSED_RGBA_S3TC_DXT5_EXT;

			}

		}

		if (p === RGB_PVRTC_4BPPV1_Format || p === RGB_PVRTC_2BPPV1_Format ||
			p === RGBA_PVRTC_4BPPV1_Format || p === RGBA_PVRTC_2BPPV1_Format) {

			extension = extensions.get('WEBGL_compressed_texture_pvrtc');

			if (extension !== null) {

				if (p === RGB_PVRTC_4BPPV1_Format) return extension.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
				if (p === RGB_PVRTC_2BPPV1_Format) return extension.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
				if (p === RGBA_PVRTC_4BPPV1_Format) return extension.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
				if (p === RGBA_PVRTC_2BPPV1_Format) return extension.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;

			}

		}

		if (p === RGB_ETC1_Format) {

			extension = extensions.get('WEBGL_compressed_texture_etc1');

			if (extension !== null) return extension.COMPRESSED_RGB_ETC1_WEBGL;

		}

		if (p === MinEquation || p === MaxEquation) {

			extension = extensions.get('EXT_blend_minmax');

			if (extension !== null) {

				if (p === MinEquation) return extension.MIN_EXT;
				if (p === MaxEquation) return extension.MAX_EXT;

			}

		}

		if (p === UnsignedInt248Type) {

			extension = extensions.get('WEBGL_depth_texture');

			if (extension !== null) return extension.UNSIGNED_INT_24_8_WEBGL;

		}

		return 0;

	};

	let attributeLocations = {
		"position": {name: "position", location: 0},
		"color": {name: "color", location: 1},
		"rgba": {name: "color", location: 1},
		"intensity": {name: "intensity", location: 2},
		"classification": {name: "classification", location: 3},
		"returnNumber": {name: "returnNumber", location: 4},
		"return number": {name: "returnNumber", location: 4},
		"returns": {name: "returnNumber", location: 4},
		"numberOfReturns": {name: "numberOfReturns", location: 5},
		"number of returns": {name: "numberOfReturns", location: 5},
		"pointSourceID": {name: "pointSourceID", location: 6},
		"source id": {name: "pointSourceID", location: 6},
		"point source id": {name: "pointSourceID", location: 6},
		"indices": {name: "indices", location: 7},
		"normal": {name: "normal", location: 8},
		"spacing": {name: "spacing", location: 9},
		"gps-time":  {name: "gpsTime", location: 10},
		"aExtra":  {name: "aExtra", location: 11},
	};

	class Shader {

		constructor(gl, name, vsSource, fsSource) {
			this.gl = gl;
			this.name = name;
			this.vsSource = vsSource;
			this.fsSource = fsSource;

			this.cache = new Map();

			this.vs = null;
			this.fs = null;
			this.program = null;

			this.uniformLocations = {};
			this.attributeLocations = {};
			this.uniformBlockIndices = {};
			this.uniformBlocks = {};
			this.uniforms = {};

			this.update(vsSource, fsSource);
		}

		update(vsSource, fsSource) {
			this.vsSource = vsSource;
			this.fsSource = fsSource;

			this.linkProgram();
		}

		compileShader(shader, source){
			let gl = this.gl;

			gl.shaderSource(shader, source);

			gl.compileShader(shader);

			let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
			if (!success) {
				let info = gl.getShaderInfoLog(shader);
				let numberedSource = source.split("\n").map((a, i) => `${i + 1}`.padEnd(5) + a).join("\n");
				throw `could not compile shader ${this.name}: ${info}, \n${numberedSource}`;
			}
		}

		linkProgram() {

			const tStart = performance.now();

			let gl = this.gl;

			this.uniformLocations = {};
			this.attributeLocations = {};
			this.uniforms = {};

			gl.useProgram(null);

			let cached = this.cache.get(`${this.vsSource}, ${this.fsSource}`);
			if (cached) {
				this.program = cached.program;
				this.vs = cached.vs;
				this.fs = cached.fs;
				this.attributeLocations = cached.attributeLocations;
				this.uniformLocations = cached.uniformLocations;
				this.uniformBlocks = cached.uniformBlocks;
				this.uniforms = cached.uniforms;

				return;
			} else {

				this.vs = gl.createShader(gl.VERTEX_SHADER);
				this.fs = gl.createShader(gl.FRAGMENT_SHADER);
				this.program = gl.createProgram();

				for(let name of Object.keys(attributeLocations)){
					let location = attributeLocations[name].location;
					let glslName = attributeLocations[name].name;
					gl.bindAttribLocation(this.program, location, glslName);
				}

				this.compileShader(this.vs, this.vsSource);
				this.compileShader(this.fs, this.fsSource);

				let program = this.program;

				gl.attachShader(program, this.vs);
				gl.attachShader(program, this.fs);

				gl.linkProgram(program);

				gl.detachShader(program, this.vs);
				gl.detachShader(program, this.fs);

				let success = gl.getProgramParameter(program, gl.LINK_STATUS);
				if (!success) {
					let info = gl.getProgramInfoLog(program);
					throw `could not link program ${this.name}: ${info}`;
				}

				{ // attribute locations
					let numAttributes = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES);

					for (let i = 0; i < numAttributes; i++) {
						let attribute = gl.getActiveAttrib(program, i);

						let location = gl.getAttribLocation(program, attribute.name);

						this.attributeLocations[attribute.name] = location;
					}
				}

				{ // uniform locations
					let numUniforms = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);

					for (let i = 0; i < numUniforms; i++) {
						let uniform = gl.getActiveUniform(program, i);

						let location = gl.getUniformLocation(program, uniform.name);

						this.uniformLocations[uniform.name] = location;
						this.uniforms[uniform.name] = {
							location: location,
							value: null,
						};
					}
				}

				// uniform blocks
				if(gl instanceof WebGL2RenderingContext){ 
					let numBlocks = gl.getProgramParameter(program, gl.ACTIVE_UNIFORM_BLOCKS);

					for (let i = 0; i < numBlocks; i++) {
						let blockName = gl.getActiveUniformBlockName(program, i);

						let blockIndex = gl.getUniformBlockIndex(program, blockName);

						this.uniformBlockIndices[blockName] = blockIndex;

						gl.uniformBlockBinding(program, blockIndex, blockIndex);
						let dataSize = gl.getActiveUniformBlockParameter(program, blockIndex, gl.UNIFORM_BLOCK_DATA_SIZE);

						let uBuffer = gl.createBuffer();	
						gl.bindBuffer(gl.UNIFORM_BUFFER, uBuffer);
						gl.bufferData(gl.UNIFORM_BUFFER, dataSize, gl.DYNAMIC_READ);

						gl.bindBufferBase(gl.UNIFORM_BUFFER, blockIndex, uBuffer);

						gl.bindBuffer(gl.UNIFORM_BUFFER, null);

						this.uniformBlocks[blockName] = {
							name: blockName,
							index: blockIndex,
							dataSize: dataSize,
							buffer: uBuffer
						};

					}
				}

				let cached = {
					program: this.program,
					vs: this.vs,
					fs: this.fs,
					attributeLocations: this.attributeLocations,
					uniformLocations: this.uniformLocations,
					uniforms: this.uniforms,
					uniformBlocks: this.uniformBlocks,
				};

				this.cache.set(`${this.vsSource}, ${this.fsSource}`, cached);
			}

			const tEnd = performance.now();
			const duration = tEnd - tStart;

			console.log(`shader compile duration: ${duration.toFixed(3)}`);


		}

		setUniformMatrix4(name, value) {
			const gl = this.gl;
			const location = this.uniformLocations[name];

			if (location == null) {
				return;
			}

			let tmp = new Float32Array(value.elements);
			gl.uniformMatrix4fv(location, false, tmp);
		}

		setUniform1f(name, value) {
			const gl = this.gl;
			const uniform = this.uniforms[name];

			if (uniform === undefined) {
				return;
			}

			if(uniform.value === value){
				return;
			}

			uniform.value = value;

			gl.uniform1f(uniform.location, value);
		}

		setUniformBoolean(name, value) {
			const gl = this.gl;
			const uniform = this.uniforms[name];

			if (uniform === undefined) {
				return;
			}

			if(uniform.value === value){
				return;
			}

			uniform.value = value;

			gl.uniform1i(uniform.location, value);
		}

		setUniformTexture(name, value) {
			const gl = this.gl;
			const location = this.uniformLocations[name];

			if (location == null) {
				return;
			}

			gl.uniform1i(location, value);
		}

		setUniform2f(name, value) {
			const gl = this.gl;
			const location = this.uniformLocations[name];

			if (location == null) {
				return;
			}

			gl.uniform2f(location, value[0], value[1]);
		}

		setUniform3f(name, value) {
			const gl = this.gl;
			const location = this.uniformLocations[name];

			if (location == null) {
				return;
			}

			gl.uniform3f(location, value[0], value[1], value[2]);
		}

		setUniform(name, value) {

			if (value.constructor === Matrix4) {
				this.setUniformMatrix4(name, value);
			} else if (typeof value === "number") {
				this.setUniform1f(name, value);
			} else if (typeof value === "boolean") {
				this.setUniformBoolean(name, value);
			} else if (value instanceof WebGLTexture) {
				this.setUniformTexture(name, value);
			} else if (value instanceof Array) {

				if (value.length === 2) {
					this.setUniform2f(name, value);
				} else if (value.length === 3) {
					this.setUniform3f(name, value);
				}

			} else {
				console.error("unhandled uniform type: ", name, value);
			}

		}


		setUniform1i(name, value) {
			let gl = this.gl;
			let location = this.uniformLocations[name];

			if (location == null) {
				return;
			}

			gl.uniform1i(location, value);
		}

	};

	class WebGLTexture {

		constructor(gl, texture) {
			this.gl = gl;

			this.texture = texture;
			this.id = gl.createTexture();

			this.target = gl.TEXTURE_2D;
			this.version = -1;

			this.update(texture);
		}

		update() {

			if (!this.texture.image) {
				this.version = this.texture.version;

				return;
			}

			let gl = this.gl;
			let texture = this.texture;

			if (this.version === texture.version) {
				return;
			}

			this.target = gl.TEXTURE_2D;

			gl.bindTexture(this.target, this.id);

			let level = 0;
			let internalFormat = paramThreeToGL(gl, texture.format);
			let width = texture.image.width;
			let height = texture.image.height;
			let border = 0;
			let srcFormat = internalFormat;
			let srcType = paramThreeToGL(gl, texture.type);
			let data;

			gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, texture.flipY);
			gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha);
			gl.pixelStorei(gl.UNPACK_ALIGNMENT, texture.unpackAlignment);

			if (texture instanceof DataTexture) {
				data = texture.image.data;

				gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
				gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

				gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, paramThreeToGL(gl, texture.magFilter));
				gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, paramThreeToGL(gl, texture.minFilter));

				gl.texImage2D(this.target, level, internalFormat,
					width, height, border, srcFormat, srcType,
					data);
			} else if ((texture instanceof CanvasTexture) || (texture instanceof Texture)) {
				data = texture.image;

				gl.texParameteri(this.target, gl.TEXTURE_WRAP_S, paramThreeToGL(gl, texture.wrapS));
				gl.texParameteri(this.target, gl.TEXTURE_WRAP_T, paramThreeToGL(gl, texture.wrapT));

				gl.texParameteri(this.target, gl.TEXTURE_MAG_FILTER, paramThreeToGL(gl, texture.magFilter));
				gl.texParameteri(this.target, gl.TEXTURE_MIN_FILTER, paramThreeToGL(gl, texture.minFilter));

				gl.texImage2D(this.target, level, internalFormat,
					internalFormat, srcType, data);

				if (texture instanceof Texture) {gl.generateMipmap(gl.TEXTURE_2D);}
			}

			gl.bindTexture(this.target, null);

			this.version = texture.version;
		}

	};

	class WebGLBuffer {

		constructor() {
			this.numElements = 0;
			this.vao = null;
			this.vbos = new Map();
		}

	};

	class Renderer {

		constructor(threeRenderer) {
			this.threeRenderer = threeRenderer;
			this.gl = this.threeRenderer.getContext();

			this.buffers = new Map();
			this.shaders = new Map();
			this.textures = new Map();

			this.glTypeMapping = new Map();
			this.glTypeMapping.set(Float32Array, this.gl.FLOAT);
			this.glTypeMapping.set(Uint8Array, this.gl.UNSIGNED_BYTE);
			this.glTypeMapping.set(Uint16Array, this.gl.UNSIGNED_SHORT);

			this.toggle = 0;
		}

		deleteBuffer(geometry) {

			let gl = this.gl;
			let webglBuffer = this.buffers.get(geometry);
			if (webglBuffer != null) {
				for (let attributeName in geometry.attributes) {
					gl.deleteBuffer(webglBuffer.vbos.get(attributeName).handle);
				}
				this.buffers.delete(geometry);
			}
		}

		createBuffer(geometry){
			let gl = this.gl;
			let webglBuffer = new WebGLBuffer();
			webglBuffer.vao = gl.createVertexArray();
			webglBuffer.numElements = geometry.attributes.position.count;

			gl.bindVertexArray(webglBuffer.vao);

			for(let attributeName in geometry.attributes){
				let bufferAttribute = geometry.attributes[attributeName];

				let vbo = gl.createBuffer();
				gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
				gl.bufferData(gl.ARRAY_BUFFER, bufferAttribute.array, gl.STATIC_DRAW);

				let normalized = bufferAttribute.normalized;
				let type = this.glTypeMapping.get(bufferAttribute.array.constructor);

				if(attributeLocations[attributeName] === undefined){
					//attributeLocation = attributeLocations["aExtra"];
				}else {
					let attributeLocation = attributeLocations[attributeName].location;

					gl.vertexAttribPointer(attributeLocation, bufferAttribute.itemSize, type, normalized, 0, 0);
					gl.enableVertexAttribArray(attributeLocation);
				}


				webglBuffer.vbos.set(attributeName, {
					handle: vbo,
					name: attributeName,
					count: bufferAttribute.count,
					itemSize: bufferAttribute.itemSize,
					type: geometry.attributes.position.array.constructor,
					version: 0
				});
			}

			gl.bindBuffer(gl.ARRAY_BUFFER, null);
			gl.bindVertexArray(null);

			let disposeHandler = (event) => {
				this.deleteBuffer(geometry);
				geometry.removeEventListener("dispose", disposeHandler);
			};
			geometry.addEventListener("dispose", disposeHandler);

			return webglBuffer;
		}

		updateBuffer(geometry){
			let gl = this.gl;

			let webglBuffer = this.buffers.get(geometry);

			gl.bindVertexArray(webglBuffer.vao);

			for(let attributeName in geometry.attributes){
				let bufferAttribute = geometry.attributes[attributeName];

				let normalized = bufferAttribute.normalized;
				let type = this.glTypeMapping.get(bufferAttribute.array.constructor);

				let vbo = null;
				if(!webglBuffer.vbos.has(attributeName)){
					vbo = gl.createBuffer();

					webglBuffer.vbos.set(attributeName, {
						handle: vbo,
						name: attributeName,
						count: bufferAttribute.count,
						itemSize: bufferAttribute.itemSize,
						type: geometry.attributes.position.array.constructor,
						version: bufferAttribute.version
					});
				}else {
					vbo = webglBuffer.vbos.get(attributeName).handle;
					webglBuffer.vbos.get(attributeName).version = bufferAttribute.version;
				}

				gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
				gl.bufferData(gl.ARRAY_BUFFER, bufferAttribute.array, gl.STATIC_DRAW);

				if(attributeLocations[attributeName] === undefined){
					//attributeLocation = attributeLocations["aExtra"];
				}else {
					let attributeLocation = attributeLocations[attributeName].location;
					
					gl.vertexAttribPointer(attributeLocation, bufferAttribute.itemSize, type, normalized, 0, 0);
					gl.enableVertexAttribArray(attributeLocation);
				}
			}

			gl.bindBuffer(gl.ARRAY_BUFFER, null);
			gl.bindVertexArray(null);
		}

		traverse(scene) {

			let octrees = [];

			let stack = [scene];
			while (stack.length > 0) {

				let node = stack.pop();

				if (node instanceof PointCloudTree) {
					octrees.push(node);
					continue;
				}

				let visibleChildren = node.children.filter(c => c.visible);
				stack.push(...visibleChildren);

			}

			let result = {
				octrees: octrees
			};

			return result;
		}



		renderNodes(octree, nodes, visibilityTextureData, camera, target, shader, params) {

			if (exports.measureTimings) performance.mark("renderNodes-start");

			let gl = this.gl;

			let material = params.material ? params.material : octree.material;
			let shadowMaps = params.shadowMaps == null ? [] : params.shadowMaps;
			let view = camera.matrixWorldInverse;

			if(params.viewOverride){
				view = params.viewOverride;
			}

			let worldView = new Matrix4();

			let mat4holder = new Float32Array(16);

			let i = 0;
			for (let node of nodes) {

				if(exports.debug.allowedNodes !== undefined){
					if(!exports.debug.allowedNodes.includes(node.name)){
						continue;
					}
				}

				let world = node.sceneNode.matrixWorld;
				worldView.multiplyMatrices(view, world);

				if (visibilityTextureData) {
					let vnStart = visibilityTextureData.offsets.get(node);
					shader.setUniform1f("uVNStart", vnStart);
				}


				let level = node.getLevel();

				if(node.debug){
					shader.setUniform("uDebug", true);
				}else {
					shader.setUniform("uDebug", false);
				}

				// let isLeaf = false;
				// if(node instanceof PointCloudOctreeNode){
				// 	isLeaf = Object.keys(node.children).length === 0;
				// }else if(node instanceof PointCloudArena4DNode){
				// 	isLeaf = node.geometryNode.isLeaf;
				// }
				// shader.setUniform("uIsLeafNode", isLeaf);

				// let isLeaf = node.children.filter(n => n != null).length === 0;
				// if(!isLeaf){
				// 	continue;
				// }


				// TODO consider passing matrices in an array to avoid uniformMatrix4fv overhead
				const lModel = shader.uniformLocations["modelMatrix"];
				if (lModel) {
					mat4holder.set(world.elements);
					gl.uniformMatrix4fv(lModel, false, mat4holder);
				}

				const lModelView = shader.uniformLocations["modelViewMatrix"];
				//mat4holder.set(worldView.elements);
				// faster then set in chrome 63
				for(let j = 0; j < 16; j++){
					mat4holder[j] = worldView.elements[j];
				}
				gl.uniformMatrix4fv(lModelView, false, mat4holder);

				{ // Clip Polygons
					if(material.clipPolygons && material.clipPolygons.length > 0){

						let clipPolygonVCount = [];
						let worldViewProjMatrices = [];

						for(let clipPolygon of material.clipPolygons){

							let view = clipPolygon.viewMatrix;
							let proj = clipPolygon.projMatrix;

							let worldViewProj = proj.clone().multiply(view).multiply(world);

							clipPolygonVCount.push(clipPolygon.markers.length);
							worldViewProjMatrices.push(worldViewProj);
						}

						let flattenedMatrices = [].concat(...worldViewProjMatrices.map(m => m.elements));

						let flattenedVertices = new Array(8 * 3 * material.clipPolygons.length);
						for(let i = 0; i < material.clipPolygons.length; i++){
							let clipPolygon = material.clipPolygons[i];
							for(let j = 0; j < clipPolygon.markers.length; j++){
								flattenedVertices[i * 24 + (j * 3 + 0)] = clipPolygon.markers[j].position.x;
								flattenedVertices[i * 24 + (j * 3 + 1)] = clipPolygon.markers[j].position.y;
								flattenedVertices[i * 24 + (j * 3 + 2)] = clipPolygon.markers[j].position.z;
							}
						}

						const lClipPolygonVCount = shader.uniformLocations["uClipPolygonVCount[0]"];
						gl.uniform1iv(lClipPolygonVCount, clipPolygonVCount);

						const lClipPolygonVP = shader.uniformLocations["uClipPolygonWVP[0]"];
						gl.uniformMatrix4fv(lClipPolygonVP, false, flattenedMatrices);

						const lClipPolygons = shader.uniformLocations["uClipPolygonVertices[0]"];
						gl.uniform3fv(lClipPolygons, flattenedVertices);

					}
				}


				//shader.setUniformMatrix4("modelMatrix", world);
				//shader.setUniformMatrix4("modelViewMatrix", worldView);
				shader.setUniform1f("uLevel", level);
				shader.setUniform1f("uNodeSpacing", node.geometryNode.estimatedSpacing);

				shader.setUniform1f("uPCIndex", i);
				// uBBSize

				if (shadowMaps.length > 0) {

					const lShadowMap = shader.uniformLocations["uShadowMap[0]"];

					shader.setUniform3f("uShadowColor", material.uniforms.uShadowColor.value);

					let bindingStart = 5;
					let bindingPoints = new Array(shadowMaps.length).fill(bindingStart).map((a, i) => (a + i));
					gl.uniform1iv(lShadowMap, bindingPoints);

					for (let i = 0; i < shadowMaps.length; i++) {
						let shadowMap = shadowMaps[i];
						let bindingPoint = bindingPoints[i];
						let glTexture = this.threeRenderer.properties.get(shadowMap.target.texture).__webglTexture;

						gl.activeTexture(gl[`TEXTURE${bindingPoint}`]);
						gl.bindTexture(gl.TEXTURE_2D, glTexture);
					}

					{

						let worldViewMatrices = shadowMaps
							.map(sm => sm.camera.matrixWorldInverse)
							.map(view => new Matrix4().multiplyMatrices(view, world));

						let flattenedMatrices = [].concat(...worldViewMatrices.map(c => c.elements));
						const lWorldView = shader.uniformLocations["uShadowWorldView[0]"];
						gl.uniformMatrix4fv(lWorldView, false, flattenedMatrices);
					}

					{
						let flattenedMatrices = [].concat(...shadowMaps.map(sm => sm.camera.projectionMatrix.elements));
						const lProj = shader.uniformLocations["uShadowProj[0]"];
						gl.uniformMatrix4fv(lProj, false, flattenedMatrices);
					}
				}

				const geometry = node.geometryNode.geometry;

				if(geometry.attributes["gps-time"]){
					const bufferAttribute = geometry.attributes["gps-time"];
					const attGPS = octree.getAttribute("gps-time");

					let initialRange = attGPS.initialRange;
					let initialRangeSize = initialRange[1] - initialRange[0];

					let globalRange = attGPS.range;
					let globalRangeSize = globalRange[1] - globalRange[0];

					let scale = initialRangeSize / globalRangeSize;
					let offset = -(globalRange[0] - initialRange[0]) / initialRangeSize;

					scale = Number.isNaN(scale) ? 1 : scale;
					offset = Number.isNaN(offset) ? 0 : offset;

					shader.setUniform1f("uGpsScale", scale);
					shader.setUniform1f("uGpsOffset", offset);
					//shader.setUniform2f("uFilterGPSTimeClipRange", [-Infinity, Infinity]);

					let uFilterGPSTimeClipRange = material.uniforms.uFilterGPSTimeClipRange.value;
					// let gpsCliPRangeMin = uFilterGPSTimeClipRange[0]
					// let gpsCliPRangeMax = uFilterGPSTimeClipRange[1]
					// shader.setUniform2f("uFilterGPSTimeClipRange", [gpsCliPRangeMin, gpsCliPRangeMax]);

					let normalizedClipRange = [
						(uFilterGPSTimeClipRange[0] - globalRange[0]) / globalRangeSize,
						(uFilterGPSTimeClipRange[1] - globalRange[0]) / globalRangeSize,
					];

					shader.setUniform2f("uFilterGPSTimeClipRange", normalizedClipRange);



					// // ranges in full gps coordinate system
					// const globalRange = attGPS.range;
					// const bufferRange = bufferAttribute.potree.range;

					// // ranges in [0, 1]
					// // normalizedGlobalRange = [0, 1]
					// // normalizedBufferRange: norm buffer within norm global range e.g. [0.2, 0.8]
					// const globalWidth = globalRange[1] - globalRange[0];
					// const normalizedBufferRange = [
					// 	(bufferRange[0] - globalRange[0]) / globalWidth,
					// 	(bufferRange[1] - globalRange[0]) / globalWidth,
					// ];

					// shader.setUniform2f("uNormalizedGpsBufferRange", normalizedBufferRange);

					// let uFilterGPSTimeClipRange = material.uniforms.uFilterGPSTimeClipRange.value;
					// let gpsCliPRangeMin = uFilterGPSTimeClipRange[0]
					// let gpsCliPRangeMax = uFilterGPSTimeClipRange[1]
					// shader.setUniform2f("uFilterGPSTimeClipRange", [gpsCliPRangeMin, gpsCliPRangeMax]);

					// shader.setUniform1f("uGpsScale", bufferAttribute.potree.scale);
					// shader.setUniform1f("uGpsOffset", bufferAttribute.potree.offset);
				}

				{
					let uFilterReturnNumberRange = material.uniforms.uFilterReturnNumberRange.value;
					let uFilterNumberOfReturnsRange = material.uniforms.uFilterNumberOfReturnsRange.value;
					let uFilterPointSourceIDClipRange = material.uniforms.uFilterPointSourceIDClipRange.value;
					
					
					
					shader.setUniform2f("uFilterReturnNumberRange", uFilterReturnNumberRange);
					shader.setUniform2f("uFilterNumberOfReturnsRange", uFilterNumberOfReturnsRange);
					shader.setUniform2f("uFilterPointSourceIDClipRange", uFilterPointSourceIDClipRange);
				}

				let webglBuffer = null;
				if(!this.buffers.has(geometry)){
					webglBuffer = this.createBuffer(geometry);
					this.buffers.set(geometry, webglBuffer);
				}else {
					webglBuffer = this.buffers.get(geometry);
					for(let attributeName in geometry.attributes){
						let attribute = geometry.attributes[attributeName];

						if(attribute.version > webglBuffer.vbos.get(attributeName).version){
							this.updateBuffer(geometry);
						}
					}
				}

				gl.bindVertexArray(webglBuffer.vao);

				let isExtraAttribute =
					attributeLocations[material.activeAttributeName] === undefined
					&& Object.keys(geometry.attributes).includes(material.activeAttributeName);

				if(isExtraAttribute){

					const attributeLocation = attributeLocations["aExtra"].location;

					for(const attributeName in geometry.attributes){
						const bufferAttribute = geometry.attributes[attributeName];
						const vbo = webglBuffer.vbos.get(attributeName);
						
						gl.bindBuffer(gl.ARRAY_BUFFER, vbo.handle);
						gl.disableVertexAttribArray(attributeLocation);
					}

					const attName = material.activeAttributeName;
					const bufferAttribute = geometry.attributes[attName];
					const vbo = webglBuffer.vbos.get(attName);

					if(bufferAttribute !== undefined && vbo !== undefined){
						let type = this.glTypeMapping.get(bufferAttribute.array.constructor);
						let normalized = bufferAttribute.normalized;

						gl.bindBuffer(gl.ARRAY_BUFFER, vbo.handle);
						gl.vertexAttribPointer(attributeLocation, bufferAttribute.itemSize, type, normalized, 0, 0);
						gl.enableVertexAttribArray(attributeLocation);
					}




					{
						const attExtra = octree.pcoGeometry.pointAttributes.attributes
							.find(a => a.name === attName);

						let range = material.getRange(attName);
						if(!range){
							range = attExtra.range;
						}

						if(!range){
							range = [0, 1];
						}

						let initialRange = attExtra.initialRange;
						let initialRangeSize = initialRange[1] - initialRange[0];

						let globalRange = range;
						let globalRangeSize = globalRange[1] - globalRange[0];

						let scale = initialRangeSize / globalRangeSize;
						let offset = -(globalRange[0] - initialRange[0]) / initialRangeSize;

						scale = Number.isNaN(scale) ? 1 : scale;
						offset = Number.isNaN(offset) ? 0 : offset;

						shader.setUniform1f("uExtraScale", scale);
						shader.setUniform1f("uExtraOffset", offset);					
					}

				}else {

					for(const attributeName in geometry.attributes){
						const bufferAttribute = geometry.attributes[attributeName];
						const vbo = webglBuffer.vbos.get(attributeName);


						if(attributeLocations[attributeName] !== undefined){
							const attributeLocation = attributeLocations[attributeName].location;

							let type = this.glTypeMapping.get(bufferAttribute.array.constructor);
							let normalized = bufferAttribute.normalized;
							
							gl.bindBuffer(gl.ARRAY_BUFFER, vbo.handle);
							gl.vertexAttribPointer(attributeLocation, bufferAttribute.itemSize, type, normalized, 0, 0);
							gl.enableVertexAttribArray(attributeLocation);
							
						}
					}
				}

				let numPoints = webglBuffer.numElements;
				gl.drawArrays(gl.POINTS, 0, numPoints);

				i++;
			}

			gl.bindVertexArray(null);

			if (exports.measureTimings) {
				performance.mark("renderNodes-end");
				performance.measure("render.renderNodes", "renderNodes-start", "renderNodes-end");
			}
		}

		renderOctree(octree, nodes, camera, target, params = {}){

			if(octree.pcoGeometry.root==null)
				return;
			
			let gl = this.gl;

			let material = params.material ? params.material : octree.material;
			let shadowMaps = params.shadowMaps == null ? [] : params.shadowMaps;
			let view = camera.matrixWorldInverse;
			let viewInv = camera.matrixWorld;

			if(params.viewOverride){
				view = params.viewOverride;
				viewInv = view.clone().invert();
			}

			let proj = camera.projectionMatrix;
			let projInv = proj.clone().invert();
			//let worldView = new THREE.Matrix4();

			let shader = null;
			let visibilityTextureData = null;

			let currentTextureBindingPoint = 0;

			if (material.pointSizeType >= 0) {
				if (material.pointSizeType === PointSizeType.ADAPTIVE ||
					material.activeAttributeName === "level of detail") {

					let vnNodes = (params.vnTextureNodes != null) ? params.vnTextureNodes : nodes;
					visibilityTextureData = octree.computeVisibilityTextureData(vnNodes, camera);

					const vnt = material.visibleNodesTexture;
					const data = vnt.image.data;
					data.set(visibilityTextureData.data);
					vnt.needsUpdate = true;

				}
			}

			{ // UPDATE SHADER AND TEXTURES
				if (!this.shaders.has(material)) {
					let [vs, fs] = [material.vertexShader, material.fragmentShader];
					let shader = new Shader(gl, "pointcloud", vs, fs);

					this.shaders.set(material, shader);
				}

				shader = this.shaders.get(material);

				//if(material.needsUpdate){
				{
					let [vs, fs] = [material.vertexShader, material.fragmentShader];

					let numSnapshots = material.snapEnabled ? material.numSnapshots : 0;
					let numClipBoxes = (material.clipBoxes && material.clipBoxes.length) ? material.clipBoxes.length : 0;
					let numClipSpheres = (params.clipSpheres && params.clipSpheres.length) ? params.clipSpheres.length : 0;
					let numClipPolygons = (material.clipPolygons && material.clipPolygons.length) ? material.clipPolygons.length : 0;

					let defines = [
						`#define num_shadowmaps ${shadowMaps.length}`,
						`#define num_snapshots ${numSnapshots}`,
						`#define num_clipboxes ${numClipBoxes}`,
						`#define num_clipspheres ${numClipSpheres}`,
						`#define num_clippolygons ${numClipPolygons}`,
					];


					if(octree.pcoGeometry.root.isLoaded()){
						let attributes = octree.pcoGeometry.root.geometry.attributes;

						if(attributes["gps-time"]){
							defines.push("#define clip_gps_enabled");
						}

						if(attributes["return number"]){
							defines.push("#define clip_return_number_enabled");
						}

						if(attributes["number of returns"]){
							defines.push("#define clip_number_of_returns_enabled");
						}

						if(attributes["source id"] || attributes["point source id"]){
							defines.push("#define clip_point_source_id_enabled");
						}

					}

					let definesString = defines.join("\n");

					let vsVersionIndex = vs.indexOf("#version ");
					let fsVersionIndex = fs.indexOf("#version ");

					if(vsVersionIndex >= 0){
						vs = vs.replace(/(#version .*)/, `$1\n${definesString}`);
					}else {
						vs = `${definesString}\n${vs}`;
					}

					if(fsVersionIndex >= 0){
						fs = fs.replace(/(#version .*)/, `$1\n${definesString}`);
					}else {
						fs = `${definesString}\n${fs}`;
					}


					shader.update(vs, fs);

					material.needsUpdate = false;
				}

				for (let uniformName of Object.keys(material.uniforms)) {
					let uniform = material.uniforms[uniformName];

					if (uniform.type == "t") {

						let texture = uniform.value;

						if (!texture) {
							continue;
						}

						if (!this.textures.has(texture)) {
							let webglTexture = new WebGLTexture(gl, texture);

							this.textures.set(texture, webglTexture);
						}

						let webGLTexture = this.textures.get(texture);
						webGLTexture.update();


					}
				}
			}

			gl.useProgram(shader.program);

			let transparent = false;
			if(params.transparent !== undefined){
				transparent = params.transparent && material.opacity < 1;
			}else {
				transparent = material.opacity < 1;
			}

			if (transparent){
				gl.enable(gl.BLEND);
				gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
				gl.depthMask(false);
				gl.disable(gl.DEPTH_TEST);
			} else {
				gl.disable(gl.BLEND);
				gl.depthMask(true);
				gl.enable(gl.DEPTH_TEST);
			}

			if(params.blendFunc !== undefined){
				gl.enable(gl.BLEND);
				gl.blendFunc(...params.blendFunc);
			}

			if(params.depthTest !== undefined){
				if(params.depthTest === true){
					gl.enable(gl.DEPTH_TEST);
				}else {
					gl.disable(gl.DEPTH_TEST);
				}
			}

			if(params.depthWrite !== undefined){
				 if(params.depthWrite === true){
					 gl.depthMask(true);
				 }else {
					 gl.depthMask(false);
				 }
				 
			}


			{ // UPDATE UNIFORMS
				shader.setUniformMatrix4("projectionMatrix", proj);
				shader.setUniformMatrix4("viewMatrix", view);
				shader.setUniformMatrix4("uViewInv", viewInv);
				shader.setUniformMatrix4("uProjInv", projInv);

				let screenWidth = target ? target.width : material.screenWidth;
				let screenHeight = target ? target.height : material.screenHeight;

				shader.setUniform1f("uScreenWidth", screenWidth);
				shader.setUniform1f("uScreenHeight", screenHeight);
				shader.setUniform1f("fov", Math.PI * camera.fov / 180);
				shader.setUniform1f("near", camera.near);
				shader.setUniform1f("far", camera.far);
				
				if(camera instanceof OrthographicCamera){
					shader.setUniform("uUseOrthographicCamera", true);
					shader.setUniform("uOrthoWidth", camera.right - camera.left); 
					shader.setUniform("uOrthoHeight", camera.top - camera.bottom);
				}else {
					shader.setUniform("uUseOrthographicCamera", false);
				}

				if(material.clipBoxes.length + material.clipPolygons.length === 0){
					shader.setUniform1i("clipTask", ClipTask.NONE);
				}else {
					shader.setUniform1i("clipTask", material.clipTask);
				}

				shader.setUniform1i("clipMethod", material.clipMethod);

				if (material.clipBoxes && material.clipBoxes.length > 0) {
					//let flattenedMatrices = [].concat(...material.clipBoxes.map(c => c.inverse.elements));

					//const lClipBoxes = shader.uniformLocations["clipBoxes[0]"];
					//gl.uniformMatrix4fv(lClipBoxes, false, flattenedMatrices);

					const lClipBoxes = shader.uniformLocations["clipBoxes[0]"];
					gl.uniformMatrix4fv(lClipBoxes, false, material.uniforms.clipBoxes.value);
				}

				// TODO CLIPSPHERES
				if(params.clipSpheres && params.clipSpheres.length > 0){

					let clipSpheres = params.clipSpheres;

					let matrices = [];
					for(let clipSphere of clipSpheres){
						//let mScale = new THREE.Matrix4().makeScale(...clipSphere.scale.toArray());
						//let mTranslate = new THREE.Matrix4().makeTranslation(...clipSphere.position.toArray());

						//let clipToWorld = new THREE.Matrix4().multiplyMatrices(mTranslate, mScale);
						let clipToWorld = clipSphere.matrixWorld;
						let viewToWorld = camera.matrixWorld;
						let worldToClip = clipToWorld.clone().invert();

						let viewToClip = new Matrix4().multiplyMatrices(worldToClip, viewToWorld);

						matrices.push(viewToClip);
					}

					let flattenedMatrices = [].concat(...matrices.map(matrix => matrix.elements));

					const lClipSpheres = shader.uniformLocations["uClipSpheres[0]"];
					gl.uniformMatrix4fv(lClipSpheres, false, flattenedMatrices);
					
					//const lClipSpheres = shader.uniformLocations["uClipSpheres[0]"];
					//gl.uniformMatrix4fv(lClipSpheres, false, material.uniforms.clipSpheres.value);
				}


				shader.setUniform1f("size", material.size);
				shader.setUniform1f("maxSize", material.uniforms.maxSize.value);
				shader.setUniform1f("minSize", material.uniforms.minSize.value);


				// uniform float uPCIndex
				shader.setUniform1f("uOctreeSpacing", material.spacing);
				shader.setUniform("uOctreeSize", material.uniforms.octreeSize.value);


				//uniform vec3 uColor;
				shader.setUniform3f("uColor", material.color.toArray());
				//uniform float opacity;
				shader.setUniform1f("uOpacity", material.opacity);

				shader.setUniform2f("elevationRange", material.elevationRange);
				shader.setUniform2f("intensityRange", material.intensityRange);


				shader.setUniform3f("uIntensity_gbc", [
					material.intensityGamma, 
					material.intensityBrightness, 
					material.intensityContrast
				]);

				shader.setUniform3f("uRGB_gbc", [
					material.rgbGamma, 
					material.rgbBrightness, 
					material.rgbContrast
				]);

				shader.setUniform1f("uTransition", material.transition);
				shader.setUniform1f("wRGB", material.weightRGB);
				shader.setUniform1f("wIntensity", material.weightIntensity);
				shader.setUniform1f("wElevation", material.weightElevation);
				shader.setUniform1f("wClassification", material.weightClassification);
				shader.setUniform1f("wReturnNumber", material.weightReturnNumber);
				shader.setUniform1f("wSourceID", material.weightSourceID);

				shader.setUniform("backfaceCulling", material.uniforms.backfaceCulling.value);

				let vnWebGLTexture = this.textures.get(material.visibleNodesTexture);
				if(vnWebGLTexture){
					shader.setUniform1i("visibleNodesTexture", currentTextureBindingPoint);
					gl.activeTexture(gl.TEXTURE0 + currentTextureBindingPoint);
					gl.bindTexture(vnWebGLTexture.target, vnWebGLTexture.id);
					currentTextureBindingPoint++;
				}

				let gradientTexture = this.textures.get(material.gradientTexture);
				shader.setUniform1i("gradient", currentTextureBindingPoint);
				gl.activeTexture(gl.TEXTURE0 + currentTextureBindingPoint);
				gl.bindTexture(gradientTexture.target, gradientTexture.id);

				const repeat = material.elevationGradientRepeat;
				if(repeat === ElevationGradientRepeat.REPEAT){
					gl.texParameteri(gradientTexture.target, gl.TEXTURE_WRAP_S, gl.REPEAT);
					gl.texParameteri(gradientTexture.target, gl.TEXTURE_WRAP_T, gl.REPEAT);
				}else if(repeat === ElevationGradientRepeat.MIRRORED_REPEAT){
					gl.texParameteri(gradientTexture.target, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
					gl.texParameteri(gradientTexture.target, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);
				}else {
					gl.texParameteri(gradientTexture.target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
					gl.texParameteri(gradientTexture.target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
				}
				currentTextureBindingPoint++;

				let classificationTexture = this.textures.get(material.classificationTexture);
				shader.setUniform1i("classificationLUT", currentTextureBindingPoint);
				gl.activeTexture(gl.TEXTURE0 + currentTextureBindingPoint);
				gl.bindTexture(classificationTexture.target, classificationTexture.id);
				currentTextureBindingPoint++;

				let matcapTexture = this.textures.get(material.matcapTexture);
				shader.setUniform1i("matcapTextureUniform", currentTextureBindingPoint);
				gl.activeTexture(gl.TEXTURE0 + currentTextureBindingPoint);
				gl.bindTexture(matcapTexture.target, matcapTexture.id);
				currentTextureBindingPoint++;


				if (material.snapEnabled === true) {

					{
						const lSnapshot = shader.uniformLocations["uSnapshot[0]"];
						const lSnapshotDepth = shader.uniformLocations["uSnapshotDepth[0]"];

						let bindingStart = currentTextureBindingPoint;
						let lSnapshotBindingPoints = new Array(5).fill(bindingStart).map((a, i) => (a + i));
						let lSnapshotDepthBindingPoints = new Array(5)
							.fill(1 + Math.max(...lSnapshotBindingPoints))
							.map((a, i) => (a + i));
						currentTextureBindingPoint = 1 + Math.max(...lSnapshotDepthBindingPoints);

						gl.uniform1iv(lSnapshot, lSnapshotBindingPoints);
						gl.uniform1iv(lSnapshotDepth, lSnapshotDepthBindingPoints);

						for (let i = 0; i < 5; i++) {
							let texture = material.uniforms[`uSnapshot`].value[i];
							let textureDepth = material.uniforms[`uSnapshotDepth`].value[i];

							if (!texture) {
								break;
							}

							let snapTexture = this.threeRenderer.properties.get(texture).__webglTexture;
							let snapTextureDepth = this.threeRenderer.properties.get(textureDepth).__webglTexture;

							let bindingPoint = lSnapshotBindingPoints[i];
							let depthBindingPoint = lSnapshotDepthBindingPoints[i];

							gl.activeTexture(gl[`TEXTURE${bindingPoint}`]);
							gl.bindTexture(gl.TEXTURE_2D, snapTexture);

							gl.activeTexture(gl[`TEXTURE${depthBindingPoint}`]);
							gl.bindTexture(gl.TEXTURE_2D, snapTextureDepth);
						}
					}

					{
						let flattenedMatrices = [].concat(...material.uniforms.uSnapView.value.map(c => c.elements));
						const lSnapView = shader.uniformLocations["uSnapView[0]"];
						gl.uniformMatrix4fv(lSnapView, false, flattenedMatrices);
					}
					{
						let flattenedMatrices = [].concat(...material.uniforms.uSnapProj.value.map(c => c.elements));
						const lSnapProj = shader.uniformLocations["uSnapProj[0]"];
						gl.uniformMatrix4fv(lSnapProj, false, flattenedMatrices);
					}
					{
						let flattenedMatrices = [].concat(...material.uniforms.uSnapProjInv.value.map(c => c.elements));
						const lSnapProjInv = shader.uniformLocations["uSnapProjInv[0]"];
						gl.uniformMatrix4fv(lSnapProjInv, false, flattenedMatrices);
					}
					{
						let flattenedMatrices = [].concat(...material.uniforms.uSnapViewInv.value.map(c => c.elements));
						const lSnapViewInv = shader.uniformLocations["uSnapViewInv[0]"];
						gl.uniformMatrix4fv(lSnapViewInv, false, flattenedMatrices);
					}

				}
			}

			this.renderNodes(octree, nodes, visibilityTextureData, camera, target, shader, params);

			gl.activeTexture(gl.TEXTURE2);
			gl.bindTexture(gl.TEXTURE_2D, null);
			gl.activeTexture(gl.TEXTURE0);
		}

		render(scene, camera, target = null, params = {}) {

			const gl = this.gl;

			// PREPARE 
			if (target != null) {
				this.threeRenderer.setRenderTarget(target);
			}

			//camera.updateProjectionMatrix();
			// camera.matrixWorldInverse.invert(camera.matrixWorld);

			const traversalResult = this.traverse(scene);


			// RENDER
			for (const octree of traversalResult.octrees) {
				let nodes = octree.visibleNodes;
				this.renderOctree(octree, nodes, camera, target, params);
			}


			// CLEANUP
			gl.activeTexture(gl.TEXTURE1);
			gl.bindTexture(gl.TEXTURE_2D, null);
			gl.bindBuffer(gl.ARRAY_BUFFER, null);
			gl.bindVertexArray(null);

			this.threeRenderer.resetState();
		}



	};

	class ProfileData {
		constructor (profile) {
			this.profile = profile;

			this.segments = [];
			this.boundingBox = new Box3();

			for (let i = 0; i < profile.points.length - 1; i++) {
				let start = profile.points[i];
				let end = profile.points[i + 1];

				let startGround = new Vector3(start.x, start.y, 0);
				let endGround = new Vector3(end.x, end.y, 0);

				let center = new Vector3().addVectors(endGround, startGround).multiplyScalar(0.5);
				let length = startGround.distanceTo(endGround);
				let side = new Vector3().subVectors(endGround, startGround).normalize();
				let up = new Vector3(0, 0, 1);
				let forward = new Vector3().crossVectors(side, up).normalize();
				let N = forward;
				let cutPlane = new Plane().setFromNormalAndCoplanarPoint(N, startGround);
				let halfPlane = new Plane().setFromNormalAndCoplanarPoint(side, center);

				let segment = {
					start: start,
					end: end,
					cutPlane: cutPlane,
					halfPlane: halfPlane,
					length: length,
					points: new Points$1()
				};

				this.segments.push(segment);
			}
		}

		size () {
			let size = 0;
			for (let segment of this.segments) {
				size += segment.points.numPoints;
			}

			return size;
		}
	};

	class ProfileRequest {
		constructor (pointcloud, profile, maxDepth, callback) {
			this.pointcloud = pointcloud;
			this.profile = profile;
			this.maxDepth = maxDepth || Number.MAX_VALUE;
			this.callback = callback;
			this.temporaryResult = new ProfileData(this.profile);
			this.pointsServed = 0;
			this.highestLevelServed = 0;

			this.priorityQueue = new BinaryHeap(function (x) { return 1 / x.weight; });

			this.initialize();
		}

		initialize () {
			this.priorityQueue.push({node: this.pointcloud.pcoGeometry.root, weight: Infinity});
		};

		// traverse the node and add intersecting descendants to queue
		traverse (node) {
			let stack = [];
			for (let i = 0; i < 8; i++) {
				let child = node.children[i];
				if (child && this.pointcloud.nodeIntersectsProfile(child, this.profile)) {
					stack.push(child);
				}
			}

			while (stack.length > 0) {
				let node = stack.pop();
				let weight = node.boundingSphere.radius;

				this.priorityQueue.push({node: node, weight: weight});

				// add children that intersect the cutting plane
				if (node.level < this.maxDepth) {
					for (let i = 0; i < 8; i++) {
						let child = node.children[i];
						if (child && this.pointcloud.nodeIntersectsProfile(child, this.profile)) {
							stack.push(child);
						}
					}
				}
			}
		}

		update(){
			if(!this.updateGeneratorInstance){
				this.updateGeneratorInstance = this.updateGenerator();
			}

			let result = this.updateGeneratorInstance.next();
			if(result.done){
				this.updateGeneratorInstance = null;
			}
		}

		* updateGenerator(){
			// load nodes in queue
			// if hierarchy expands, also load nodes from expanded hierarchy
			// once loaded, add data to this.points and remove node from queue
			// only evaluate 1-50 nodes per frame to maintain responsiveness

			let start = performance.now();

			let maxNodesPerUpdate = 1;
			let intersectedNodes = [];

			for (let i = 0; i < Math.min(maxNodesPerUpdate, this.priorityQueue.size()); i++) {
				let element = this.priorityQueue.pop();
				let node = element.node;

				if(node.level > this.maxDepth){
					continue;
				}

				if (node.loaded) {
					// add points to result
					intersectedNodes.push(node);
					exports.lru.touch(node);
					this.highestLevelServed = Math.max(node.getLevel(), this.highestLevelServed);

					var geom = node.pcoGeometry;
					var hierarchyStepSize = geom ? geom.hierarchyStepSize : 1;

					var doTraverse = node.getLevel() === 0 ||
						(node.level % hierarchyStepSize === 0 && node.hasChildren);

					if (doTraverse) {
						this.traverse(node);
					}
				} else {
					node.load();
					this.priorityQueue.push(element);
				}
			}

			if (intersectedNodes.length > 0) {

				for(let done of this.getPointsInsideProfile(intersectedNodes, this.temporaryResult)){
					if(!done){
						//console.log("updateGenerator yields");
						yield false;
					}
				}
				if (this.temporaryResult.size() > 100) {
					this.pointsServed += this.temporaryResult.size();
					this.callback.onProgress({request: this, points: this.temporaryResult});
					this.temporaryResult = new ProfileData(this.profile);
				}
			}

			if (this.priorityQueue.size() === 0) {
				// we're done! inform callback and remove from pending requests

				if (this.temporaryResult.size() > 0) {
					this.pointsServed += this.temporaryResult.size();
					this.callback.onProgress({request: this, points: this.temporaryResult});
					this.temporaryResult = new ProfileData(this.profile);
				}

				this.callback.onFinish({request: this});

				let index = this.pointcloud.profileRequests.indexOf(this);
				if (index >= 0) {
					this.pointcloud.profileRequests.splice(index, 1);
				}
			}

			yield true;
		};

		* getAccepted(numPoints, node, matrix, segment, segmentDir, points, totalMileage){
			let checkpoint = performance.now();

			let accepted = new Uint32Array(numPoints);
			let mileage = new Float64Array(numPoints);
			let acceptedPositions = new Float32Array(numPoints * 3);
			let numAccepted = 0;

			let pos = new Vector3();
			let svp = new Vector3();

			let view = new Float32Array(node.geometry.attributes.position.array);

			for (let i = 0; i < numPoints; i++) {

				pos.set(
					view[i * 3 + 0],
					view[i * 3 + 1],
					view[i * 3 + 2]);

				pos.applyMatrix4(matrix);
				let distance = Math.abs(segment.cutPlane.distanceToPoint(pos));
				let centerDistance = Math.abs(segment.halfPlane.distanceToPoint(pos));

				if (distance < this.profile.width / 2 && centerDistance < segment.length / 2) {
					svp.subVectors(pos, segment.start);
					let localMileage = segmentDir.dot(svp);

					accepted[numAccepted] = i;
					mileage[numAccepted] = localMileage + totalMileage;
					points.boundingBox.expandByPoint(pos);

					pos.sub(this.pointcloud.position);

					acceptedPositions[3 * numAccepted + 0] = pos.x;
					acceptedPositions[3 * numAccepted + 1] = pos.y;
					acceptedPositions[3 * numAccepted + 2] = pos.z;

					numAccepted++;
				}

				if((i % 1000) === 0){
					let duration = performance.now() - checkpoint;
					if(duration > 4){
						//console.log(`getAccepted yield after ${duration}ms`);
						yield false;
						checkpoint = performance.now();
					}
				}
			}

			accepted = accepted.subarray(0, numAccepted);
			mileage = mileage.subarray(0, numAccepted);
			acceptedPositions = acceptedPositions.subarray(0, numAccepted * 3);

			//let end = performance.now();
			//let duration = end - start;
			//console.log("accepted duration ", duration)

			//console.log(`getAccepted finished`);

			yield [accepted, mileage, acceptedPositions];
		}

		* getPointsInsideProfile(nodes, target){
			let checkpoint = performance.now();
			let totalMileage = 0;

			let pointsProcessed = 0;

			for (let segment of target.segments) {
				for (let node of nodes) {
					let numPoints = node.numPoints;
					let geometry = node.geometry;

					if(!numPoints){
						continue;
					}

					{ // skip if current node doesn't intersect current segment
						let bbWorld = node.boundingBox.clone().applyMatrix4(this.pointcloud.matrixWorld);
						let bsWorld = bbWorld.getBoundingSphere(new Sphere());

						let start = new Vector3(segment.start.x, segment.start.y, bsWorld.center.z);
						let end = new Vector3(segment.end.x, segment.end.y, bsWorld.center.z);

						let closest = new Line3(start, end).closestPointToPoint(bsWorld.center, true, new Vector3());
						let distance = closest.distanceTo(bsWorld.center);

						let intersects = (distance < (bsWorld.radius + target.profile.width));

						if(!intersects){
							continue;
						}
					}

					//{// DEBUG
					//	console.log(node.name);
					//	let boxHelper = new Potree.Box3Helper(node.getBoundingBox());
					//	boxHelper.matrixAutoUpdate = false;
					//	boxHelper.matrix.copy(viewer.scene.pointclouds[0].matrixWorld);
					//	viewer.scene.scene.add(boxHelper);
					//}

					let sv = new Vector3().subVectors(segment.end, segment.start).setZ(0);
					let segmentDir = sv.clone().normalize();

					let points = new Points$1();

					let nodeMatrix = new Matrix4().makeTranslation(...node.boundingBox.min.toArray());

					let matrix = new Matrix4().multiplyMatrices(
						this.pointcloud.matrixWorld, nodeMatrix);

					pointsProcessed = pointsProcessed + numPoints;

					let accepted = null;
					let mileage = null;
					let acceptedPositions = null;
					for(let result of this.getAccepted(numPoints, node, matrix, segment, segmentDir, points,totalMileage)){
						if(!result){
							let duration = performance.now() - checkpoint;
							//console.log(`getPointsInsideProfile yield after ${duration}ms`);
							yield false;
							checkpoint = performance.now();
						}else {
							[accepted, mileage, acceptedPositions] = result;
						}
					}

					let duration = performance.now() - checkpoint;
					if(duration > 4){
						//console.log(`getPointsInsideProfile yield after ${duration}ms`);
						yield false;
						checkpoint = performance.now();
					}

					points.data.position = acceptedPositions;

					let relevantAttributes = Object.keys(geometry.attributes).filter(a => !["position", "indices"].includes(a));
					for(let attributeName of relevantAttributes){

						let attribute = geometry.attributes[attributeName];
						let numElements = attribute.array.length / numPoints;

						if(numElements !== parseInt(numElements)){
							debugger;
						}

						let Type = attribute.array.constructor;

						let filteredBuffer = new Type(numElements * accepted.length);

						let source = attribute.array;
						let target = filteredBuffer;

						for(let i = 0; i < accepted.length; i++){

							let index = accepted[i];

							let start = index * numElements;
							let end = start + numElements;
							let sub = source.subarray(start, end);

							target.set(sub, i * numElements);
						}

						points.data[attributeName] = filteredBuffer;
					}

					points.data['mileage'] = mileage;
					points.numPoints = accepted.length;

					segment.points.add(points);
				}

				totalMileage += segment.length;
			}

			for (let segment of target.segments) {
				target.boundingBox.union(segment.points.boundingBox);
			}

			//console.log(`getPointsInsideProfile finished`);
			yield true;
		};

		finishLevelThenCancel () {
			if (this.cancelRequested) {
				return;
			}

			this.maxDepth = this.highestLevelServed;
			this.cancelRequested = true;

			//console.log(`maxDepth: ${this.maxDepth}`);
		};

		cancel () {
			this.callback.onCancel();

			this.priorityQueue = new BinaryHeap(function (x) { return 1 / x.weight; });

			let index = this.pointcloud.profileRequests.indexOf(this);
			if (index >= 0) {
				this.pointcloud.profileRequests.splice(index, 1);
			}
		};
	}

	class Version{

		constructor(version){
			this.version = version;
			let vmLength = (version.indexOf('.') === -1) ? version.length : version.indexOf('.');
			this.versionMajor = parseInt(version.substr(0, vmLength));
			this.versionMinor = parseInt(version.substr(vmLength + 1));
			if (this.versionMinor.length === 0) {
				this.versionMinor = 0;
			}
		}

		newerThan(version){
			let v = new Version(version);

			if (this.versionMajor > v.versionMajor) {
				return true;
			} else if (this.versionMajor === v.versionMajor && this.versionMinor > v.versionMinor) {
				return true;
			} else {
				return false;
			}
		}

		equalOrHigher(version){
			let v = new Version(version);

			if (this.versionMajor > v.versionMajor) {
				return true;
			} else if (this.versionMajor === v.versionMajor && this.versionMinor >= v.versionMinor) {
				return true;
			} else {
				return false;
			}
		}

		upTo(version){
			return !this.newerThan(version);
		}

	}

	class WorkerPool{
		constructor(){
			this.workers = {};
		}

		getWorker(url){
			if (!this.workers[url]){
				this.workers[url] = [];
			}

			if (this.workers[url].length === 0){
				let worker = new Worker(url);
				this.workers[url].push(worker);
			}

			let worker = this.workers[url].pop();

			return worker;
		}

		returnWorker(url, worker){
			this.workers[url].push(worker);
		}
	};

	//Potree.workerPool = new Potree.WorkerPool();

	function createPointcloudData(pointcloud) {

		let material = pointcloud.material;

		let ranges = [];
		
		for(let [name, value] of material.ranges){
			ranges.push({
				name: name,
				value: value,
			});
		}

		if(typeof material.elevationRange[0] === "number"){
			ranges.push({
				name: "elevationRange",
				value: material.elevationRange,
			});
		}
		if(typeof material.intensityRange[0] === "number"){
			ranges.push({
				name: "intensityRange",
				value: material.intensityRange,
			});
		}

		let pointSizeTypeName = Object.entries(Potree.PointSizeType).find(e => e[1] === material.pointSizeType)[0];

		let jsonMaterial = {
			activeAttributeName: material.activeAttributeName,
			ranges: ranges,
			size: material.size,
			minSize: material.minSize,
			pointSizeType: pointSizeTypeName,
			matcap: material.matcap,
		};

		const pcdata = {
			name: pointcloud.name,
			url: pointcloud.pcoGeometry.url,
			position: pointcloud.position.toArray(),
			rotation: pointcloud.rotation.toArray(),
			scale: pointcloud.scale.toArray(),
			material: jsonMaterial,
		};

		return pcdata;
	}

	function createProfileData(profile){
		const data = {
			uuid: profile.uuid,
			name: profile.name,
			points: profile.points.map(p => p.toArray()),
			height: profile.height,
			width: profile.width,
		};

		return data;
	}

	function createVolumeData(volume){
		const data = {
			uuid: volume.uuid,
			type: volume.constructor.name,
			name: volume.name,
			position: volume.position.toArray(),
			rotation: volume.rotation.toArray(),
			scale: volume.scale.toArray(),
			visible: volume.visible,
			clip: volume.clip,
		};

		return data;
	}

	function createCameraAnimationData(animation){

		const controlPoints = animation.controlPoints.map( cp => {
			const cpdata = {
				position: cp.position.toArray(),
				target: cp.target.toArray(),
			};

			return cpdata;
		});

		const data = {
			uuid: animation.uuid,
			name: animation.name,
			duration: animation.duration,
			t: animation.t,
			curveType: animation.curveType,
			visible: animation.visible,
			controlPoints: controlPoints,
		};

		return data;
	}

	function createMeasurementData(measurement){

		const data = {
			uuid: measurement.uuid,
			name: measurement.name,
			points: measurement.points.map(p => p.position.toArray()),
			showDistances: measurement.showDistances,
			showCoordinates: measurement.showCoordinates,
			showArea: measurement.showArea,
			closed: measurement.closed,
			showAngles: measurement.showAngles,
			showHeight: measurement.showHeight,
			showCircle: measurement.showCircle,
			showAzimuth: measurement.showAzimuth,
			showEdges: measurement.showEdges,
			color: measurement.color.toArray(),
		};

		return data;
	}

	function createOrientedImagesData(images){
		const data = {
			cameraParamsPath: images.cameraParamsPath,
			imageParamsPath: images.imageParamsPath,
		};

		return data;
	}

	function createGeopackageData(geopackage){
		const data = {
			path: geopackage.path,
		};

		return data;
	}

	function createAnnotationData(annotation){

		const data = {
			uuid: annotation.uuid,
			title: annotation.title.toString(),
			description: annotation.description,
			position: annotation.position.toArray(),
			offset: annotation.offset.toArray(),
			children: [],
		};

		if(annotation.cameraPosition){
			data.cameraPosition = annotation.cameraPosition.toArray();
		}

		if(annotation.cameraTarget){
			data.cameraTarget = annotation.cameraTarget.toArray();
		}

		if(typeof annotation.radius !== "undefined"){
			data.radius = annotation.radius;
		}

		return data;
	}

	function createAnnotationsData(viewer){
		
		const map = new Map();

		viewer.scene.annotations.traverseDescendants(a => {
			const aData = createAnnotationData(a);

			map.set(a, aData);
		});

		for(const [annotation, data] of map){
			for(const child of annotation.children){
				const childData = map.get(child);
				data.children.push(childData);
			}
		}

		const annotations = viewer.scene.annotations.children.map(a => map.get(a));

		return annotations;
	}

	function createSettingsData(viewer){
		return {
			pointBudget: viewer.getPointBudget(),
			fov: viewer.getFOV(),
			edlEnabled: viewer.getEDLEnabled(),
			edlRadius: viewer.getEDLRadius(),
			edlStrength: viewer.getEDLStrength(),
			background: viewer.getBackground(),
			minNodeSize: viewer.getMinNodeSize(),
			showBoundingBoxes: viewer.getShowBoundingBox(),
		};
	}

	function createSceneContentData(viewer){

		const data = [];

		const potreeObjects = [];

		viewer.scene.scene.traverse(node => {
			if(node.potree){
				potreeObjects.push(node);
			}
		});

		for(const object of potreeObjects){
			
			if(object.potree.file){
				const saveObject = {
					file: object.potree.file,
				};

				data.push(saveObject);
			}


		}


		return data;
	}

	function createViewData(viewer){
		const view = viewer.scene.view;

		const data = {
			position: view.position.toArray(),
			target: view.getPivot().toArray(),
		};

		return data;
	}

	function createClassificationData(viewer){
		const classifications = viewer.classifications;

		const data = classifications;

		return data;
	}

	function saveProject(viewer) {

		const scene = viewer.scene;

		const data = {
			type: "Potree",
			version: 1.7,
			settings: createSettingsData(viewer),
			view: createViewData(viewer),
			classification: createClassificationData(viewer),
			pointclouds: scene.pointclouds.map(createPointcloudData),
			measurements: scene.measurements.map(createMeasurementData),
			volumes: scene.volumes.map(createVolumeData),
			cameraAnimations: scene.cameraAnimations.map(createCameraAnimationData),
			profiles: scene.profiles.map(createProfileData),
			annotations: createAnnotationsData(viewer),
			orientedImages: scene.orientedImages.map(createOrientedImagesData),
			geopackages: scene.geopackages.map(createGeopackageData),
			// objects: createSceneContentData(viewer),
		};

		return data;
	}

	class ControlPoint{

		constructor(){
			this.position = new Vector3(0, 0, 0);
			this.target = new Vector3(0, 0, 0);
			this.positionHandle = null;
			this.targetHandle = null;
		}

	};



	class CameraAnimation extends EventDispatcher{

		constructor(viewer){
			super();
			
			this.viewer = viewer;

			this.selectedElement = null;

			this.controlPoints = [];

			this.uuid = MathUtils.generateUUID();

			this.node = new Object3D();
			this.node.name = "camera animation";
			this.viewer.scene.scene.add(this.node);

			this.frustum = this.createFrustum();
			this.node.add(this.frustum);

			this.name = "Camera Animation";
			this.duration = 5;
			this.t = 0;
			// "centripetal", "chordal", "catmullrom"
			this.curveType = "centripetal"; 
			this.visible = true;

			this.createUpdateHook();
			this.createPath();
		}

		static defaultFromView(viewer){
			const animation = new CameraAnimation(viewer);

			const camera = viewer.scene.getActiveCamera();
			const target = viewer.scene.view.getPivot();

			const cpCenter = new Vector3(
				0.3 * camera.position.x + 0.7 * target.x,
				0.3 * camera.position.y + 0.7 * target.y,
				0.3 * camera.position.z + 0.7 * target.z,
			);

			const targetCenter = new Vector3(
				0.05 * camera.position.x + 0.95 * target.x,
				0.05 * camera.position.y + 0.95 * target.y,
				0.05 * camera.position.z + 0.95 * target.z,
			);

			const r = camera.position.distanceTo(target) * 0.3;

			//const dir = target.clone().sub(camera.position).normalize();
			const angle = Utils.computeAzimuth(camera.position, target);

			const n = 5;
			for(let i = 0; i < n; i++){
				let u = 1.5 * Math.PI * (i / n) + angle;

				const dx = r * Math.cos(u);
				const dy = r * Math.sin(u);

				const cpPos = [
					cpCenter.x + dx,
					cpCenter.y + dy,
					cpCenter.z,
				];

				const targetPos = [
					targetCenter.x + dx * 0.1,
					targetCenter.y + dy * 0.1,
					targetCenter.z,
				];

				const cp = animation.createControlPoint();
				cp.position.set(...cpPos);
				cp.target.set(...targetPos);
			}

			return animation;
		}

		createUpdateHook(){
			const viewer = this.viewer;

			viewer.addEventListener("update", () => {

				const camera = viewer.scene.getActiveCamera();
				const {width, height} = viewer.renderer.getSize(new Vector2());

				this.node.visible = this.visible;

				for(const cp of this.controlPoints){
					
					{ // position
						const projected = cp.position.clone().project(camera);

						const visible = this.visible && (projected.z < 1 && projected.z > -1);

						if(visible){
							const x = width * (projected.x * 0.5 + 0.5);
							const y = height - height * (projected.y * 0.5 + 0.5);

							cp.positionHandle.svg.style.left = x - cp.positionHandle.svg.clientWidth / 2;
							cp.positionHandle.svg.style.top = y - cp.positionHandle.svg.clientHeight / 2;
							cp.positionHandle.svg.style.display = "";
						}else {
							cp.positionHandle.svg.style.display = "none";
						}
					}

					{ // target
						const projected = cp.target.clone().project(camera);

						const visible = this.visible && (projected.z < 1 && projected.z > -1);

						if(visible){
							const x = width * (projected.x * 0.5 + 0.5);
							const y = height - height * (projected.y * 0.5 + 0.5);

							cp.targetHandle.svg.style.left = x - cp.targetHandle.svg.clientWidth / 2;
							cp.targetHandle.svg.style.top = y - cp.targetHandle.svg.clientHeight / 2;
							cp.targetHandle.svg.style.display = "";
						}else {
							cp.targetHandle.svg.style.display = "none";
						}
					}

				}

				this.line.material.resolution.set(width, height);

				this.updatePath();

				{ // frustum
					const frame = this.at(this.t);
					const frustum = this.frustum;

					frustum.position.copy(frame.position);
					frustum.lookAt(...frame.target.toArray());
					frustum.scale.set(20, 20, 20);

					frustum.material.resolution.set(width, height);
				}

			});
		}

		createControlPoint(index){

			if(index === undefined){
				index = this.controlPoints.length;
			}

			const cp = new ControlPoint();


			if(this.controlPoints.length >= 2 && index === 0){
				const cp1 = this.controlPoints[0];
				const cp2 = this.controlPoints[1];

				const dir = cp1.position.clone().sub(cp2.position).multiplyScalar(0.5);
				cp.position.copy(cp1.position).add(dir);

				const tDir = cp1.target.clone().sub(cp2.target).multiplyScalar(0.5);
				cp.target.copy(cp1.target).add(tDir);
			}else if(this.controlPoints.length >= 2 && index === this.controlPoints.length){
				const cp1 = this.controlPoints[this.controlPoints.length - 2];
				const cp2 = this.controlPoints[this.controlPoints.length - 1];

				const dir = cp2.position.clone().sub(cp1.position).multiplyScalar(0.5);
				cp.position.copy(cp1.position).add(dir);

				const tDir = cp2.target.clone().sub(cp1.target).multiplyScalar(0.5);
				cp.target.copy(cp2.target).add(tDir);
			}else if(this.controlPoints.length >= 2){
				const cp1 = this.controlPoints[index - 1];
				const cp2 = this.controlPoints[index];

				cp.position.copy(cp1.position.clone().add(cp2.position).multiplyScalar(0.5));
				cp.target.copy(cp1.target.clone().add(cp2.target).multiplyScalar(0.5));
			}

			// cp.position.copy(viewer.scene.view.position);
			// cp.target.copy(viewer.scene.view.getPivot());

			cp.positionHandle = this.createHandle(cp.position);
			cp.targetHandle = this.createHandle(cp.target);

			this.controlPoints.splice(index, 0, cp);

			this.dispatchEvent({
				type: "controlpoint_added",
				controlpoint: cp,
			});

			return cp;
		}

		removeControlPoint(cp){
			this.controlPoints = this.controlPoints.filter(_cp => _cp !== cp);

			this.dispatchEvent({
				type: "controlpoint_removed",
				controlpoint: cp,
			});

			cp.positionHandle.svg.remove();
			cp.targetHandle.svg.remove();

			// TODO destroy cp
		}

		createPath(){

			{ // position
				const geometry = new LineGeometry();

				let material = new LineMaterial({ 
					color: 0x00ff00, 
					dashSize: 5, 
					gapSize: 2,
					linewidth: 2, 
					resolution:  new Vector2(1000, 1000),
				});

				const line = new Line2(geometry, material);

				this.line = line;
				this.node.add(line);
			}

			{ // target
				const geometry = new LineGeometry();

				let material = new LineMaterial({ 
					color: 0x0000ff, 
					dashSize: 5, 
					gapSize: 2,
					linewidth: 2, 
					resolution:  new Vector2(1000, 1000),
				});

				const line = new Line2(geometry, material);

				this.targetLine = line;
				this.node.add(line);
			}
		}

		createFrustum(){

			const f = 0.3;

			const positions = [
				 0,  0,  0,
				-f, -f, +1,

				 0,  0,  0,
				 f, -f, +1,

				 0,  0,  0,
				 f,  f, +1,

				 0,  0,  0,
				-f,  f, +1,

				-f, -f, +1,
				 f, -f, +1,

				 f, -f, +1,
				 f,  f, +1,

				 f,  f, +1,
				-f,  f, +1,

				-f,  f, +1,
				-f, -f, +1,
			];

			const geometry = new LineGeometry();

			geometry.setPositions(positions);
			geometry.verticesNeedUpdate = true;
			geometry.computeBoundingSphere();

			let material = new LineMaterial({ 
				color: 0xff0000, 
				linewidth: 2, 
				resolution:  new Vector2(1000, 1000),
			});

			const line = new Line2(geometry, material);
			line.computeLineDistances();
			
			return line;
		}

		updatePath(){

			{ // positions
				const positions = this.controlPoints.map(cp => cp.position);
				const first = positions[0];

				const curve = new CatmullRomCurve3(positions);
				curve.curveType = this.curveType;

				const n = 100;

				const curvePositions = [];
				for(let k = 0; k <= n; k++){
					const t = k / n;

					const position = curve.getPoint(t).sub(first);

					curvePositions.push(position.x, position.y, position.z);
				}

				this.line.geometry.setPositions(curvePositions);
				this.line.geometry.verticesNeedUpdate = true;
				this.line.geometry.computeBoundingSphere();
				this.line.position.copy(first);
				this.line.computeLineDistances();

				this.cameraCurve = curve;
			}

			{ // targets
				const positions = this.controlPoints.map(cp => cp.target);
				const first = positions[0];

				const curve = new CatmullRomCurve3(positions);
				curve.curveType = this.curveType;

				const n = 100;

				const curvePositions = [];
				for(let k = 0; k <= n; k++){
					const t = k / n;

					const position = curve.getPoint(t).sub(first);

					curvePositions.push(position.x, position.y, position.z);
				}

				this.targetLine.geometry.setPositions(curvePositions);
				this.targetLine.geometry.verticesNeedUpdate = true;
				this.targetLine.geometry.computeBoundingSphere();
				this.targetLine.position.copy(first);
				this.targetLine.computeLineDistances();

				this.targetCurve = curve;
			}
		}

		at(t){
			
			if(t > 1){
				t = 1;
			}else if(t < 0){
				t = 0;
			}

			const camPos = this.cameraCurve.getPointAt(t);
			const target = this.targetCurve.getPointAt(t);

			const frame = {
				position: camPos,
				target: target,
			};

			return frame;
		}

		set(t){
			this.t = t;
		}

		createHandle(vector){
			
			const svgns = "http://www.w3.org/2000/svg";
			const svg = document.createElementNS(svgns, "svg");

			svg.setAttribute("width", "2em");
			svg.setAttribute("height", "2em");
			svg.setAttribute("position", "absolute");

			svg.style.left = "50px";
			svg.style.top = "50px";
			svg.style.position = "absolute";
			svg.style.zIndex = "10000";

			const circle = document.createElementNS(svgns, 'circle');
			circle.setAttributeNS(null, 'cx', "1em");
			circle.setAttributeNS(null, 'cy', "1em");
			circle.setAttributeNS(null, 'r', "0.5em");
			circle.setAttributeNS(null, 'style', 'fill: red; stroke: black; stroke-width: 0.2em;' );
			svg.appendChild(circle);


			const element = this.viewer.renderer.domElement.parentElement;
			element.appendChild(svg);


			const startDrag = (evt) => {
				this.selectedElement = svg;

				document.addEventListener("mousemove", drag);
			};

			const endDrag = (evt) => {
				this.selectedElement = null;

				document.removeEventListener("mousemove", drag);
			};

			const drag = (evt) => {
				if (this.selectedElement) {
					evt.preventDefault();

					const rect = viewer.renderer.domElement.getBoundingClientRect();

					const x = evt.clientX - rect.x;
					const y = evt.clientY - rect.y;

					const {width, height} = this.viewer.renderer.getSize(new Vector2());
					const camera = this.viewer.scene.getActiveCamera();
					//const cp = this.controlPoints.find(cp => cp.handle.svg === svg);
					const projected = vector.clone().project(camera);

					projected.x = ((x / width) - 0.5) / 0.5;
					projected.y = (-(y - height) / height - 0.5) / 0.5;

					const unprojected = projected.clone().unproject(camera);
					vector.set(unprojected.x, unprojected.y, unprojected.z);


				}
			};

			svg.addEventListener('mousedown', startDrag);
			svg.addEventListener('mouseup', endDrag);

			const handle = {
				svg: svg,
			};

			return handle;
		}

		setVisible(visible){
			this.node.visible = visible;

			const display = visible ? "" : "none";

			for(const cp of this.controlPoints){
				cp.positionHandle.svg.style.display = display;
				cp.targetHandle.svg.style.display = display;
			}

			this.visible = visible;
		}

		setDuration(duration){
			this.duration = duration;
		}

		getDuration(duration){
			return this.duration;
		}

		play(){

			const tStart = performance.now();
			const duration = this.duration;

			const originalyVisible = this.visible;
			this.setVisible(false);

			const onUpdate = (delta) => {

				let tNow = performance.now();
				let elapsed = (tNow - tStart) / 1000;
				let t = elapsed / duration;

				this.set(t);

				const frame = this.at(t);

				viewer.scene.view.position.copy(frame.position);
				viewer.scene.view.lookAt(frame.target);


				if(t > 1){
					this.setVisible(originalyVisible);

					this.viewer.removeEventListener("update", onUpdate);
				}

			};

			this.viewer.addEventListener("update", onUpdate);

		}

	}

	function loadPointCloud(viewer, data){

		let loadMaterial = (target) => {

			if(data.material){

				if(data.material.activeAttributeName != null){
					target.activeAttributeName = data.material.activeAttributeName;
				}

				if(data.material.ranges != null){
					for(let range of data.material.ranges){

						if(range.name === "elevationRange"){
							target.elevationRange = range.value;
						}else if(range.name === "intensityRange"){
							target.intensityRange = range.value;
						}else {
							target.setRange(range.name, range.value);
						}

					}
				}

				if(data.material.size != null){
					target.size = data.material.size;
				}

				if(data.material.minSize != null){
					target.minSize = data.material.minSize;
				}

				if(data.material.pointSizeType != null){
					target.pointSizeType = PointSizeType[data.material.pointSizeType];
				}

				if(data.material.matcap != null){
					target.matcap = data.material.matcap;
				}

			}else if(data.activeAttributeName != null){
				target.activeAttributeName = data.activeAttributeName;
			}else {
				// no material data
			}

		};

		const promise = new Promise((resolve) => {

			const names = viewer.scene.pointclouds.map(p => p.name);
			const alreadyExists = names.includes(data.name);

			if(alreadyExists){
				resolve();
				return;
			}

			Potree.loadPointCloud(data.url, data.name, (e) => {
				const {pointcloud} = e;

				pointcloud.position.set(...data.position);
				pointcloud.rotation.set(...data.rotation);
				pointcloud.scale.set(...data.scale);

				loadMaterial(pointcloud.material);

				viewer.scene.addPointCloud(pointcloud);

				resolve(pointcloud);
			});
		});

		return promise;
	}

	function loadMeasurement(viewer, data){

		const duplicate = viewer.scene.measurements.find(measure => measure.uuid === data.uuid);
		if(duplicate){
			return;
		}

		const measure = new Measure();

		measure.uuid = data.uuid;
		measure.name = data.name;
		measure.showDistances = data.showDistances;
		measure.showCoordinates = data.showCoordinates;
		measure.showArea = data.showArea;
		measure.closed = data.closed;
		measure.showAngles = data.showAngles;
		measure.showHeight = data.showHeight;
		measure.showCircle = data.showCircle;
		measure.showAzimuth = data.showAzimuth;
		measure.showEdges = data.showEdges;
		// color

		for(const point of data.points){
			const pos = new Vector3(...point);
			measure.addMarker(pos);
		}

		viewer.scene.addMeasurement(measure);

	}

	function loadVolume(viewer, data){

		const duplicate = viewer.scene.volumes.find(volume => volume.uuid === data.uuid);
		if(duplicate){
			return;
		}

		let volume = new Potree[data.type];

		volume.uuid = data.uuid;
		volume.name = data.name;
		volume.position.set(...data.position);
		volume.rotation.set(...data.rotation);
		volume.scale.set(...data.scale);
		volume.visible = data.visible;
		volume.clip = data.clip;

		viewer.scene.addVolume(volume);
	}

	function loadCameraAnimation(viewer, data){

		const duplicate = viewer.scene.cameraAnimations.find(a => a.uuid === data.uuid);
		if(duplicate){
			return;
		}

		const animation = new CameraAnimation(viewer);

		animation.uuid = data.uuid;
		animation.name = data.name;
		animation.duration = data.duration;
		animation.t = data.t;
		animation.curveType = data.curveType;
		animation.visible = data.visible;
		animation.controlPoints = [];

		for(const cpdata of data.controlPoints){
			const cp = animation.createControlPoint();

			cp.position.set(...cpdata.position);
			cp.target.set(...cpdata.target);
		}

		viewer.scene.addCameraAnimation(animation);
	}

	function loadOrientedImages(viewer, images){

		const {cameraParamsPath, imageParamsPath} = images;

		const duplicate = viewer.scene.orientedImages.find(i => i.imageParamsPath === imageParamsPath);
		if(duplicate){
			return;
		}

		Potree.OrientedImageLoader.load(cameraParamsPath, imageParamsPath, viewer).then( images => {
			viewer.scene.addOrientedImages(images);
		});

	}

	function loadGeopackage(viewer, geopackage){

		const path = geopackage.path;

		const duplicate = viewer.scene.geopackages.find(i => i.path === path);
		if(duplicate){
			return;
		}

		const projection = viewer.getProjection();

		proj4.defs("WGS84", "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs");
		proj4.defs("pointcloud", projection);
		const transform = proj4("WGS84", "pointcloud");
		const params = {
			transform: transform,
		};

		Potree.GeoPackageLoader.loadUrl(path, params).then(data => {
			viewer.scene.addGeopackage(data);
		});
		

	}

	function loadSettings(viewer, data){
		if(!data){
			return;
		}

		viewer.setPointBudget(data.pointBudget);
		viewer.setFOV(data.fov);
		viewer.setEDLEnabled(data.edlEnabled);
		viewer.setEDLRadius(data.edlRadius);
		viewer.setEDLStrength(data.edlStrength);
		viewer.setBackground(data.background);
		viewer.setMinNodeSize(data.minNodeSize);
		viewer.setShowBoundingBox(data.showBoundingBoxes);
	}

	function loadView(viewer, view){
		viewer.scene.view.position.set(...view.position);
		viewer.scene.view.lookAt(...view.target);
	}

	function loadAnnotationItem(item){

		const annotation = new Annotation({
			position: item.position,
			title: item.title,
			cameraPosition: item.cameraPosition,
			cameraTarget: item.cameraTarget,
		});


		annotation.description = item.description;
		annotation.uuid = item.uuid;

		if(item.offset){
			annotation.offset.set(...item.offset);
		}

		return annotation;
	}

	function loadAnnotations(viewer, data){

		if(!data){
			return;
		}

		const findDuplicate = (item) => {

			let duplicate = null;

			viewer.scene.annotations.traverse( a => {
				if(a.uuid === item.uuid){
					duplicate = a;
				}
			});

			return duplicate;
		};

		const traverse = (item, parent) => {

			const duplicate = findDuplicate(item);
			if(duplicate){
				return;
			}

			const annotation = loadAnnotationItem(item);

			for(const childItem of item.children){
				traverse(childItem, annotation);
			}

			parent.add(annotation);

		};

		for(const item of data){
			traverse(item, viewer.scene.annotations);
		}

	}

	function loadProfile(viewer, data){
		
		const {name, points} = data;

		const duplicate = viewer.scene.profiles.find(profile => profile.uuid === data.uuid);
		if(duplicate){
			return;
		}

		let profile = new Potree.Profile();
		profile.name = name;
		profile.uuid = data.uuid;

		profile.setWidth(data.width);

		for(const point of points){
			profile.addMarker(new Vector3(...point));
		}
		
		viewer.scene.addProfile(profile);
	}

	function loadClassification(viewer, data){
		if(!data){
			return;
		}

		const classifications = data;

		viewer.setClassifications(classifications);
	}

	async function loadProject(viewer, data){

		if(data.type !== "Potree"){
			console.error("not a valid Potree project");
			return;
		}

		loadSettings(viewer, data.settings);

		loadView(viewer, data.view);

		const pointcloudPromises = [];
		for(const pointcloud of data.pointclouds){
			const promise = loadPointCloud(viewer, pointcloud);
			pointcloudPromises.push(promise);
		}

		for(const measure of data.measurements){
			loadMeasurement(viewer, measure);
		}

		for(const volume of data.volumes){
			loadVolume(viewer, volume);
		}

		for(const animation of data.cameraAnimations){
			loadCameraAnimation(viewer, animation);
		}

		for(const profile of data.profiles){
			loadProfile(viewer, profile);
		}

		if(data.orientedImages){
			for(const images of data.orientedImages){
				loadOrientedImages(viewer, images);
			}
		}

		loadAnnotations(viewer, data.annotations);

		loadClassification(viewer, data.classification);

		// need to load at least one point cloud that defines the scene projection,
		// before we can load stuff in other projections such as geopackages
		//await Promise.any(pointcloudPromises); // (not yet supported)
		Utils.waitAny(pointcloudPromises).then( () => {
			if(data.geopackages){
				for(const geopackage of data.geopackages){
					loadGeopackage(viewer, geopackage);
				}
			}
		});

		await Promise.all(pointcloudPromises);
	}

	//
	// Algorithm by Christian Boucheny
	// shader code taken and adapted from CloudCompare
	//
	// see
	// https://github.com/cloudcompare/trunk/tree/master/plugins/qEDL/shaders/EDL
	// http://www.kitware.com/source/home/post/9
	// https://tel.archives-ouvertes.fr/tel-00438464/document p. 115+ (french)

	class EyeDomeLightingMaterial extends RawShaderMaterial{

		constructor(parameters = {}){
			super();

			let uniforms = {
				screenWidth:    { type: 'f', 	value: 0 },
				screenHeight:   { type: 'f', 	value: 0 },
				edlStrength:    { type: 'f', 	value: 1.0 },
				uNear:          { type: 'f', 	value: 1.0 },
				uFar:           { type: 'f', 	value: 1.0 },
				radius:         { type: 'f', 	value: 1.0 },
				neighbours:     { type: '2fv', 	value: [] },
				depthMap:       { type: 't', 	value: null },
				uEDLColor:      { type: 't', 	value: null },
				uEDLDepth:      { type: 't', 	value: null },
				opacity:        { type: 'f',	value: 1.0 },
				uProj:          { type: "Matrix4fv", value: [] },
			};

			this.setValues({
				uniforms: uniforms,
				vertexShader: this.getDefines() + Shaders['edl.vs'],
				fragmentShader: this.getDefines() + Shaders['edl.fs'],
				lights: false
			});

			this.neighbourCount = 8;
		}

		getDefines() {
			let defines = '';

			defines += '#define NEIGHBOUR_COUNT ' + this.neighbourCount + '\n';

			return defines;
		}

		updateShaderSource() {

			let vs = this.getDefines() + Shaders['edl.vs'];
			let fs = this.getDefines() + Shaders['edl.fs'];

			this.setValues({
				vertexShader: vs,
				fragmentShader: fs
			});

			this.uniforms.neighbours.value = this.neighbours;

			this.needsUpdate = true;
		}

		get neighbourCount(){
			return this._neighbourCount;
		}

		set neighbourCount(value){
			if (this._neighbourCount !== value) {
				this._neighbourCount = value;
				this.neighbours = new Float32Array(this._neighbourCount * 2);
				for (let c = 0; c < this._neighbourCount; c++) {
					this.neighbours[2 * c + 0] = Math.cos(2 * c * Math.PI / this._neighbourCount);
					this.neighbours[2 * c + 1] = Math.sin(2 * c * Math.PI / this._neighbourCount);
				}

				this.updateShaderSource();
			}
		}

		
	}

	class NormalizationEDLMaterial extends RawShaderMaterial{

		constructor(parameters = {}){
			super();

			let uniforms = {
				screenWidth:    { type: 'f',   value: 0 },
				screenHeight:   { type: 'f',   value: 0 },
				edlStrength:    { type: 'f',   value: 1.0 },
				radius:         { type: 'f',   value: 1.0 },
				neighbours:     { type: '2fv', value: [] },
				uEDLMap:        { type: 't',   value: null },
				uDepthMap:      { type: 't',   value: null },
				uWeightMap:     { type: 't',   value: null },
			};

			this.setValues({
				uniforms: uniforms,
				vertexShader: this.getDefines() + Shaders['normalize.vs'],
				fragmentShader: this.getDefines() + Shaders['normalize_and_edl.fs'],
			});

			this.neighbourCount = 8;
		}

		getDefines() {
			let defines = '';

			defines += '#define NEIGHBOUR_COUNT ' + this.neighbourCount + '\n';

			return defines;
		}

		updateShaderSource() {

			let vs = this.getDefines() + Shaders['normalize.vs'];
			let fs = this.getDefines() + Shaders['normalize_and_edl.fs'];

			this.setValues({
				vertexShader: vs,
				fragmentShader: fs
			});

			this.uniforms.neighbours.value = this.neighbours;

			this.needsUpdate = true;
		}

		get neighbourCount(){
			return this._neighbourCount;
		}

		set neighbourCount(value){
			if (this._neighbourCount !== value) {
				this._neighbourCount = value;
				this.neighbours = new Float32Array(this._neighbourCount * 2);
				for (let c = 0; c < this._neighbourCount; c++) {
					this.neighbours[2 * c + 0] = Math.cos(2 * c * Math.PI / this._neighbourCount);
					this.neighbours[2 * c + 1] = Math.sin(2 * c * Math.PI / this._neighbourCount);
				}

				this.updateShaderSource();
			}
		}
		
	}

	class NormalizationMaterial extends RawShaderMaterial{

		constructor(parameters = {}){
			super();

			let uniforms = {
				uDepthMap:		{ type: 't', value: null },
				uWeightMap:		{ type: 't', value: null },
			};

			this.setValues({
				uniforms: uniforms,
				vertexShader: this.getDefines() + Shaders['normalize.vs'],
				fragmentShader: this.getDefines() + Shaders['normalize.fs'],
			});
		}

		getDefines() {
			let defines = '';

			return defines;
		}

		updateShaderSource() {

			let vs = this.getDefines() + Shaders['normalize.vs'];
			let fs = this.getDefines() + Shaders['normalize.fs'];

			this.setValues({
				vertexShader: vs,
				fragmentShader: fs
			});

			this.needsUpdate = true;
		}

	}

	/**
	 * laslaz code taken and adapted from plas.io js-laslaz
	 *	http://plas.io/
	 *  https://github.com/verma/plasio
	 *
	 * Thanks to Uday Verma and Howard Butler
	 *
	 */

	class LasLazLoader {

		constructor (version, extension) {
			if (typeof (version) === 'string') {
				this.version = new Version(version);
			} else {
				this.version = version;
			}

			this.extension = extension;
		}

		static progressCB () {

		}

		load (node) {
			if (node.loaded) {
				return;
			}

			let url = node.getURL();

			if (this.version.equalOrHigher('1.4')) {
				url += `.${this.extension}`;
			}

			let xhr = XHRFactory.createXMLHttpRequest();
			xhr.open('GET', url, true);
			xhr.responseType = 'arraybuffer';
			xhr.overrideMimeType('text/plain; charset=x-user-defined');
			xhr.onreadystatechange = () => {
				if (xhr.readyState === 4) {
					if (xhr.status === 200 || xhr.status === 0) {
						let buffer = xhr.response;
						this.parse(node, buffer);
					} else {
						console.log('Failed to load file! HTTP status: ' + xhr.status + ', file: ' + url);
					}
				}
			};

			xhr.send(null);
		}

		async parse(node, buffer){
			let lf = new LASFile(buffer);
			let handler = new LasLazBatcher(node);

			try{
				 await lf.open();
				 lf.isOpen = true;
			}catch(e){
				console.log("failed to open file. :(");

				return;
			}

			let header = await lf.getHeader();

			let skip = 1;
			let totalRead = 0;
			let totalToRead = (skip <= 1 ? header.pointsCount : header.pointsCount / skip);

			let hasMoreData = true;

			while(hasMoreData){
				let data = await lf.readData(1000 * 1000, 0, skip);

				handler.push(new LASDecoder(data.buffer,
					header.pointsFormatId,
					header.pointsStructSize,
					data.count,
					header.scale,
					header.offset,
					header.mins, header.maxs));

				totalRead += data.count;
				LasLazLoader.progressCB(totalRead / totalToRead);

				hasMoreData = data.hasMoreData;
			}

			header.totalRead = totalRead;
			header.versionAsString = lf.versionAsString;
			header.isCompressed = lf.isCompressed;

			LasLazLoader.progressCB(1);

			try{
				await lf.close();

				lf.isOpen = false;
			}catch(e){
				console.error("failed to close las/laz file!!!");
				
				throw e;
			}
		}

		handle (node, url) {

		}
	};

	class LasLazBatcher{

		constructor (node) {
			this.node = node;
		}

		push (lasBuffer) {
			const workerPath = Potree.scriptPath + '/workers/LASDecoderWorker.js';
			const worker = Potree.workerPool.getWorker(workerPath);
			const node = this.node;
			const pointAttributes = node.pcoGeometry.pointAttributes;

			worker.onmessage = (e) => {
				let geometry = new BufferGeometry();
				let numPoints = lasBuffer.pointsCount;

				let positions = new Float32Array(e.data.position);
				let colors = new Uint8Array(e.data.color);
				let intensities = new Float32Array(e.data.intensity);
				let classifications = new Uint8Array(e.data.classification);
				let returnNumbers = new Uint8Array(e.data.returnNumber);
				let numberOfReturns = new Uint8Array(e.data.numberOfReturns);
				let pointSourceIDs = new Uint16Array(e.data.pointSourceID);
				let indices = new Uint8Array(e.data.indices);

				geometry.setAttribute('position', new BufferAttribute(positions, 3));
				geometry.setAttribute('color', new BufferAttribute(colors, 4, true));
				geometry.setAttribute('intensity', new BufferAttribute(intensities, 1));
				geometry.setAttribute('classification', new BufferAttribute(classifications, 1));
				geometry.setAttribute('return number', new BufferAttribute(returnNumbers, 1));
				geometry.setAttribute('number of returns', new BufferAttribute(numberOfReturns, 1));
				geometry.setAttribute('source id', new BufferAttribute(pointSourceIDs, 1));
				geometry.setAttribute('indices', new BufferAttribute(indices, 4));
				geometry.attributes.indices.normalized = true;

				for(const key in e.data.ranges){
					const range = e.data.ranges[key];

					const attribute = pointAttributes.attributes.find(a => a.name === key);
					attribute.range[0] = Math.min(attribute.range[0], range[0]);
					attribute.range[1] = Math.max(attribute.range[1], range[1]);
				}

				let tightBoundingBox = new Box3(
					new Vector3().fromArray(e.data.tightBoundingBox.min),
					new Vector3().fromArray(e.data.tightBoundingBox.max)
				);

				geometry.boundingBox = this.node.boundingBox;
				this.node.tightBoundingBox = tightBoundingBox;

				this.node.geometry = geometry;
				this.node.numPoints = numPoints;
				this.node.loaded = true;
				this.node.loading = false;
				Potree.numNodesLoading--;
				this.node.mean = new Vector3(...e.data.mean);

				Potree.workerPool.returnWorker(workerPath, worker);
			};

			let message = {
				buffer: lasBuffer.arrayb,
				numPoints: lasBuffer.pointsCount,
				pointSize: lasBuffer.pointSize,
				pointFormatID: 2,
				scale: lasBuffer.scale,
				offset: lasBuffer.offset,
				mins: lasBuffer.mins,
				maxs: lasBuffer.maxs
			};
			worker.postMessage(message, [message.buffer]);
		};
	}

	class BinaryLoader{

		constructor(version, boundingBox, scale){
			if (typeof (version) === 'string') {
				this.version = new Version(version);
			} else {
				this.version = version;
			}

			this.boundingBox = boundingBox;
			this.scale = scale;
		}

		load(node){
			if (node.loaded) {
				return;
			}

			let url = node.getURL();

			if (this.version.equalOrHigher('1.4')) {
				url += '.bin';
			}

			let xhr = XHRFactory.createXMLHttpRequest();
			xhr.open('GET', url, true);
			xhr.responseType = 'arraybuffer';
			xhr.overrideMimeType('text/plain; charset=x-user-defined');
			xhr.onreadystatechange = () => {
				if (xhr.readyState === 4) {
					if((xhr.status === 200 || xhr.status === 0) &&  xhr.response !== null){
						let buffer = xhr.response;
						this.parse(node, buffer);
					} else {
						//console.error(`Failed to load file! HTTP status: ${xhr.status}, file: ${url}`);
						throw new Error(`Failed to load file! HTTP status: ${xhr.status}, file: ${url}`);
					}
				}
			};
			
			try {
				xhr.send(null);
			} catch (e) {
				console.log('fehler beim laden der punktwolke: ' + e);
			}
		};

		parse(node, buffer){
			let pointAttributes = node.pcoGeometry.pointAttributes;
			let numPoints = buffer.byteLength / node.pcoGeometry.pointAttributes.byteSize;

			if (this.version.upTo('1.5')) {
				node.numPoints = numPoints;
			}

			let workerPath = Potree.scriptPath + '/workers/BinaryDecoderWorker.js';
			let worker = Potree.workerPool.getWorker(workerPath);

			worker.onmessage = function (e) {

				let data = e.data;
				let buffers = data.attributeBuffers;
				let tightBoundingBox = new Box3(
					new Vector3().fromArray(data.tightBoundingBox.min),
					new Vector3().fromArray(data.tightBoundingBox.max)
				);

				Potree.workerPool.returnWorker(workerPath, worker);

				let geometry = new BufferGeometry();

				for(let property in buffers){
					let buffer = buffers[property].buffer;
					let batchAttribute = buffers[property].attribute;

					if (property === "POSITION_CARTESIAN") {
						geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffer), 3));
					} else if (property === "rgba") {
						geometry.setAttribute("rgba", new BufferAttribute(new Uint8Array(buffer), 4, true));
					} else if (property === "NORMAL_SPHEREMAPPED") {
						geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3));
					} else if (property === "NORMAL_OCT16") {
						geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3));
					} else if (property === "NORMAL") {
						geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3));
					} else if (property === "INDICES") {
						let bufferAttribute = new BufferAttribute(new Uint8Array(buffer), 4);
						bufferAttribute.normalized = true;
						geometry.setAttribute('indices', bufferAttribute);
					} else if (property === "SPACING") {
						let bufferAttribute = new BufferAttribute(new Float32Array(buffer), 1);
						geometry.setAttribute('spacing', bufferAttribute);
					} else {
						const bufferAttribute = new BufferAttribute(new Float32Array(buffer), 1);

						bufferAttribute.potree = {
							offset: buffers[property].offset,
							scale: buffers[property].scale,
							preciseBuffer: buffers[property].preciseBuffer,
							range: batchAttribute.range,
						};

						geometry.setAttribute(property, bufferAttribute);

						const attribute = pointAttributes.attributes.find(a => a.name === batchAttribute.name);
						attribute.range[0] = Math.min(attribute.range[0], batchAttribute.range[0]);
						attribute.range[1] = Math.max(attribute.range[1], batchAttribute.range[1]);

						if(node.getLevel() === 0){
							attribute.initialRange = batchAttribute.range;
						}

					}
				}

				tightBoundingBox.max.sub(tightBoundingBox.min);
				tightBoundingBox.min.set(0, 0, 0);

				let numPoints = e.data.buffer.byteLength / pointAttributes.byteSize;
				
				node.numPoints = numPoints;
				node.geometry = geometry;
				node.mean = new Vector3(...data.mean);
				node.tightBoundingBox = tightBoundingBox;
				node.loaded = true;
				node.loading = false;
				node.estimatedSpacing = data.estimatedSpacing;
				Potree.numNodesLoading--;
			};

			let message = {
				buffer: buffer,
				pointAttributes: pointAttributes,
				version: this.version.version,
				min: [ node.boundingBox.min.x, node.boundingBox.min.y, node.boundingBox.min.z ],
				offset: [node.pcoGeometry.offset.x, node.pcoGeometry.offset.y, node.pcoGeometry.offset.z],
				scale: this.scale,
				spacing: node.spacing,
				hasChildren: node.hasChildren,
				name: node.name
			};
			worker.postMessage(message, [message.buffer]);
		};

		
	}

	function parseAttributes(cloudjs){

		let version = new Version(cloudjs.version);

		const replacements = {
			"COLOR_PACKED": "rgba",
			"RGBA": "rgba",
			"INTENSITY": "intensity",
			"CLASSIFICATION": "classification",
			"GPS_TIME": "gps-time",
		};

		const replaceOldNames = (old) => {
			if(replacements[old]){
				return replacements[old];
			}else {
				return old;
			}
		};

		const pointAttributes = [];
		if(version.upTo('1.7')){
			
			for(let attributeName of cloudjs.pointAttributes){
				const oldAttribute = PointAttribute[attributeName];

				const attribute = {
					name: oldAttribute.name,
					size: oldAttribute.byteSize,
					elements: oldAttribute.numElements,
					elementSize: oldAttribute.byteSize / oldAttribute.numElements,
					type: oldAttribute.type.name,
					description: "",
				};

				pointAttributes.push(attribute);
			}

		}else {
			pointAttributes.push(...cloudjs.pointAttributes);
		}


		{
			const attributes = new PointAttributes();

			const typeConversion = {
				int8:   PointAttributeTypes.DATA_TYPE_INT8,
				int16:  PointAttributeTypes.DATA_TYPE_INT16,
				int32:  PointAttributeTypes.DATA_TYPE_INT32,
				int64:  PointAttributeTypes.DATA_TYPE_INT64,
				uint8:  PointAttributeTypes.DATA_TYPE_UINT8,
				uint16: PointAttributeTypes.DATA_TYPE_UINT16,
				uint32: PointAttributeTypes.DATA_TYPE_UINT32,
				uint64: PointAttributeTypes.DATA_TYPE_UINT64,
				double: PointAttributeTypes.DATA_TYPE_DOUBLE,
				float:  PointAttributeTypes.DATA_TYPE_FLOAT,
			};

			for(const jsAttribute of pointAttributes){
				const name = replaceOldNames(jsAttribute.name);
				const type = typeConversion[jsAttribute.type];
				const numElements = jsAttribute.elements;
				const description = jsAttribute.description;

				const attribute = new PointAttribute(name, type, numElements);

				attributes.add(attribute);
			}

			{
				// check if it has normals
				let hasNormals = 
					pointAttributes.find(a => a.name === "NormalX") !== undefined &&
					pointAttributes.find(a => a.name === "NormalY") !== undefined &&
					pointAttributes.find(a => a.name === "NormalZ") !== undefined;

				if(hasNormals){
					let vector = {
						name: "NORMAL",
						attributes: ["NormalX", "NormalY", "NormalZ"],
					};
					attributes.addVector(vector);
				}
			}

			return attributes;
		}

	}

	function lasLazAttributes(fMno){
		const attributes = new PointAttributes();

		attributes.add(PointAttribute.POSITION_CARTESIAN);
		attributes.add(new PointAttribute("rgba", PointAttributeTypes.DATA_TYPE_UINT8, 4));
		attributes.add(new PointAttribute("intensity", PointAttributeTypes.DATA_TYPE_UINT16, 1));
		attributes.add(new PointAttribute("classification", PointAttributeTypes.DATA_TYPE_UINT8, 1));
		attributes.add(new PointAttribute("gps-time", PointAttributeTypes.DATA_TYPE_DOUBLE, 1));
		attributes.add(new PointAttribute("number of returns", PointAttributeTypes.DATA_TYPE_UINT8, 1));
		attributes.add(new PointAttribute("return number", PointAttributeTypes.DATA_TYPE_UINT8, 1));
		attributes.add(new PointAttribute("source id", PointAttributeTypes.DATA_TYPE_UINT16, 1));
		//attributes.add(new PointAttribute("pointSourceID", PointAttributeTypes.DATA_TYPE_INT8, 4));


		return attributes;
	}

	class POCLoader {

		static load(url, callback){
			try {
				let pco = new PointCloudOctreeGeometry();
				pco.url = url;
				let xhr = XHRFactory.createXMLHttpRequest();
				xhr.open('GET', url, true);

				xhr.onreadystatechange = function () {
					if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 0)) {
						let fMno = JSON.parse(xhr.responseText);

						let version = new Version(fMno.version);

						// assume octreeDir is absolute if it starts with http
						if (fMno.octreeDir.indexOf('http') === 0) {
							pco.octreeDir = fMno.octreeDir;
						} else {
							pco.octreeDir = url + '/../' + fMno.octreeDir;
						}

						pco.spacing = fMno.spacing;
						pco.hierarchyStepSize = fMno.hierarchyStepSize;

						pco.pointAttributes = fMno.pointAttributes;

						let min = new Vector3(fMno.boundingBox.lx, fMno.boundingBox.ly, fMno.boundingBox.lz);
						let max = new Vector3(fMno.boundingBox.ux, fMno.boundingBox.uy, fMno.boundingBox.uz);
						let boundingBox = new Box3(min, max);
						let tightBoundingBox = boundingBox.clone();

						if (fMno.tightBoundingBox) {
							tightBoundingBox.min.copy(new Vector3(fMno.tightBoundingBox.lx, fMno.tightBoundingBox.ly, fMno.tightBoundingBox.lz));
							tightBoundingBox.max.copy(new Vector3(fMno.tightBoundingBox.ux, fMno.tightBoundingBox.uy, fMno.tightBoundingBox.uz));
						}

						let offset = min.clone();

						boundingBox.min.sub(offset);
						boundingBox.max.sub(offset);

						tightBoundingBox.min.sub(offset);
						tightBoundingBox.max.sub(offset);

						pco.projection = fMno.projection;
						pco.boundingBox = boundingBox;
						pco.tightBoundingBox = tightBoundingBox;
						pco.boundingSphere = boundingBox.getBoundingSphere(new Sphere());
						pco.tightBoundingSphere = tightBoundingBox.getBoundingSphere(new Sphere());
						pco.offset = offset;
						if (fMno.pointAttributes === 'LAS') {
							pco.loader = new LasLazLoader(fMno.version, "las");
							pco.pointAttributes = lasLazAttributes(fMno);
						} else if (fMno.pointAttributes === 'LAZ') {
							pco.loader = new LasLazLoader(fMno.version, "laz");
							pco.pointAttributes = lasLazAttributes(fMno);
						} else {
							pco.loader = new BinaryLoader(fMno.version, boundingBox, fMno.scale);
							pco.pointAttributes = parseAttributes(fMno);
						}

						let nodes = {};

						{ // load root
							let name = 'r';

							let root = new PointCloudOctreeGeometryNode(name, pco, boundingBox);
							root.level = 0;
							root.hasChildren = true;
							root.spacing = pco.spacing;
							if (version.upTo('1.5')) {
								root.numPoints = fMno.hierarchy[0][1];
							} else {
								root.numPoints = 0;
							}
							pco.root = root;
							pco.root.load();
							nodes[name] = root;
						}

						// load remaining hierarchy
						if (version.upTo('1.4')) {
							for (let i = 1; i < fMno.hierarchy.length; i++) {
								let name = fMno.hierarchy[i][0];
								let numPoints = fMno.hierarchy[i][1];
								let index = parseInt(name.charAt(name.length - 1));
								let parentName = name.substring(0, name.length - 1);
								let parentNode = nodes[parentName];
								let level = name.length - 1;
								//let boundingBox = POCLoader.createChildAABB(parentNode.boundingBox, index);
								let boundingBox = Utils.createChildAABB(parentNode.boundingBox, index);

								let node = new PointCloudOctreeGeometryNode(name, pco, boundingBox);
								node.level = level;
								node.numPoints = numPoints;
								node.spacing = pco.spacing / Math.pow(2, level);
								parentNode.addChild(node);
								nodes[name] = node;
							}
						}

						pco.nodes = nodes;

						callback(pco);
					}
				};

				xhr.send(null);
			} catch (e) {
				console.log("loading failed: '" + url + "'");
				console.log(e);

				callback();
			}
		}

		loadPointAttributes(mno){
			let fpa = mno.pointAttributes;
			let pa = new PointAttributes();

			for (let i = 0; i < fpa.length; i++) {
				let pointAttribute = PointAttribute[fpa[i]];
				pa.add(pointAttribute);
			}

			return pa;
		}

		createChildAABB(aabb, index){
			let min = aabb.min.clone();
			let max = aabb.max.clone();
			let size = new Vector3().subVectors(max, min);

			if ((index & 0b0001) > 0) {
				min.z += size.z / 2;
			} else {
				max.z -= size.z / 2;
			}

			if ((index & 0b0010) > 0) {
				min.y += size.y / 2;
			} else {
				max.y -= size.y / 2;
			}

			if ((index & 0b0100) > 0) {
				min.x += size.x / 2;
			} else {
				max.x -= size.x / 2;
			}

			return new Box3(min, max);
		}
	}

	class OctreeGeometry{

		constructor(){
			this.url = null;
			this.spacing = 0;
			this.boundingBox = null;
			this.root = null;
			this.pointAttributes = null;
			this.loader = null;
		}

	};

	class OctreeGeometryNode{

		constructor(name, octreeGeometry, boundingBox){
			this.id = OctreeGeometryNode.IDCount++;
			this.name = name;
			this.index = parseInt(name.charAt(name.length - 1));
			this.octreeGeometry = octreeGeometry;
			this.boundingBox = boundingBox;
			this.boundingSphere = boundingBox.getBoundingSphere(new Sphere());
			this.children = {};
			this.numPoints = 0;
			this.level = null;
			this.oneTimeDisposeHandlers = [];
		}

		isGeometryNode(){
			return true;
		}

		getLevel(){
			return this.level;
		}

		isTreeNode(){
			return false;
		}

		isLoaded(){
			return this.loaded;
		}

		getBoundingSphere(){
			return this.boundingSphere;
		}

		getBoundingBox(){
			return this.boundingBox;
		}

		getChildren(){
			let children = [];

			for (let i = 0; i < 8; i++) {
				if (this.children[i]) {
					children.push(this.children[i]);
				}
			}

			return children;
		}

		getBoundingBox(){
			return this.boundingBox;
		}

		load(){

			if (Potree.numNodesLoading >= Potree.maxNodesLoading) {
				return;
			}

			this.octreeGeometry.loader.load(this);
		}

		getNumPoints(){
			return this.numPoints;
		}

		dispose(){
			if (this.geometry && this.parent != null) {
				this.geometry.dispose();
				this.geometry = null;
				this.loaded = false;

				// this.dispatchEvent( { type: 'dispose' } );
				for (let i = 0; i < this.oneTimeDisposeHandlers.length; i++) {
					let handler = this.oneTimeDisposeHandlers[i];
					handler();
				}
				this.oneTimeDisposeHandlers = [];
			}
		}

	};

	OctreeGeometryNode.IDCount = 0;

	// let loadedNodes = new Set();

	class NodeLoader{

		constructor(url){
			this.url = url;
		}

		async load(node){

			if(node.loaded || node.loading){
				return;
			}

			node.loading = true;
			Potree.numNodesLoading++;

			// console.log(node.name, node.numPoints);

			// if(loadedNodes.has(node.name)){
			// 	// debugger;
			// }
			// loadedNodes.add(node.name);

			try{
				if(node.nodeType === 2){
					await this.loadHierarchy(node);
				}

				let {byteOffset, byteSize} = node;


				let urlOctree = `${this.url}/../octree.bin`;

				let first = byteOffset;
				let last = byteOffset + byteSize - 1n;

				let buffer;

				if(byteSize === 0n){
					buffer = new ArrayBuffer(0);
					console.warn(`loaded node with 0 bytes: ${node.name}`);
				}else {
					let response = await fetch(urlOctree, {
						headers: {
							'content-type': 'multipart/byteranges',
							'Range': `bytes=${first}-${last}`,
						},
					});

					buffer = await response.arrayBuffer();
				}

				let workerPath;
				if(this.metadata.encoding === "BROTLI"){
					workerPath = Potree.scriptPath + '/workers/2.0/DecoderWorker_brotli.js';
				}else {
					workerPath = Potree.scriptPath + '/workers/2.0/DecoderWorker.js';
				}

				let worker = Potree.workerPool.getWorker(workerPath);

				worker.onmessage = function (e) {

					let data = e.data;
					let buffers = data.attributeBuffers;

					Potree.workerPool.returnWorker(workerPath, worker);

					let geometry = new BufferGeometry();
					
					for(let property in buffers){

						let buffer = buffers[property].buffer;

						if(property === "position"){
							geometry.setAttribute('position', new BufferAttribute(new Float32Array(buffer), 3));
						}else if(property === "rgba"){
							geometry.setAttribute('rgba', new BufferAttribute(new Uint8Array(buffer), 4, true));
						}else if(property === "NORMAL"){
							//geometry.setAttribute('rgba', new THREE.BufferAttribute(new Uint8Array(buffer), 4, true));
							geometry.setAttribute('normal', new BufferAttribute(new Float32Array(buffer), 3));
						}else if (property === "INDICES") {
							let bufferAttribute = new BufferAttribute(new Uint8Array(buffer), 4);
							bufferAttribute.normalized = true;
							geometry.setAttribute('indices', bufferAttribute);
						}else {
							const bufferAttribute = new BufferAttribute(new Float32Array(buffer), 1);

							let batchAttribute = buffers[property].attribute;
							bufferAttribute.potree = {
								offset: buffers[property].offset,
								scale: buffers[property].scale,
								preciseBuffer: buffers[property].preciseBuffer,
								range: batchAttribute.range,
							};

							geometry.setAttribute(property, bufferAttribute);
						}

					}
					// indices ??

					node.density = data.density;
					node.geometry = geometry;
					node.loaded = true;
					node.loading = false;
					Potree.numNodesLoading--;
				};

				let pointAttributes = node.octreeGeometry.pointAttributes;
				let scale = node.octreeGeometry.scale;

				let box = node.boundingBox;
				let min = node.octreeGeometry.offset.clone().add(box.min);
				let size = box.max.clone().sub(box.min);
				let max = min.clone().add(size);
				let numPoints = node.numPoints;

				let offset = node.octreeGeometry.loader.offset;

				let message = {
					name: node.name,
					buffer: buffer,
					pointAttributes: pointAttributes,
					scale: scale,
					min: min,
					max: max,
					size: size,
					offset: offset,
					numPoints: numPoints
				};

				worker.postMessage(message, [message.buffer]);
			}catch(e){
				node.loaded = false;
				node.loading = false;
				Potree.numNodesLoading--;

				console.log(`failed to load ${node.name}`);
				console.log(e);
				console.log(`trying again!`);
			}
		}

		parseHierarchy(node, buffer){

			let view = new DataView(buffer);
			let tStart = performance.now();

			let bytesPerNode = 22;
			let numNodes = buffer.byteLength / bytesPerNode;

			let octree = node.octreeGeometry;
			// let nodes = [node];
			let nodes = new Array(numNodes);
			nodes[0] = node;
			let nodePos = 1;

			for(let i = 0; i < numNodes; i++){
				let current = nodes[i];

				let type = view.getUint8(i * bytesPerNode + 0);
				let childMask = view.getUint8(i * bytesPerNode + 1);
				let numPoints = view.getUint32(i * bytesPerNode + 2, true);
				let byteOffset = view.getBigInt64(i * bytesPerNode + 6, true);
				let byteSize = view.getBigInt64(i * bytesPerNode + 14, true);

				// if(byteSize === 0n){
				// 	// debugger;
				// }


				if(current.nodeType === 2){
					// replace proxy with real node
					current.byteOffset = byteOffset;
					current.byteSize = byteSize;
					current.numPoints = numPoints;
				}else if(type === 2){
					// load proxy
					current.hierarchyByteOffset = byteOffset;
					current.hierarchyByteSize = byteSize;
					current.numPoints = numPoints;
				}else {
					// load real node 
					current.byteOffset = byteOffset;
					current.byteSize = byteSize;
					current.numPoints = numPoints;
				}
				
				current.nodeType = type;

				if(current.nodeType === 2){
					continue;
				}

				for(let childIndex = 0; childIndex < 8; childIndex++){
					let childExists = ((1 << childIndex) & childMask) !== 0;

					if(!childExists){
						continue;
					}

					let childName = current.name + childIndex;

					let childAABB = createChildAABB(current.boundingBox, childIndex);
					let child = new OctreeGeometryNode(childName, octree, childAABB);
					child.name = childName;
					child.spacing = current.spacing / 2;
					child.level = current.level + 1;

					current.children[childIndex] = child;
					child.parent = current;

					// nodes.push(child);
					nodes[nodePos] = child;
					nodePos++;
				}

				// if((i % 500) === 0){
				// 	yield;
				// }
			}

			let duration = (performance.now() - tStart);

			// if(duration > 20){
			// 	let msg = `duration: ${duration}ms, numNodes: ${numNodes}`;
			// 	console.log(msg);
			// }
		}

		async loadHierarchy(node){

			let {hierarchyByteOffset, hierarchyByteSize} = node;
			let hierarchyPath = `${this.url}/../hierarchy.bin`;
			
			let first = hierarchyByteOffset;
			let last = first + hierarchyByteSize - 1n;

			let response = await fetch(hierarchyPath, {
				headers: {
					'content-type': 'multipart/byteranges',
					'Range': `bytes=${first}-${last}`,
				},
			});



			let buffer = await response.arrayBuffer();

			this.parseHierarchy(node, buffer);

			// let promise = new Promise((resolve) => {
			// 	let generator = this.parseHierarchy(node, buffer);

			// 	let repeatUntilDone = () => {
			// 		let result = generator.next();

			// 		if(result.done){
			// 			resolve();
			// 		}else{
			// 			requestAnimationFrame(repeatUntilDone);
			// 		}
			// 	};
				
			// 	repeatUntilDone();
			// });

			// await promise;

			



		}

	}

	let tmpVec3 = new Vector3();
	function createChildAABB(aabb, index){
		let min = aabb.min.clone();
		let max = aabb.max.clone();
		let size = tmpVec3.subVectors(max, min);

		if ((index & 0b0001) > 0) {
			min.z += size.z / 2;
		} else {
			max.z -= size.z / 2;
		}

		if ((index & 0b0010) > 0) {
			min.y += size.y / 2;
		} else {
			max.y -= size.y / 2;
		}
		
		if ((index & 0b0100) > 0) {
			min.x += size.x / 2;
		} else {
			max.x -= size.x / 2;
		}

		return new Box3(min, max);
	}

	let typenameTypeattributeMap = {
		"double": PointAttributeTypes.DATA_TYPE_DOUBLE,
		"float": PointAttributeTypes.DATA_TYPE_FLOAT,
		"int8": PointAttributeTypes.DATA_TYPE_INT8,
		"uint8": PointAttributeTypes.DATA_TYPE_UINT8,
		"int16": PointAttributeTypes.DATA_TYPE_INT16,
		"uint16": PointAttributeTypes.DATA_TYPE_UINT16,
		"int32": PointAttributeTypes.DATA_TYPE_INT32,
		"uint32": PointAttributeTypes.DATA_TYPE_UINT32,
		"int64": PointAttributeTypes.DATA_TYPE_INT64,
		"uint64": PointAttributeTypes.DATA_TYPE_UINT64,
	};

	class OctreeLoader{

		static parseAttributes(jsonAttributes){

			let attributes = new PointAttributes();

			let replacements = {
				"rgb": "rgba",
			};

			for (const jsonAttribute of jsonAttributes) {
				let {name, description, size, numElements, elementSize, min, max} = jsonAttribute;

				let type = typenameTypeattributeMap[jsonAttribute.type];

				let potreeAttributeName = replacements[name] ? replacements[name] : name;

				let attribute = new PointAttribute(potreeAttributeName, type, numElements);

				if(numElements === 1){
					attribute.range = [min[0], max[0]];
				}else {
					attribute.range = [min, max];
				}

				if (name === "gps-time") { // HACK: Guard against bad gpsTime range in metadata, see potree/potree#909
					if (attribute.range[0] === attribute.range[1]) {
						attribute.range[1] += 1;
					}
				}

				attribute.initialRange = attribute.range;

				attributes.add(attribute);
			}

			{
				// check if it has normals
				let hasNormals = 
					attributes.attributes.find(a => a.name === "NormalX") !== undefined &&
					attributes.attributes.find(a => a.name === "NormalY") !== undefined &&
					attributes.attributes.find(a => a.name === "NormalZ") !== undefined;

				if(hasNormals){
					let vector = {
						name: "NORMAL",
						attributes: ["NormalX", "NormalY", "NormalZ"],
					};
					attributes.addVector(vector);
				}
			}

			return attributes;
		}

		static async load(url){

			let response = await fetch(url);
			let metadata = await response.json();

			let attributes = OctreeLoader.parseAttributes(metadata.attributes);

			let loader = new NodeLoader(url);
			loader.metadata = metadata;
			loader.attributes = attributes;
			loader.scale = metadata.scale;
			loader.offset = metadata.offset;

			let octree = new OctreeGeometry();
			octree.url = url;
			octree.spacing = metadata.spacing;
			octree.scale = metadata.scale;

			// let aPosition = metadata.attributes.find(a => a.name === "position");
			// octree

			let min = new Vector3(...metadata.boundingBox.min);
			let max = new Vector3(...metadata.boundingBox.max);
			let boundingBox = new Box3(min, max);

			let offset = min.clone();
			boundingBox.min.sub(offset);
			boundingBox.max.sub(offset);

			octree.projection = metadata.projection;
			octree.boundingBox = boundingBox;
			octree.tightBoundingBox = boundingBox.clone();
			octree.boundingSphere = boundingBox.getBoundingSphere(new Sphere());
			octree.tightBoundingSphere = boundingBox.getBoundingSphere(new Sphere());
			octree.offset = offset;
			octree.pointAttributes = OctreeLoader.parseAttributes(metadata.attributes);
			octree.loader = loader;

			let root = new OctreeGeometryNode("r", octree, boundingBox);
			root.level = 0;
			root.nodeType = 2;
			root.hierarchyByteOffset = 0n;
			root.hierarchyByteSize = BigInt(metadata.hierarchy.firstChunkSize);
			root.hasChildren = false;
			root.spacing = octree.spacing;
			root.byteOffset = 0;

			octree.root = root;

			loader.load(root);

			let result = {
				geometry: octree,
			};

			return result;

		}

	};

	/**
	 * @author Connor Manning
	 */

	class EptLoader {
		static async load(file, callback) {

			let response = await fetch(file);
			let json = await response.json();

			let url = file.substr(0, file.lastIndexOf('ept.json'));
			let geometry = new Potree.PointCloudEptGeometry(url, json);
			let root = new Potree.PointCloudEptGeometryNode(geometry);

			geometry.root = root;
			geometry.root.load();

			callback(geometry);
		}
	};

	class EptBinaryLoader {
		extension() {
			return '.bin';
		}

		workerPath() {
			return Potree.scriptPath + '/workers/EptBinaryDecoderWorker.js';
		}

		load(node) {
			if (node.loaded) return;

			let url = node.url() + this.extension();

			let xhr = XHRFactory.createXMLHttpRequest();
			xhr.open('GET', url, true);
			xhr.responseType = 'arraybuffer';
			xhr.overrideMimeType('text/plain; charset=x-user-defined');
			xhr.onreadystatechange = () => {
				if (xhr.readyState === 4) {
					if (xhr.status === 200) {
						let buffer = xhr.response;
						this.parse(node, buffer);
					} else {
						console.log('Failed ' + url + ': ' + xhr.status);
					}
				}
			};

			try {
				xhr.send(null);
			}
			catch (e) {
				console.log('Failed request: ' + e);
			}
		}

		parse(node, buffer) {
			let workerPath = this.workerPath();
			let worker = Potree.workerPool.getWorker(workerPath);

			worker.onmessage = function(e) {
				let g = new BufferGeometry();
				let numPoints = e.data.numPoints;

				let position = new Float32Array(e.data.position);
				g.setAttribute('position', new BufferAttribute(position, 3));

				let indices = new Uint8Array(e.data.indices);
				g.setAttribute('indices', new BufferAttribute(indices, 4));

				if (e.data.color) {
					let color = new Uint8Array(e.data.color);
					g.setAttribute('color', new BufferAttribute(color, 4, true));
				}
				if (e.data.intensity) {
					let intensity = new Float32Array(e.data.intensity);
					g.setAttribute('intensity',
							new BufferAttribute(intensity, 1));
				}
				if (e.data.classification) {
					let classification = new Uint8Array(e.data.classification);
					g.setAttribute('classification',
							new BufferAttribute(classification, 1));
				}
				if (e.data.returnNumber) {
					let returnNumber = new Uint8Array(e.data.returnNumber);
					g.setAttribute('return number',
							new BufferAttribute(returnNumber, 1));
				}
				if (e.data.numberOfReturns) {
					let numberOfReturns = new Uint8Array(e.data.numberOfReturns);
					g.setAttribute('number of returns',
							new BufferAttribute(numberOfReturns, 1));
				}
				if (e.data.pointSourceId) {
					let pointSourceId = new Uint16Array(e.data.pointSourceId);
					g.setAttribute('source id',
							new BufferAttribute(pointSourceId, 1));
				}

				g.attributes.indices.normalized = true;

				let tightBoundingBox = new Box3(
					new Vector3().fromArray(e.data.tightBoundingBox.min),
					new Vector3().fromArray(e.data.tightBoundingBox.max)
				);

				node.doneLoading(
						g,
						tightBoundingBox,
						numPoints,
						new Vector3(...e.data.mean));

				Potree.workerPool.returnWorker(workerPath, worker);
			};

			let toArray = (v) => [v.x, v.y, v.z];
			let message = {
				buffer: buffer,
				schema: node.ept.schema,
				scale: node.ept.eptScale,
				offset: node.ept.eptOffset,
				mins: toArray(node.key.b.min)
			};

			worker.postMessage(message, [message.buffer]);
		}
	};

	/**
	 * laslaz code taken and adapted from plas.io js-laslaz
	 *	  http://plas.io/
	 *	https://github.com/verma/plasio
	 *
	 * Thanks to Uday Verma and Howard Butler
	 *
	 */

	class EptLaszipLoader {
		load(node) {
			if (node.loaded) return;

			let url = node.url() + '.laz';

			let xhr = XHRFactory.createXMLHttpRequest();
			xhr.open('GET', url, true);
			xhr.responseType = 'arraybuffer';
			xhr.overrideMimeType('text/plain; charset=x-user-defined');
			xhr.onreadystatechange = () => {
				if (xhr.readyState === 4) {
					if (xhr.status === 200) {
						let buffer = xhr.response;
						this.parse(node, buffer);
					} else {
						console.log('Failed ' + url + ': ' + xhr.status);
					}
				}
			};

			xhr.send(null);
		}

		async parse(node, buffer){
			let lf = new LASFile(buffer);
			let handler = new EptLazBatcher(node);

			try{
				await lf.open();

				lf.isOpen = true;

				const header = await lf.getHeader();

				{
					let i = 0;

					let toArray = (v) => [v.x, v.y, v.z];
					let mins = toArray(node.key.b.min);
					let maxs = toArray(node.key.b.max);

					let hasMoreData = true;

					while(hasMoreData){
						const data = await lf.readData(1000000, 0, 1);

						let d = new LASDecoder(
							data.buffer,
							header.pointsFormatId,
							header.pointsStructSize,
							data.count,
							header.scale,
							header.offset,
							mins,
							maxs);

						d.extraBytes = header.extraBytes;
						d.pointsFormatId = header.pointsFormatId;
						handler.push(d);

						i += data.count;

						hasMoreData = data.hasMoreData;
					}

					header.totalRead = i;
					header.versionAsString = lf.versionAsString;
					header.isCompressed = lf.isCompressed;

					await lf.close();

					lf.isOpen = false;
				}

			}catch(err){
				console.error('Error reading LAZ:', err);
				
				if (lf.isOpen) {
					await lf.close();

					lf.isOpen = false;
				}
				
				throw err;
			}
		}
	};

	class EptLazBatcher {
		constructor(node) { this.node = node; }

		push(las) {
			let workerPath = Potree.scriptPath +
				'/workers/EptLaszipDecoderWorker.js';
			let worker = Potree.workerPool.getWorker(workerPath);

			worker.onmessage = (e) => {
				let g = new BufferGeometry();
				let numPoints = las.pointsCount;

				let positions = new Float32Array(e.data.position);
				let colors = new Uint8Array(e.data.color);

				let intensities = new Float32Array(e.data.intensity);
				let classifications = new Uint8Array(e.data.classification);
				let returnNumbers = new Uint8Array(e.data.returnNumber);
				let numberOfReturns = new Uint8Array(e.data.numberOfReturns);
				let pointSourceIDs = new Uint16Array(e.data.pointSourceID);
				let indices = new Uint8Array(e.data.indices);
				let gpsTime = new Float32Array(e.data.gpsTime);

				g.setAttribute('position',
						new BufferAttribute(positions, 3));
				g.setAttribute('rgba',
						new BufferAttribute(colors, 4, true));
				g.setAttribute('intensity',
						new BufferAttribute(intensities, 1));
				g.setAttribute('classification',
						new BufferAttribute(classifications, 1));
				g.setAttribute('return number',
						new BufferAttribute(returnNumbers, 1));
				g.setAttribute('number of returns',
						new BufferAttribute(numberOfReturns, 1));
				g.setAttribute('source id',
						new BufferAttribute(pointSourceIDs, 1));
				g.setAttribute('indices',
						new BufferAttribute(indices, 4));
				g.setAttribute('gpsTime',
						new BufferAttribute(gpsTime, 1));
				this.node.gpsTime = e.data.gpsMeta;

				g.attributes.indices.normalized = true;

				let tightBoundingBox = new Box3(
					new Vector3().fromArray(e.data.tightBoundingBox.min),
					new Vector3().fromArray(e.data.tightBoundingBox.max)
				);

				this.node.doneLoading(
					g,
					tightBoundingBox,
					numPoints,
					new Vector3(...e.data.mean));

				Potree.workerPool.returnWorker(workerPath, worker);
			};

			let message = {
				buffer: las.arrayb,
				numPoints: las.pointsCount,
				pointSize: las.pointSize,
				pointFormatID: las.pointsFormatId,
				scale: las.scale,
				offset: las.offset,
				mins: las.mins,
				maxs: las.maxs
			};

			worker.postMessage(message, [message.buffer]);
		};
	};

	class EptZstandardLoader extends EptBinaryLoader {
	    extension() {
	        return '.zst';
	    }

	    workerPath() {
	        return Potree.scriptPath + '/workers/EptZstandardDecoderWorker.js';
	    }
	};

	class ShapefileLoader{

		constructor(){
			this.transform = null;
		}

		async load(path){

			const matLine = new LineMaterial( {
				color: 0xff0000,
				linewidth: 3, // in pixels
				resolution:  new Vector2(1000, 1000),
				dashed: false
			} );

			const features = await this.loadShapefileFeatures(path);
			const node = new Object3D();
			
			for(const feature of features){
				const fnode = this.featureToSceneNode(feature, matLine);
				node.add(fnode);
			}

			let setResolution = (x, y) => {
				matLine.resolution.set(x, y);
			};

			const result = {
				features: features,
				node: node,
				setResolution: setResolution,
			};

			return result;
		}

		featureToSceneNode(feature, matLine){
			let geometry = feature.geometry;
			
			let color = new Color(1, 1, 1);

			let transform = this.transform;
			if(transform === null){
				transform = {forward: (v) => v};
			}
			
			if(feature.geometry.type === "Point"){
				let sg = new SphereGeometry(1, 18, 18);
				let sm = new MeshNormalMaterial();
				let s = new Mesh(sg, sm);
				
				let [long, lat] = geometry.coordinates;
				let pos = transform.forward([long, lat]);
				
				s.position.set(...pos, 20);
				
				s.scale.set(10, 10, 10);
				
				return s;
			}else if(geometry.type === "LineString"){
				let coordinates = [];
				
				let min = new Vector3(Infinity, Infinity, Infinity);
				for(let i = 0; i < geometry.coordinates.length; i++){
					let [long, lat] = geometry.coordinates[i];
					let pos = transform.forward([long, lat]);
					
					min.x = Math.min(min.x, pos[0]);
					min.y = Math.min(min.y, pos[1]);
					min.z = Math.min(min.z, 20);
					
					coordinates.push(...pos, 20);
					if(i > 0 && i < geometry.coordinates.length - 1){
						coordinates.push(...pos, 20);
					}
				}
				
				for(let i = 0; i < coordinates.length; i += 3){
					coordinates[i+0] -= min.x;
					coordinates[i+1] -= min.y;
					coordinates[i+2] -= min.z;
				}
				
				const lineGeometry = new LineGeometry();
				lineGeometry.setPositions( coordinates );

				const line = new Line2( lineGeometry, matLine );
				line.computeLineDistances();
				line.scale.set( 1, 1, 1 );
				line.position.copy(min);
				
				return line;
			}else if(geometry.type === "Polygon"){
				for(let pc of geometry.coordinates){
					let coordinates = [];
					
					let min = new Vector3(Infinity, Infinity, Infinity);
					for(let i = 0; i < pc.length; i++){
						let [long, lat] = pc[i];
						let pos = transform.forward([long, lat]);
						
						min.x = Math.min(min.x, pos[0]);
						min.y = Math.min(min.y, pos[1]);
						min.z = Math.min(min.z, 20);
						
						coordinates.push(...pos, 20);
						if(i > 0 && i < pc.length - 1){
							coordinates.push(...pos, 20);
						}
					}
					
					for(let i = 0; i < coordinates.length; i += 3){
						coordinates[i+0] -= min.x;
						coordinates[i+1] -= min.y;
						coordinates[i+2] -= min.z;
					}

					const lineGeometry = new LineGeometry();
					lineGeometry.setPositions( coordinates );

					const line = new Line2( lineGeometry, matLine );
					line.computeLineDistances();
					line.scale.set( 1, 1, 1 );
					line.position.copy(min);
					
					return line;
				}
			}else {
				console.log("unhandled feature: ", feature);
			}
		}

		async loadShapefileFeatures(file){
			let features = [];

			let source = await shapefile.open(file);

			while(true){
				let result = await source.read();

				if (result.done) {
					break;
				}

				if (result.value && result.value.type === 'Feature' && result.value.geometry !== undefined) {
					features.push(result.value);
				}
			}

			return features;
		}

	};

	const defaultColors = {
		"landuse":   [0.5, 0.5, 0.5],
		"natural":   [0.0, 1.0, 0.0],
		"places":    [1.0, 0.0, 1.0],
		"points":    [0.0, 1.0, 1.0],
		"roads":     [1.0, 1.0, 0.0],
		"waterways": [0.0, 0.0, 1.0],
		"default":   [0.9, 0.6, 0.1],
	};

	function getColor(feature){
		let color = defaultColors[feature];

		if(!color){
			color = defaultColors["default"];
		}

		return color;
	}

	class Geopackage$1{
		constructor(){
			this.path = null;
			this.node = null;
		}
	};

	class GeoPackageLoader{

		constructor(){

		}

		static async loadUrl(url, params){

			await Promise.all([
				Utils.loadScript(`${Potree.scriptPath}/lazylibs/geopackage/geopackage.js`),
				Utils.loadScript(`${Potree.scriptPath}/lazylibs/sql.js/sql-wasm.js`),
			]);
			
			const result = await fetch(url);
			const buffer = await result.arrayBuffer();

			params = params || {};

			params.source = url;

			return GeoPackageLoader.loadBuffer(buffer, params);
		}

		static async loadBuffer(buffer, params){

			await Promise.all([
				Utils.loadScript(`${Potree.scriptPath}/lazylibs/geopackage/geopackage.js`),
				Utils.loadScript(`${Potree.scriptPath}/lazylibs/sql.js/sql-wasm.js`),
			]);

			params = params || {};

			const resolver = async (resolve) => {
				
				let transform = params.transform;
				if(!transform){
					transform = {forward: (arg) => arg};
				}

				const wasmPath = `${Potree.scriptPath}/lazylibs/sql.js/sql-wasm.wasm`;
				const SQL = await initSqlJs({ locateFile: filename => wasmPath});

				const u8 = new Uint8Array(buffer);

				const data = await geopackage.open(u8);
				window.data = data;

				const geopackageNode = new Object3D();
				geopackageNode.name = params.source;
				geopackageNode.potree = {
					source: params.source,
				};

				const geo = new Geopackage$1();
				geo.path = params.source;
				geo.node = geopackageNode;

				const tables = data.getTables();

				for(const table of tables.features){
					const dao = data.getFeatureDao(table);

					let boundingBox = dao.getBoundingBox();
					boundingBox = boundingBox.projectBoundingBox(dao.projection, 'EPSG:4326');
					const geoJson = data.queryForGeoJSONFeaturesInTable(table, boundingBox);

					const matLine = new LineMaterial( {
						color: new Color().setRGB(...getColor(table)),
						linewidth: 2, 
						resolution:  new Vector2(1000, 1000),
						dashed: false
					} );

					const node = new Object3D();
					node.name = table;
					geo.node.add(node);

					for(const [index, feature] of Object.entries(geoJson)){
						//const featureNode = GeoPackageLoader.featureToSceneNode(feature, matLine, transform);
						const featureNode = GeoPackageLoader.featureToSceneNode(feature, matLine, dao.projection, transform);
						node.add(featureNode);
					}
				}

				resolve(geo);
			};

			return new Promise(resolver);
		}

		static featureToSceneNode(feature, matLine, geopackageProjection, transform){
			let geometry = feature.geometry;
			
			let color = new Color(1, 1, 1);
			
			if(feature.geometry.type === "Point"){
				let sg = new SphereGeometry(1, 18, 18);
				let sm = new MeshNormalMaterial();
				let s = new Mesh(sg, sm);
				
				let [long, lat] = geometry.coordinates;
				let pos = transform.forward(geopackageProjection.forward([long, lat]));
				
				s.position.set(...pos, 20);
				
				s.scale.set(10, 10, 10);
				
				return s;
			}else if(geometry.type === "LineString"){
				let coordinates = [];
				
				let min = new Vector3(Infinity, Infinity, Infinity);
				for(let i = 0; i < geometry.coordinates.length; i++){
					let [long, lat] = geometry.coordinates[i];
					let pos = transform.forward(geopackageProjection.forward([long, lat]));
					
					min.x = Math.min(min.x, pos[0]);
					min.y = Math.min(min.y, pos[1]);
					min.z = Math.min(min.z, 20);
					
					coordinates.push(...pos, 20);
					if(i > 0 && i < geometry.coordinates.length - 1){
						coordinates.push(...pos, 20);
					}
				}
				
				for(let i = 0; i < coordinates.length; i += 3){
					coordinates[i+0] -= min.x;
					coordinates[i+1] -= min.y;
					coordinates[i+2] -= min.z;
				}
				
				const lineGeometry = new LineGeometry();
				lineGeometry.setPositions( coordinates );

				const line = new Line2( lineGeometry, matLine );
				line.computeLineDistances();
				line.scale.set( 1, 1, 1 );
				line.position.copy(min);
				
				return line;
			}else if(geometry.type === "Polygon"){
				for(let pc of geometry.coordinates){
					let coordinates = [];
					
					let min = new Vector3(Infinity, Infinity, Infinity);
					for(let i = 0; i < pc.length; i++){
						let [long, lat] = pc[i];
						
						let pos = transform.forward(geopackageProjection.forward([long, lat]));
						
						min.x = Math.min(min.x, pos[0]);
						min.y = Math.min(min.y, pos[1]);
						min.z = Math.min(min.z, 20);
						
						coordinates.push(...pos, 20);
						if(i > 0 && i < pc.length - 1){
							coordinates.push(...pos, 20);
						}
					}
					
					for(let i = 0; i < coordinates.length; i += 3){
						coordinates[i+0] -= min.x;
						coordinates[i+1] -= min.y;
						coordinates[i+2] -= min.z;
					}

					const lineGeometry = new LineGeometry();
					lineGeometry.setPositions( coordinates );

					const line = new Line2( lineGeometry, matLine );
					line.computeLineDistances();
					line.scale.set( 1, 1, 1 );
					line.position.copy(min);
					
					return line;
				}
			}else {
				console.log("unhandled feature: ", feature);
			}
		}

	};

	class ClipVolume extends Object3D{
		
		constructor(args){
			super();
			
			this.constructor.counter = (this.constructor.counter === undefined) ? 0 : this.constructor.counter + 1;
			this.name = "clip_volume_" + this.constructor.counter;

			let alpha = args.alpha || 0;
			let beta = args.beta || 0;
			let gamma = args.gamma || 0;

			this.rotation.x = alpha;
			this.rotation.y = beta;
			this.rotation.z = gamma;

			this.clipOffset = 0.001;
			this.clipRotOffset = 1;
					
			let boxGeometry = new BoxGeometry(1, 1, 1);
			boxGeometry.computeBoundingBox();
			
			let boxFrameGeometry = new Geometry();
			{			
				// bottom
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5));
				// top
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5));
				// sides
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5));

				boxFrameGeometry.colors.push(new Vector3(1, 1, 1));
			}

			let planeFrameGeometry = new Geometry();
			{						
				// middle line
				planeFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.0));
				planeFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.0));
				planeFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.0));
				planeFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.0));
				planeFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.0));
				planeFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.0));
				planeFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.0));
				planeFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.0));
			}

			this.dimension = new Vector3(1, 1, 1);
			this.material = new MeshBasicMaterial( {
				color: 0x00ff00, 
				transparent: true, 
				opacity: 0.3,
				depthTest: true, 
				depthWrite: false} );
			this.box = new Mesh(boxGeometry, this.material);
			this.box.geometry.computeBoundingBox();
			this.boundingBox = this.box.geometry.boundingBox;
			this.add(this.box);
			
			this.frame = new LineSegments( boxFrameGeometry, new LineBasicMaterial({color: 0x000000}));
			this.add(this.frame);
			this.planeFrame = new LineSegments( planeFrameGeometry, new LineBasicMaterial({color: 0xff0000}));
			this.add(this.planeFrame);

			// set default thickness
			this.setScaleZ(0.1);

			// create local coordinate system
			let createArrow = (name, direction, color) => {
				let material = new MeshBasicMaterial({
					color: color, 
					depthTest: false, 
					depthWrite: false});
					
				let shaftGeometry = new Geometry();
				shaftGeometry.vertices.push(new Vector3(0, 0, 0));
				shaftGeometry.vertices.push(new Vector3(0, 1, 0));
				
				let shaftMaterial = new LineBasicMaterial({
					color: color, 
					depthTest: false, 
					depthWrite: false,
					transparent: true
					});
				let shaft = new Line(shaftGeometry, shaftMaterial);
				shaft.name = name + "_shaft";
				
				let headGeometry = new CylinderGeometry(0, 0.04, 0.1, 10, 1, false);
				let headMaterial = material;
				let head = new Mesh(headGeometry, headMaterial);
				head.name = name + "_head";
				head.position.y = 1;
				
				let arrow = new Object3D();
				arrow.name = name;
				arrow.add(shaft);
				arrow.add(head);

				return arrow;
			};
			
			this.arrowX = createArrow("arrow_x", new Vector3(1, 0, 0), 0xFF0000);
			this.arrowY = createArrow("arrow_y", new Vector3(0, 1, 0), 0x00FF00);
			this.arrowZ = createArrow("arrow_z", new Vector3(0, 0, 1), 0x0000FF);
			
			this.arrowX.rotation.z = -Math.PI / 2;
			this.arrowZ.rotation.x = Math.PI / 2;

			this.arrowX.visible = false;
			this.arrowY.visible = false;
			this.arrowZ.visible = false;

			this.add(this.arrowX);
			this.add(this.arrowY);
			this.add(this.arrowZ);
			
			{ // event listeners
				this.addEventListener("ui_select", e => { 
					this.arrowX.visible = true;
					this.arrowY.visible = true;
					this.arrowZ.visible = true; 
				});
				this.addEventListener("ui_deselect", e => {
					this.arrowX.visible = false;
					this.arrowY.visible = false;
					this.arrowZ.visible = false; 				
				});
				this.addEventListener("select", e => { 
					let scene_header = $("#" + this.name + " .scene_header");
					if(!scene_header.next().is(":visible")) {
						scene_header.click();
					}
				});
				this.addEventListener("deselect", e => { 
					let scene_header = $("#" + this.name + " .scene_header");
					if(scene_header.next().is(":visible")) {
						scene_header.click();
					}
				});
			}
			
			this.update();
		};

		setClipOffset(offset) {		
			this.clipOffset = offset;	
		}

		setClipRotOffset(offset) {		
			this.clipRotOffset = offset;		
		}

		setScaleX(x) {
			this.box.scale.x = x;
			this.frame.scale.x = x;
			this.planeFrame.scale.x = x;			
		}

		setScaleY(y) {
			this.box.scale.y = y;
			this.frame.scale.y = y;
			this.planeFrame.scale.y = y;		
		}

		setScaleZ(z) {
			this.box.scale.z = z;
			this.frame.scale.z = z;
			this.planeFrame.scale.z = z;		
		}

		offset(args) {
			let cs = args.cs || null;
			let axis = args.axis || null;
			let dir = args.dir || null;

			if(!cs || !axis || !dir) return;

			if(axis === "x") {
				if(cs === "local") {
					this.position.add(this.localX.clone().multiplyScalar(dir * this.clipOffset));
				} else if(cs === "global") {
					this.position.x = this.position.x + dir * this.clipOffset;
				}
			}else if(axis === "y") {
				if(cs === "local") {
					this.position.add(this.localY.clone().multiplyScalar(dir * this.clipOffset));
				} else if(cs === "global") {
					this.position.y = this.position.y + dir * this.clipOffset;
				}
			}else if(axis === "z") {
				if(cs === "local") {
					this.position.add(this.localZ.clone().multiplyScalar(dir * this.clipOffset));
				} else if(cs === "global") {
					this.position.z = this.position.z + dir * this.clipOffset;
				}
			}

			this.dispatchEvent({"type": "clip_volume_changed", "viewer": viewer, "volume": this});
		}	

		rotate(args) {
			let cs = args.cs || null;
			let axis = args.axis || null;
			let dir = args.dir || null;

			if(!cs || !axis || !dir) return;

			if(cs === "local") {
				if(axis === "x") {
					this.rotateOnAxis(new Vector3(1, 0, 0), dir * this.clipRotOffset * Math.PI / 180);
				} else if(axis === "y") {
					this.rotateOnAxis(new Vector3(0, 1, 0), dir * this.clipRotOffset * Math.PI / 180);
				} else if(axis === "z") {
					this.rotateOnAxis(new Vector3(0, 0, 1), dir * this.clipRotOffset * Math.PI / 180);
				}
			} else if(cs === "global") {
				let rotaxis = new Vector4(1, 0, 0, 0);	
				if(axis === "y") {
					rotaxis = new Vector4(0, 1, 0, 0);
				} else if(axis === "z") {
					rotaxis = new Vector4(0, 0, 1, 0);
				}
				this.updateMatrixWorld();
				let invM = newthis.matrixWorld.clone().invert();
				rotaxis = rotaxis.applyMatrix4(invM).normalize();
				rotaxis = new Vector3(rotaxis.x, rotaxis.y, rotaxis.z);
				this.rotateOnAxis(rotaxis, dir * this.clipRotOffset * Math.PI / 180);
			}

			this.updateLocalSystem();

			this.dispatchEvent({"type": "clip_volume_changed", "viewer": viewer, "volume": this});
		}	

		update(){
			this.boundingBox = this.box.geometry.boundingBox;
			this.boundingSphere = this.boundingBox.getBoundingSphere(new Sphere());
			
			this.box.visible = false;

			this.updateLocalSystem();
		};

		updateLocalSystem() {		
			// extract local coordinate axes
			let rotQuat = this.getWorldQuaternion();
			this.localX = new Vector3(1, 0, 0).applyQuaternion(rotQuat).normalize();
			this.localY = new Vector3(0, 1, 0).applyQuaternion(rotQuat).normalize();
			this.localZ = new Vector3(0, 0, 1).applyQuaternion(rotQuat).normalize();
		}
		
		raycast(raycaster, intersects){
			
			let is = [];
			this.box.raycast(raycaster, is);
		
			if(is.length > 0){
				let I = is[0];
				intersects.push({
					distance: I.distance,
					object: this,
					point: I.point.clone()
				});
			}
		};
	};

	class ClippingTool extends EventDispatcher{

		constructor(viewer){
			super(); 

			this.viewer = viewer;

			this.maxPolygonVertices = 8; 
			
			this.addEventListener("start_inserting_clipping_volume", e => {
				this.viewer.dispatchEvent({
					type: "cancel_insertions"
				});
			});

			this.sceneMarker = new Scene();
			this.sceneVolume = new Scene();
			this.sceneVolume.name = "scene_clip_volume";
			this.viewer.inputHandler.registerInteractiveScene(this.sceneVolume);

			this.onRemove = e => {
				this.sceneVolume.remove(e.volume);
			};
			
			this.onAdd = e => {
				this.sceneVolume.add(e.volume);
			};
			
			this.viewer.inputHandler.addEventListener("delete", e => {
				let volumes = e.selection.filter(e => (e instanceof ClipVolume));
				volumes.forEach(e => this.viewer.scene.removeClipVolume(e));
				let polyVolumes = e.selection.filter(e => (e instanceof PolygonClipVolume));
				polyVolumes.forEach(e => this.viewer.scene.removePolygonClipVolume(e));
			});
		}

		setScene(scene){
			if(this.scene === scene){
				return;
			}
			
			if(this.scene){
				this.scene.removeEventListeners("clip_volume_added", this.onAdd);
				this.scene.removeEventListeners("clip_volume_removed", this.onRemove);
				this.scene.removeEventListeners("polygon_clip_volume_added", this.onAdd);
				this.scene.removeEventListeners("polygon_clip_volume_removed", this.onRemove);
			}
			
			this.scene = scene;
			
			this.scene.addEventListener("clip_volume_added", this.onAdd);
			this.scene.addEventListener("clip_volume_removed", this.onRemove);
			this.scene.addEventListener("polygon_clip_volume_added", this.onAdd);
			this.scene.addEventListener("polygon_clip_volume_removed", this.onRemove);
		}

		startInsertion(args = {}) {	
			let type = args.type || null;

			if(!type) return null;

			let domElement = this.viewer.renderer.domElement;
			let canvasSize = this.viewer.renderer.getSize(new Vector2());

			let svg = $(`
		<svg height="${canvasSize.height}" width="${canvasSize.width}" style="position:absolute; pointer-events: none">

			<defs>
				 <marker id="diamond" markerWidth="24" markerHeight="24" refX="12" refY="12"
						markerUnits="userSpaceOnUse">
					<circle cx="12" cy="12" r="6" fill="white" stroke="black" stroke-width="3"/>
				</marker>
			</defs>

			<polyline fill="none" stroke="black" 
				style="stroke:rgb(0, 0, 0);
				stroke-width:6;"
				stroke-dasharray="9, 6"
				stroke-dashoffset="2"
				/>

			<polyline fill="none" stroke="black" 
				style="stroke:rgb(255, 255, 255);
				stroke-width:2;"
				stroke-dasharray="5, 10"
				marker-start="url(#diamond)" 
				marker-mid="url(#diamond)" 
				marker-end="url(#diamond)" 
				/>
		</svg>`);
			$(domElement.parentElement).append(svg);

			let polyClipVol = new PolygonClipVolume(this.viewer.scene.getActiveCamera().clone());

			this.dispatchEvent({"type": "start_inserting_clipping_volume"});

			this.viewer.scene.addPolygonClipVolume(polyClipVol);
			this.sceneMarker.add(polyClipVol);

			let cancel = {
				callback: null
			};

			let insertionCallback = (e) => {
				if(e.button === MOUSE.LEFT){
					
					polyClipVol.addMarker();

					// SVC Screen Line
					svg.find("polyline").each((index, target) => {
						let newPoint = svg[0].createSVGPoint();
						newPoint.x = e.offsetX;
						newPoint.y = e.offsetY;
						let polyline = target.points.appendItem(newPoint);
					});
					
					
					if(polyClipVol.markers.length > this.maxPolygonVertices){
						cancel.callback();
					}
					
					this.viewer.inputHandler.startDragging(
						polyClipVol.markers[polyClipVol.markers.length - 1]);
				}else if(e.button === MOUSE.RIGHT){
					cancel.callback(e);
				}
			};
			
			cancel.callback = e => {

				//let first = svg.find("polyline")[0].points[0];
				//svg.find("polyline").each((index, target) => {
				//	let newPoint = svg[0].createSVGPoint();
				//	newPoint.x = first.x;
				//	newPoint.y = first.y;
				//	let polyline = target.points.appendItem(newPoint);
				//});
				svg.remove();

				if(polyClipVol.markers.length > 3) {
					polyClipVol.removeLastMarker();
					polyClipVol.initialized = true;	
				} else {
					this.viewer.scene.removePolygonClipVolume(polyClipVol);
				}

				this.viewer.renderer.domElement.removeEventListener("mouseup", insertionCallback, true);
				this.viewer.removeEventListener("cancel_insertions", cancel.callback);
				this.viewer.inputHandler.enabled = true;
			};
			
			this.viewer.addEventListener("cancel_insertions", cancel.callback);
			this.viewer.renderer.domElement.addEventListener("mouseup", insertionCallback , true);
			this.viewer.inputHandler.enabled = false;
			
			polyClipVol.addMarker();
			this.viewer.inputHandler.startDragging(
				polyClipVol.markers[polyClipVol.markers.length - 1]);

			return polyClipVol;
		}

		update() {

		}
	};

	var GeoTIFF = (function (exports) {
	'use strict';

	const Endianness = new Enum({
		LITTLE: "II",
		BIG: "MM",
	});

	const Type = new Enum({
		BYTE: {value: 1, bytes: 1},
		ASCII: {value: 2, bytes: 1},
		SHORT: {value: 3, bytes: 2},
		LONG: {value: 4, bytes: 4},
		RATIONAL: {value: 5, bytes: 8},
		SBYTE: {value: 6, bytes: 1},
		UNDEFINED: {value: 7, bytes: 1},
		SSHORT: {value: 8, bytes: 2},
		SLONG: {value: 9, bytes: 4},
		SRATIONAL: {value: 10, bytes: 8},
		FLOAT: {value: 11, bytes: 4},
		DOUBLE: {value: 12, bytes: 8},
	});

	const Tag = new Enum({
		IMAGE_WIDTH: 256,
		IMAGE_HEIGHT: 257,
		BITS_PER_SAMPLE: 258,
		COMPRESSION: 259,
		PHOTOMETRIC_INTERPRETATION: 262,
		STRIP_OFFSETS: 273,
		ORIENTATION: 274,
		SAMPLES_PER_PIXEL: 277,
		ROWS_PER_STRIP: 278,
		STRIP_BYTE_COUNTS: 279,
		X_RESOLUTION: 282,
		Y_RESOLUTION: 283,
		PLANAR_CONFIGURATION: 284,
		RESOLUTION_UNIT: 296,
		SOFTWARE: 305,
		COLOR_MAP: 320,
		SAMPLE_FORMAT: 339,
		MODEL_PIXEL_SCALE: 33550,         // [GeoTIFF] TYPE: double   N: 3
		MODEL_TIEPOINT: 33922,            // [GeoTIFF] TYPE: double   N: 6 * NUM_TIEPOINTS
		GEO_KEY_DIRECTORY: 34735,         // [GeoTIFF] TYPE: short    N: >= 4
		GEO_DOUBLE_PARAMS: 34736,         // [GeoTIFF] TYPE: short    N: variable
		GEO_ASCII_PARAMS: 34737,          // [GeoTIFF] TYPE: ascii    N: variable
	});

	const typeMapping = new Map([
		[Type.BYTE, Uint8Array],
		[Type.ASCII, Uint8Array],
		[Type.SHORT, Uint16Array],
		[Type.LONG, Uint32Array],
		[Type.RATIONAL, Uint32Array],
		[Type.SBYTE, Int8Array],
		[Type.UNDEFINED, Uint8Array],
		[Type.SSHORT, Int16Array],
		[Type.SLONG, Int32Array],
		[Type.SRATIONAL, Int32Array],
		[Type.FLOAT, Float32Array],
		[Type.DOUBLE, Float64Array],
	]);

	class IFDEntry{

		constructor(tag, type, count, offset, value){
			this.tag = tag;
			this.type = type;
			this.count = count;
			this.offset = offset;
			this.value = value;
		}

	}

	class Image{

		constructor(){
			this.width = 0;
			this.height = 0;
			this.buffer = null;
			this.metadata = [];
		}

	}

	class Reader{

		constructor(){

		}

		static read(data){

			let endiannessTag = String.fromCharCode(...Array.from(data.slice(0, 2)));
			let endianness = Endianness.fromValue(endiannessTag);

			let tiffCheckTag = data.readUInt8(2);

			if(tiffCheckTag !== 42){
				throw new Error("not a valid tiff file");
			}

			let offsetToFirstIFD = data.readUInt32LE(4);

			console.log("offsetToFirstIFD", offsetToFirstIFD);

			let ifds = [];
			let IFDsRead = false;
			let currentIFDOffset = offsetToFirstIFD;
			let i = 0;
			while(IFDsRead || i < 100){

				console.log("currentIFDOffset", currentIFDOffset);
				let numEntries = data.readUInt16LE(currentIFDOffset);
				let nextIFDOffset = data.readUInt32LE(currentIFDOffset + 2 + numEntries * 12);

				console.log("next offset: ", currentIFDOffset + 2 + numEntries * 12);

				let entryBuffer = data.slice(currentIFDOffset + 2, currentIFDOffset + 2 + 12 * numEntries);

				for(let i = 0; i < numEntries; i++){
					let tag = Tag.fromValue(entryBuffer.readUInt16LE(i * 12));
					let type = Type.fromValue(entryBuffer.readUInt16LE(i * 12 + 2));
					let count = entryBuffer.readUInt32LE(i * 12 + 4);
					let offsetOrValue = entryBuffer.readUInt32LE(i * 12 + 8);
					let valueBytes = type.bytes * count;

					let value;
					if(valueBytes <= 4){
						value = offsetOrValue;
					}else {
						let valueBuffer = new Uint8Array(valueBytes);
						valueBuffer.set(data.slice(offsetOrValue, offsetOrValue + valueBytes));
						
						let ArrayType = typeMapping.get(type);

						value = new ArrayType(valueBuffer.buffer);

						if(type === Type.ASCII){
							value = String.fromCharCode(...value);
						}
					}

					let ifd = new IFDEntry(tag, type, count, offsetOrValue, value);

					ifds.push(ifd);
				}

				console.log("nextIFDOffset", nextIFDOffset);

				if(nextIFDOffset === 0){
					break;
				}

				currentIFDOffset = nextIFDOffset;
				i++;
			}

			let ifdForTag = (tag) => {
				for(let entry of ifds){
					if(entry.tag === tag){
						return entry;
					}
				}

				return null;
			};

			let width = ifdForTag(Tag.IMAGE_WIDTH, ifds).value;
			let height = ifdForTag(Tag.IMAGE_HEIGHT, ifds).value;
			let compression = ifdForTag(Tag.COMPRESSION, ifds).value;
			let rowsPerStrip = ifdForTag(Tag.ROWS_PER_STRIP, ifds).value; 
			let ifdStripOffsets = ifdForTag(Tag.STRIP_OFFSETS, ifds);
			let ifdStripByteCounts = ifdForTag(Tag.STRIP_BYTE_COUNTS, ifds);

			let numStrips = Math.ceil(height / rowsPerStrip);

			let stripByteCounts = [];
			for(let i = 0; i < ifdStripByteCounts.count; i++){
				let type = ifdStripByteCounts.type;
				let offset = ifdStripByteCounts.offset + i * type.bytes;

				let value;
				if(type === Type.SHORT){
					value = data.readUInt16LE(offset);
				}else if(type === Type.LONG){
					value = data.readUInt32LE(offset);
				}

				stripByteCounts.push(value);
			}

			let stripOffsets = [];
			for(let i = 0; i < ifdStripOffsets.count; i++){
				let type = ifdStripOffsets.type;
				let offset = ifdStripOffsets.offset + i * type.bytes;

				let value;
				if(type === Type.SHORT){
					value = data.readUInt16LE(offset);
				}else if(type === Type.LONG){
					value = data.readUInt32LE(offset);
				}

				stripOffsets.push(value);
			}

			let imageBuffer = new Uint8Array(width * height * 3);
			
			let linesProcessed = 0;
			for(let i = 0; i < numStrips; i++){
				let stripOffset = stripOffsets[i];
				let stripBytes = stripByteCounts[i];
				let stripData = data.slice(stripOffset, stripOffset + stripBytes);
				let lineBytes = width * 3;
				for(let y = 0; y < rowsPerStrip; y++){
					let line = stripData.slice(y * lineBytes, y * lineBytes + lineBytes);
					imageBuffer.set(line, linesProcessed * lineBytes);
			
					if(line.length === lineBytes){
						linesProcessed++;
					}else {
						break;
					}
				}
			}

			console.log(`width: ${width}`);
			console.log(`height: ${height}`);
			console.log(`numStrips: ${numStrips}`);
			console.log("stripByteCounts", stripByteCounts.join(", "));
			console.log("stripOffsets", stripOffsets.join(", "));

			let image = new Image();
			image.width = width;
			image.height = height;
			image.buffer = imageBuffer;
			image.metadata = ifds;

			return image;
		}

	}


	class Exporter{

		constructor(){

		}

		static toTiffBuffer(image, params = {}){

			let offsetToFirstIFD = 8;
			
			let headerBuffer = new Uint8Array([0x49, 0x49, 42, 0, offsetToFirstIFD, 0, 0, 0]);

			let [width, height] = [image.width, image.height];

			let ifds = [
				new IFDEntry(Tag.IMAGE_WIDTH,                Type.SHORT,    1,   null, width),
				new IFDEntry(Tag.IMAGE_HEIGHT,               Type.SHORT,    1,   null, height),
				new IFDEntry(Tag.BITS_PER_SAMPLE,            Type.SHORT,    4,   null, new Uint16Array([8, 8, 8, 8])),
				new IFDEntry(Tag.COMPRESSION,                Type.SHORT,    1,   null, 1),
				new IFDEntry(Tag.PHOTOMETRIC_INTERPRETATION, Type.SHORT,    1,   null, 2),
				new IFDEntry(Tag.ORIENTATION,                Type.SHORT,    1,   null, 1),
				new IFDEntry(Tag.SAMPLES_PER_PIXEL,          Type.SHORT,    1,   null, 4),
				new IFDEntry(Tag.ROWS_PER_STRIP,             Type.LONG,     1,   null, height),
				new IFDEntry(Tag.STRIP_BYTE_COUNTS,          Type.LONG,     1,   null, width * height * 3),
				new IFDEntry(Tag.PLANAR_CONFIGURATION,       Type.SHORT,    1,   null, 1),
				new IFDEntry(Tag.RESOLUTION_UNIT,            Type.SHORT,    1,   null, 1),
				new IFDEntry(Tag.SOFTWARE,                   Type.ASCII,    6,   null, "......"),
				new IFDEntry(Tag.STRIP_OFFSETS,              Type.LONG,     1,   null, null),
				new IFDEntry(Tag.X_RESOLUTION,               Type.RATIONAL, 1,   null, new Uint32Array([1, 1])),
				new IFDEntry(Tag.Y_RESOLUTION,               Type.RATIONAL, 1,   null, new Uint32Array([1, 1])),
			];

			if(params.ifdEntries){
				ifds.push(...params.ifdEntries);
			}

			let valueOffset = offsetToFirstIFD + 2 + ifds.length * 12 + 4;

			// create 12 byte buffer for each ifd and variable length buffers for ifd values
			let ifdEntryBuffers = new Map();
			let ifdValueBuffers = new Map();
			for(let ifd of ifds){
				let entryBuffer = new ArrayBuffer(12);
				let entryView = new DataView(entryBuffer);

				let valueBytes = ifd.type.bytes * ifd.count;

				entryView.setUint16(0, ifd.tag.value, true);
				entryView.setUint16(2, ifd.type.value, true);
				entryView.setUint32(4, ifd.count, true);

				if(ifd.count === 1 && ifd.type.bytes <= 4){
					entryView.setUint32(8, ifd.value, true);
				}else {
					entryView.setUint32(8, valueOffset, true);

					let valueBuffer = new Uint8Array(ifd.count * ifd.type.bytes);
					if(ifd.type === Type.ASCII){
						valueBuffer.set(new Uint8Array(ifd.value.split("").map(c => c.charCodeAt(0))));
					}else {
						valueBuffer.set(new Uint8Array(ifd.value.buffer));
					}
					ifdValueBuffers.set(ifd.tag, valueBuffer);

					valueOffset = valueOffset + valueBuffer.byteLength;
				}

				ifdEntryBuffers.set(ifd.tag, entryBuffer);
			}

			let imageBufferOffset = valueOffset;

			new DataView(ifdEntryBuffers.get(Tag.STRIP_OFFSETS)).setUint32(8, imageBufferOffset, true);

			let concatBuffers = (buffers) => {

				let totalLength = buffers.reduce( (sum, buffer) => (sum + buffer.byteLength), 0);
				let merged = new Uint8Array(totalLength);

				let offset = 0;
				for(let buffer of buffers){
					merged.set(new Uint8Array(buffer), offset);
					offset += buffer.byteLength;
				}

				return merged;
			};
			
			let ifdBuffer = concatBuffers([
				new Uint16Array([ifds.length]), 
				...ifdEntryBuffers.values(), 
				new Uint32Array([0])]);
			let ifdValueBuffer = concatBuffers([...ifdValueBuffers.values()]);

			let tiffBuffer = concatBuffers([
				headerBuffer,
				ifdBuffer,
				ifdValueBuffer,
				image.buffer
			]);

			return {width: width, height: height, buffer: tiffBuffer};
		}

	}

	exports.Tag = Tag;
	exports.Type = Type;
	exports.IFDEntry = IFDEntry;
	exports.Image = Image;
	exports.Reader = Reader;
	exports.Exporter = Exporter;

	return exports;

	}({}));

	function updateAzimuth(viewer, measure){

		const azimuth = measure.azimuth;

		const isOkay = measure.points.length === 2;

		azimuth.node.visible = isOkay && measure.showAzimuth;

		if(!azimuth.node.visible){
			return;
		}

		const camera = viewer.scene.getActiveCamera();
		const renderAreaSize = viewer.renderer.getSize(new Vector2());
		const width = renderAreaSize.width;
		const height = renderAreaSize.height;
		
		const [p0, p1] = measure.points;
		const r = p0.position.distanceTo(p1.position);
		const northVec = Utils.getNorthVec(p0.position, r, viewer.getProjection());
		const northPos = p0.position.clone().add(northVec);

		azimuth.center.position.copy(p0.position);
		azimuth.center.scale.set(2, 2, 2);
		
		azimuth.center.visible = false;
		// azimuth.target.visible = false;


		{ // north
			azimuth.north.position.copy(northPos);
			azimuth.north.scale.set(2, 2, 2);

			let distance = azimuth.north.position.distanceTo(camera.position);
			let pr = Utils.projectedRadius(1, camera, distance, width, height);

			let scale = (5 / pr);
			azimuth.north.scale.set(scale, scale, scale);
		}

		{ // target
			azimuth.target.position.copy(p1.position);
			azimuth.target.position.z = azimuth.north.position.z;

			let distance = azimuth.target.position.distanceTo(camera.position);
			let pr = Utils.projectedRadius(1, camera, distance, width, height);

			let scale = (5 / pr);
			azimuth.target.scale.set(scale, scale, scale);
		}


		azimuth.circle.position.copy(p0.position);
		azimuth.circle.scale.set(r, r, r);
		azimuth.circle.material.resolution.set(width, height);

		// to target
		azimuth.centerToTarget.geometry.setPositions([
			0, 0, 0,
			...p1.position.clone().sub(p0.position).toArray(),
		]);
		azimuth.centerToTarget.position.copy(p0.position);
		azimuth.centerToTarget.geometry.verticesNeedUpdate = true;
		azimuth.centerToTarget.geometry.computeBoundingSphere();
		azimuth.centerToTarget.computeLineDistances();
		azimuth.centerToTarget.material.resolution.set(width, height);

		// to target ground
		azimuth.centerToTargetground.geometry.setPositions([
			0, 0, 0,
			p1.position.x - p0.position.x,
			p1.position.y - p0.position.y,
			0,
		]);
		azimuth.centerToTargetground.position.copy(p0.position);
		azimuth.centerToTargetground.geometry.verticesNeedUpdate = true;
		azimuth.centerToTargetground.geometry.computeBoundingSphere();
		azimuth.centerToTargetground.computeLineDistances();
		azimuth.centerToTargetground.material.resolution.set(width, height);

		// to north
		azimuth.centerToNorth.geometry.setPositions([
			0, 0, 0,
			northPos.x - p0.position.x,
			northPos.y - p0.position.y,
			0,
		]);
		azimuth.centerToNorth.position.copy(p0.position);
		azimuth.centerToNorth.geometry.verticesNeedUpdate = true;
		azimuth.centerToNorth.geometry.computeBoundingSphere();
		azimuth.centerToNorth.computeLineDistances();
		azimuth.centerToNorth.material.resolution.set(width, height);

		// label
		const radians = Utils.computeAzimuth(p0.position, p1.position, viewer.getProjection());
		let degrees = MathUtils.radToDeg(radians);
		if(degrees < 0){
			degrees = 360 + degrees;
		}
		const txtDegrees = `${degrees.toFixed(2)}°`;
		const labelDir = northPos.clone().add(p1.position).multiplyScalar(0.5).sub(p0.position);
		if(labelDir.length() > 0){
			labelDir.z = 0;
			labelDir.normalize();
			const labelVec = labelDir.clone().multiplyScalar(r);
			const labelPos = p0.position.clone().add(labelVec);
			azimuth.label.position.copy(labelPos);
		}
		azimuth.label.setText(txtDegrees);
		let distance = azimuth.label.position.distanceTo(camera.position);
		let pr = Utils.projectedRadius(1, camera, distance, width, height);
		let scale = (70 / pr);
		azimuth.label.scale.set(scale, scale, scale);
	}

	class MeasuringTool extends EventDispatcher{
		constructor (viewer) {
			super();

			this.viewer = viewer;
			this.renderer = viewer.renderer;

			this.addEventListener('start_inserting_measurement', e => {
				this.viewer.dispatchEvent({
					type: 'cancel_insertions'
				});
			});

			this.showLabels = true;
			this.scene = new Scene();
			this.scene.name = 'scene_measurement';
			this.light = new PointLight(0xffffff, 1.0);
			this.scene.add(this.light);

			this.viewer.inputHandler.registerInteractiveScene(this.scene);

			this.onRemove = (e) => { this.scene.remove(e.measurement);};
			this.onAdd = e => {this.scene.add(e.measurement);};

			for(let measurement of viewer.scene.measurements){
				this.onAdd({measurement: measurement});
			}

			viewer.addEventListener("update", this.update.bind(this));
			viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this));
			viewer.addEventListener("scene_changed", this.onSceneChange.bind(this));

			viewer.scene.addEventListener('measurement_added', this.onAdd);
			viewer.scene.addEventListener('measurement_removed', this.onRemove);
		}

		onSceneChange(e){
			if(e.oldScene){
				e.oldScene.removeEventListener('measurement_added', this.onAdd);
				e.oldScene.removeEventListener('measurement_removed', this.onRemove);
			}

			e.scene.addEventListener('measurement_added', this.onAdd);
			e.scene.addEventListener('measurement_removed', this.onRemove);
		}

		startInsertion (args = {}) {
			let domElement = this.viewer.renderer.domElement;

			let measure = new Measure();

			this.dispatchEvent({
				type: 'start_inserting_measurement',
				measure: measure
			});

			const pick = (defaul, alternative) => {
				if(defaul != null){
					return defaul;
				}else {
					return alternative;
				}
			};

			measure.showDistances = (args.showDistances === null) ? true : args.showDistances;

			measure.showArea = pick(args.showArea, false);
			measure.showAngles = pick(args.showAngles, false);
			measure.showCoordinates = pick(args.showCoordinates, false);
			measure.showHeight = pick(args.showHeight, false);
			measure.showCircle = pick(args.showCircle, false);
			measure.showAzimuth = pick(args.showAzimuth, false);
			measure.showEdges = pick(args.showEdges, true);
			measure.closed = pick(args.closed, false);
			measure.maxMarkers = pick(args.maxMarkers, Infinity);

			measure.name = args.name || 'Measurement';

			this.scene.add(measure);

			let cancel = {
				removeLastMarker: measure.maxMarkers > 3,
				callback: null
			};

			let insertionCallback = (e) => {
				if (e.button === MOUSE.LEFT) {
					measure.addMarker(measure.points[measure.points.length - 1].position.clone());

					if (measure.points.length >= measure.maxMarkers) {
						cancel.callback();
					}

					this.viewer.inputHandler.startDragging(
						measure.spheres[measure.spheres.length - 1]);
				} else if (e.button === MOUSE.RIGHT) {
					cancel.callback();
				}
			};

			cancel.callback = e => {
				if (cancel.removeLastMarker) {
					measure.removeMarker(measure.points.length - 1);
				}
				domElement.removeEventListener('mouseup', insertionCallback, true);
				this.viewer.removeEventListener('cancel_insertions', cancel.callback);
			};

			if (measure.maxMarkers > 1) {
				this.viewer.addEventListener('cancel_insertions', cancel.callback);
				domElement.addEventListener('mouseup', insertionCallback, true);
			}

			measure.addMarker(new Vector3(0, 0, 0));
			this.viewer.inputHandler.startDragging(
				measure.spheres[measure.spheres.length - 1]);

			this.viewer.scene.addMeasurement(measure);

			return measure;
		}
		
		update(){
			let camera = this.viewer.scene.getActiveCamera();
			let domElement = this.renderer.domElement;
			let measurements = this.viewer.scene.measurements;

			const renderAreaSize = this.renderer.getSize(new Vector2());
			let clientWidth = renderAreaSize.width;
			let clientHeight = renderAreaSize.height;

			this.light.position.copy(camera.position);

			// make size independant of distance
			for (let measure of measurements) {
				measure.lengthUnit = this.viewer.lengthUnit;
				measure.lengthUnitDisplay = this.viewer.lengthUnitDisplay;
				measure.update();

				updateAzimuth(this.viewer, measure);

				// spheres
				for(let sphere of measure.spheres){
					let distance = camera.position.distanceTo(sphere.getWorldPosition(new Vector3()));
					let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
					let scale = (15 / pr);
					sphere.scale.set(scale, scale, scale);
				}

				// labels
				let labels = measure.edgeLabels.concat(measure.angleLabels);
				for(let label of labels){
					let distance = camera.position.distanceTo(label.getWorldPosition(new Vector3()));
					let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
					let scale = (70 / pr);

					if(Potree.debug.scale){
						scale = (Potree.debug.scale / pr);
					}

					label.scale.set(scale, scale, scale);
				}

				// coordinate labels
				for (let j = 0; j < measure.coordinateLabels.length; j++) {
					let label = measure.coordinateLabels[j];
					let sphere = measure.spheres[j];

					let distance = camera.position.distanceTo(sphere.getWorldPosition(new Vector3()));

					let screenPos = sphere.getWorldPosition(new Vector3()).clone().project(camera);
					screenPos.x = Math.round((screenPos.x + 1) * clientWidth / 2);
					screenPos.y = Math.round((-screenPos.y + 1) * clientHeight / 2);
					screenPos.z = 0;
					screenPos.y -= 30;

					let labelPos = new Vector3( 
						(screenPos.x / clientWidth) * 2 - 1, 
						-(screenPos.y / clientHeight) * 2 + 1, 
						0.5 );
					labelPos.unproject(camera);
					if(this.viewer.scene.cameraMode == CameraMode.PERSPECTIVE) {
						let direction = labelPos.sub(camera.position).normalize();
						labelPos = new Vector3().addVectors(
							camera.position, direction.multiplyScalar(distance));

					}
					label.position.copy(labelPos);
					let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
					let scale = (70 / pr);
					label.scale.set(scale, scale, scale);
				}

				// height label
				if (measure.showHeight) {
					let label = measure.heightLabel;

					{
						let distance = label.position.distanceTo(camera.position);
						let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
						let scale = (70 / pr);
						label.scale.set(scale, scale, scale);
					}

					{ // height edge
						let edge = measure.heightEdge;

						let sorted = measure.points.slice().sort((a, b) => a.position.z - b.position.z);
						let lowPoint = sorted[0].position.clone();
						let highPoint = sorted[sorted.length - 1].position.clone();
						let min = lowPoint.z;
						let max = highPoint.z;

						let start = new Vector3(highPoint.x, highPoint.y, min);
						let end = new Vector3(highPoint.x, highPoint.y, max);

						let lowScreen = lowPoint.clone().project(camera);
						let startScreen = start.clone().project(camera);
						let endScreen = end.clone().project(camera);

						let toPixelCoordinates = v => {
							let r = v.clone().addScalar(1).divideScalar(2);
							r.x = r.x * clientWidth;
							r.y = r.y * clientHeight;
							r.z = 0;

							return r;
						};

						let lowEL = toPixelCoordinates(lowScreen);
						let startEL = toPixelCoordinates(startScreen);
						let endEL = toPixelCoordinates(endScreen);

						let lToS = lowEL.distanceTo(startEL);
						let sToE = startEL.distanceTo(endEL);

						edge.geometry.lineDistances = [0, lToS, lToS, lToS + sToE];
						edge.geometry.lineDistancesNeedUpdate = true;

						edge.material.dashSize = 10;
						edge.material.gapSize = 10;
					}
				}

				{ // area label
					let label = measure.areaLabel;
					let distance = label.position.distanceTo(camera.position);
					let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);

					let scale = (70 / pr);
					label.scale.set(scale, scale, scale);
				}

				{ // radius label
					let label = measure.circleRadiusLabel;
					let distance = label.position.distanceTo(camera.position);
					let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);

					let scale = (70 / pr);
					label.scale.set(scale, scale, scale);
				}

				{ // edges
					const materials = [
						measure.circleRadiusLine.material,
						...measure.edges.map( (e) => e.material),
						measure.heightEdge.material,
						measure.circleLine.material,
					];

					for(const material of materials){
						material.resolution.set(clientWidth, clientHeight);
					}
				}

				if(!this.showLabels){

					const labels = [
						...measure.sphereLabels, 
						...measure.edgeLabels, 
						...measure.angleLabels, 
						...measure.coordinateLabels,
						measure.heightLabel,
						measure.areaLabel,
						measure.circleRadiusLabel,
					];

					for(const label of labels){
						label.visible = false;
					}
				}
			}
		}

		render(){
			this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera());
		}
	};

	class Message{

		constructor(content){
			this.content = content;

			let closeIcon = `${exports.resourcePath}/icons/close.svg`;

			this.element = $(`
			<div class="potree_message">
				<span name="content_container" style="flex-grow: 1; padding: 5px"></span>
				<img name="close" src="${closeIcon}" class="button-icon" style="width: 16px; height: 16px;">
			</div>`);

			this.elClose = this.element.find("img[name=close]");

			this.elContainer = this.element.find("span[name=content_container]");

			if(typeof content === "string"){
				this.elContainer.append($(`<span>${content}</span>`));
			}else {
				this.elContainer.append(content);
			}

		}

		setMessage(content){
			this.elContainer.empty();
			if(typeof content === "string"){
				this.elContainer.append($(`<span>${content}</span>`));
			}else {
				this.elContainer.append(content);
			}
		}

	}

	class PointCloudSM{

		constructor(potreeRenderer){

			this.potreeRenderer = potreeRenderer;
			this.threeRenderer = this.potreeRenderer.threeRenderer;

			this.target = new WebGLRenderTarget(2 * 1024, 2 * 1024, {
				minFilter: LinearFilter,
				magFilter: LinearFilter,
				format: RGBAFormat,
				type: FloatType
			});
			this.target.depthTexture = new DepthTexture();
			this.target.depthTexture.type = UnsignedIntType;

			//this.threeRenderer.setClearColor(0x000000, 1);
			this.threeRenderer.setClearColor(0xff0000, 1);

			//HACK? removed while moving to three.js 109
			//this.threeRenderer.clearTarget(this.target, true, true, true); 
			{
				const oldTarget = this.threeRenderer.getRenderTarget();

				this.threeRenderer.setRenderTarget(this.target);
				this.threeRenderer.clear(true, true, true);

				this.threeRenderer.setRenderTarget(oldTarget);
			}
		}

		setLight(light){
			this.light = light;

			let fov = (180 * light.angle) / Math.PI;
			let aspect = light.shadow.mapSize.width / light.shadow.mapSize.height;
			let near = 0.1;
			let far = light.distance === 0 ? 10000 : light.distance;
			this.camera = new PerspectiveCamera(fov, aspect, near, far);
			this.camera.up.set(0, 0, 1);
			this.camera.position.copy(light.position);

			let target = new Vector3().subVectors(light.position, light.getWorldDirection(new Vector3()));
			this.camera.lookAt(target);

			this.camera.updateProjectionMatrix();
			this.camera.updateMatrix();
			this.camera.updateMatrixWorld();
			this.camera.matrixWorldInverse.copy(this.camera.matrixWorld).invert();
		}

		setSize(width, height){
			if(this.target.width !== width || this.target.height !== height){
				this.target.dispose();
			}
			this.target.setSize(width, height);
		}

		render(scene, camera){

			this.threeRenderer.setClearColor(0x000000, 1);
			
			const oldTarget = this.threeRenderer.getRenderTarget();

			this.threeRenderer.setRenderTarget(this.target);
			this.threeRenderer.clear(true, true, true);

			this.potreeRenderer.render(scene, this.camera, this.target, {});

			this.threeRenderer.setRenderTarget(oldTarget);
		}


	}

	class ProfileTool extends EventDispatcher {
		constructor (viewer) {
			super();

			this.viewer = viewer;
			this.renderer = viewer.renderer;

			this.addEventListener('start_inserting_profile', e => {
				this.viewer.dispatchEvent({
					type: 'cancel_insertions'
				});
			});

			this.scene = new Scene();
			this.scene.name = 'scene_profile';
			this.light = new PointLight(0xffffff, 1.0);
			this.scene.add(this.light);

			this.viewer.inputHandler.registerInteractiveScene(this.scene);

			this.onRemove = e => this.scene.remove(e.profile);
			this.onAdd = e => this.scene.add(e.profile);

			for(let profile of viewer.scene.profiles){
				this.onAdd({profile: profile});
			}

			viewer.addEventListener("update", this.update.bind(this));
			viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this));
			viewer.addEventListener("scene_changed", this.onSceneChange.bind(this));

			viewer.scene.addEventListener('profile_added', this.onAdd);
			viewer.scene.addEventListener('profile_removed', this.onRemove);
		}

		onSceneChange(e){
			if(e.oldScene){
				e.oldScene.removeEventListeners('profile_added', this.onAdd);
				e.oldScene.removeEventListeners('profile_removed', this.onRemove);
			}

			e.scene.addEventListener('profile_added', this.onAdd);
			e.scene.addEventListener('profile_removed', this.onRemove);
		}

		startInsertion (args = {}) {
			let domElement = this.viewer.renderer.domElement;

			let profile = new Profile();
			profile.name = args.name || 'Profile';

			this.dispatchEvent({
				type: 'start_inserting_profile',
				profile: profile
			});

			this.scene.add(profile);

			let cancel = {
				callback: null
			};

			let insertionCallback = (e) => {
				if(e.button === MOUSE.LEFT){
					if(profile.points.length <= 1){
						let camera = this.viewer.scene.getActiveCamera();
						let distance = camera.position.distanceTo(profile.points[0]);
						let clientSize = this.viewer.renderer.getSize(new Vector2());
						let pr = Utils.projectedRadius(1, camera, distance, clientSize.width, clientSize.height);
						let width = (10 / pr);

						profile.setWidth(width);
					}

					profile.addMarker(profile.points[profile.points.length - 1].clone());

					this.viewer.inputHandler.startDragging(
						profile.spheres[profile.spheres.length - 1]);
				} else if (e.button === MOUSE.RIGHT) {
					cancel.callback();
				}
			};

			cancel.callback = e => {
				profile.removeMarker(profile.points.length - 1);
				domElement.removeEventListener('mouseup', insertionCallback, true);
				this.viewer.removeEventListener('cancel_insertions', cancel.callback);
			};

			this.viewer.addEventListener('cancel_insertions', cancel.callback);
			domElement.addEventListener('mouseup', insertionCallback, true);

			profile.addMarker(new Vector3(0, 0, 0));
			this.viewer.inputHandler.startDragging(
				profile.spheres[profile.spheres.length - 1]);

			this.viewer.scene.addProfile(profile);

			return profile;
		}
		
		update(){
			let camera = this.viewer.scene.getActiveCamera();
			let profiles = this.viewer.scene.profiles;
			let renderAreaSize = this.viewer.renderer.getSize(new Vector2());
			let clientWidth = renderAreaSize.width;
			let clientHeight = renderAreaSize.height;

			this.light.position.copy(camera.position);

			// make size independant of distance
			for(let profile of profiles){
				for(let sphere of profile.spheres){				
					let distance = camera.position.distanceTo(sphere.getWorldPosition(new Vector3()));
					let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);
					let scale = (15 / pr);
					sphere.scale.set(scale, scale, scale);
				}
			}
		}

		render(){
			this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera());
		}

	}

	class ScreenBoxSelectTool extends EventDispatcher{

		constructor(viewer){
			super();

			this.viewer = viewer;
			this.scene = new Scene();

			viewer.addEventListener("update", this.update.bind(this));
			viewer.addEventListener("render.pass.perspective_overlay", this.render.bind(this));
			viewer.addEventListener("scene_changed", this.onSceneChange.bind(this));
		}

		onSceneChange(scene){
			console.log("scene changed");
		}

		startInsertion(){
			let domElement = this.viewer.renderer.domElement;

			let volume = new BoxVolume();
			volume.position.set(12345, 12345, 12345);
			volume.showVolumeLabel = false;
			volume.visible = false;
			volume.update();
			this.viewer.scene.addVolume(volume);

			this.importance = 10;

			let selectionBox = $(`<div style="position: absolute; border: 2px solid white; pointer-events: none; border-style:dashed"></div>`);
			$(domElement.parentElement).append(selectionBox);
			selectionBox.css("right", "10px");
			selectionBox.css("bottom", "10px");

			let drag = e =>{

				volume.visible = true;

				let mStart = e.drag.start;
				let mEnd = e.drag.end;

				let box2D = new Box2();
				box2D.expandByPoint(mStart);
				box2D.expandByPoint(mEnd);

				selectionBox.css("left", `${box2D.min.x}px`);
				selectionBox.css("top", `${box2D.min.y}px`);
				selectionBox.css("width", `${box2D.max.x - box2D.min.x}px`);
				selectionBox.css("height", `${box2D.max.y - box2D.min.y}px`);

				let camera = e.viewer.scene.getActiveCamera();
				let size = e.viewer.renderer.getSize(new Vector2());
				let frustumSize = new Vector2(
					camera.right - camera.left, 
					camera.top - camera.bottom);

				let screenCentroid = new Vector2().addVectors(e.drag.end, e.drag.start).multiplyScalar(0.5);
				let ray = Utils.mouseToRay(screenCentroid, camera, size.width, size.height);

				let diff = new Vector2().subVectors(e.drag.end, e.drag.start);
				diff.divide(size).multiply(frustumSize);
				
				volume.position.copy(ray.origin);
				volume.up.copy(camera.up);
				volume.rotation.copy(camera.rotation);
				volume.scale.set(diff.x, diff.y, 1000 * 100);

				e.consume();
			};

			let drop = e => {
				this.importance = 0;

				$(selectionBox).remove();

				this.viewer.inputHandler.deselectAll();
				this.viewer.inputHandler.toggleSelection(volume);

				let camera = e.viewer.scene.getActiveCamera();
				let size = e.viewer.renderer.getSize(new Vector2());
				let screenCentroid = new Vector2().addVectors(e.drag.end, e.drag.start).multiplyScalar(0.5);
				let ray = Utils.mouseToRay(screenCentroid, camera, size.width, size.height);

				let line = new Line3(ray.origin, new Vector3().addVectors(ray.origin, ray.direction));

				this.removeEventListener("drag", drag);
				this.removeEventListener("drop", drop);

				let allPointsNear = [];
				let allPointsFar = [];

				// TODO support more than one point cloud
				for(let pointcloud of this.viewer.scene.pointclouds){

					if(!pointcloud.visible){
						continue;
					}

					let volCam = camera.clone();
					volCam.left = -volume.scale.x / 2; 
					volCam.right = +volume.scale.x / 2;
					volCam.top = +volume.scale.y / 2;
					volCam.bottom = -volume.scale.y / 2;
					volCam.near = -volume.scale.z / 2;
					volCam.far = +volume.scale.z / 2;
					volCam.rotation.copy(volume.rotation);
					volCam.position.copy(volume.position);

					volCam.updateMatrix();
					volCam.updateMatrixWorld();
					volCam.updateProjectionMatrix();
					volCam.matrixWorldInverse.copy(volCam.matrixWorld).invert();

					let ray = new Ray(volCam.getWorldPosition(new Vector3()), volCam.getWorldDirection(new Vector3()));
					let rayInverse = new Ray(
						ray.origin.clone().add(ray.direction.clone().multiplyScalar(volume.scale.z)),
						ray.direction.clone().multiplyScalar(-1));

					let pickerSettings = {
						width: 8, 
						height: 8, 
						pickWindowSize: 8, 
						all: true,
						pickClipped: true,
						pointSizeType: PointSizeType.FIXED,
						pointSize: 1};
					let pointsNear = pointcloud.pick(viewer, volCam, ray, pickerSettings);

					volCam.rotateX(Math.PI);
					volCam.updateMatrix();
					volCam.updateMatrixWorld();
					volCam.updateProjectionMatrix();
					volCam.matrixWorldInverse.copy(volCam.matrixWorld).invert();
					let pointsFar = pointcloud.pick(viewer, volCam, rayInverse, pickerSettings);

					allPointsNear.push(...pointsNear);
					allPointsFar.push(...pointsFar);
				}

				if(allPointsNear.length > 0 && allPointsFar.length > 0){
					let viewLine = new Line3(ray.origin, new Vector3().addVectors(ray.origin, ray.direction));

					let closestOnLine = allPointsNear.map(p => viewLine.closestPointToPoint(p.position, false, new Vector3()));
					let closest = closestOnLine.sort( (a, b) => ray.origin.distanceTo(a) - ray.origin.distanceTo(b))[0];

					let farthestOnLine = allPointsFar.map(p => viewLine.closestPointToPoint(p.position, false, new Vector3()));
					let farthest = farthestOnLine.sort( (a, b) => ray.origin.distanceTo(b) - ray.origin.distanceTo(a))[0];

					let distance = closest.distanceTo(farthest);
					let centroid = new Vector3().addVectors(closest, farthest).multiplyScalar(0.5);
					volume.scale.z = distance * 1.1;
					volume.position.copy(centroid);
				}

				volume.clip = true;
			};

			this.addEventListener("drag", drag);
			this.addEventListener("drop", drop);

			viewer.inputHandler.addInputListener(this);

			return volume;
		}

		update(e){
			//console.log(e.delta)
		}

		render(){
			this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera());
		}

	}

	class SpotLightHelper$1 extends Object3D{

		constructor(light, color){
			super();

			this.light = light;
			this.color = color;

			//this.up.set(0, 0, 1);
			this.updateMatrix();
			this.updateMatrixWorld();

			{ // SPHERE
				let sg = new SphereGeometry(1, 32, 32);
				let sm = new MeshNormalMaterial();
				this.sphere = new Mesh(sg, sm);
				this.sphere.scale.set(0.5, 0.5, 0.5);
				this.add(this.sphere);
			}

			{ // LINES
				

				let positions = new Float32Array([
					+0, +0, +0,     +0, +0, -1,

					+0, +0, +0,     -1, -1, -1,
					+0, +0, +0,     +1, -1, -1,
					+0, +0, +0,     +1, +1, -1,
					+0, +0, +0,     -1, +1, -1,

					-1, -1, -1,     +1, -1, -1,
					+1, -1, -1,     +1, +1, -1,
					+1, +1, -1,     -1, +1, -1,
					-1, +1, -1,     -1, -1, -1,
				]);

				let geometry = new BufferGeometry();
				geometry.setAttribute("position", new BufferAttribute(positions, 3));

				let material = new LineBasicMaterial();

				this.frustum = new LineSegments(geometry, material);
				this.add(this.frustum);

			}

			this.update();
		}

		update(){

			this.light.updateMatrix();
			this.light.updateMatrixWorld();

			let position = this.light.position;
			let target = new Vector3().addVectors(
				this.light.position, this.light.getWorldDirection(new Vector3()).multiplyScalar(-1));
			
			let quat = new Quaternion().setFromRotationMatrix(
				new Matrix4().lookAt( position, target, new Vector3( 0, 0, 1 ) )
			);

			this.setRotationFromQuaternion(quat);
			this.position.copy(position);


			let coneLength = (this.light.distance > 0) ? this.light.distance : 1000;
			let coneWidth = coneLength * Math.tan( this.light.angle * 0.5 );

			this.frustum.scale.set(coneWidth, coneWidth, coneLength);

		}

	}

	class TransformationTool {
		constructor(viewer) {
			this.viewer = viewer;

			this.scene = new Scene();

			this.selection = [];
			this.pivot = new Vector3();
			this.dragging = false;
			this.showPickVolumes = false;

			this.viewer.inputHandler.registerInteractiveScene(this.scene);
			this.viewer.inputHandler.addEventListener('selection_changed', (e) => {
				for(let selected of this.selection){
					this.viewer.inputHandler.blacklist.delete(selected);
				}

				this.selection = e.selection;

				for(let selected of this.selection){
					this.viewer.inputHandler.blacklist.add(selected);
				}

			});

			let red = 0xE73100;
			let green = 0x44A24A;
			let blue = 0x2669E7;
			
			this.activeHandle = null;
			this.scaleHandles = {
				"scale.x+": {name: "scale.x+", node: new Object3D(), color: red, alignment: [+1, +0, +0]},
				"scale.x-": {name: "scale.x-", node: new Object3D(), color: red, alignment: [-1, +0, +0]},
				"scale.y+": {name: "scale.y+", node: new Object3D(), color: green, alignment: [+0, +1, +0]},
				"scale.y-": {name: "scale.y-", node: new Object3D(), color: green, alignment: [+0, -1, +0]},
				"scale.z+": {name: "scale.z+", node: new Object3D(), color: blue, alignment: [+0, +0, +1]},
				"scale.z-": {name: "scale.z-", node: new Object3D(), color: blue, alignment: [+0, +0, -1]},
			};
			this.focusHandles = {
				"focus.x+": {name: "focus.x+", node:  new Object3D(), color: red, alignment: [+1, +0, +0]},
				"focus.x-": {name: "focus.x-", node:  new Object3D(), color: red, alignment: [-1, +0, +0]},
				"focus.y+": {name: "focus.y+", node:  new Object3D(), color: green, alignment: [+0, +1, +0]},
				"focus.y-": {name: "focus.y-", node:  new Object3D(), color: green, alignment: [+0, -1, +0]},
				"focus.z+": {name: "focus.z+", node:  new Object3D(), color: blue, alignment: [+0, +0, +1]},
				"focus.z-": {name: "focus.z-", node:  new Object3D(), color: blue, alignment: [+0, +0, -1]},
			};
			this.translationHandles = {
				"translation.x": {name: "translation.x", node:  new Object3D(), color: red, alignment: [1, 0, 0]},
				"translation.y": {name: "translation.y", node:  new Object3D(), color: green, alignment: [0, 1, 0]},
				"translation.z": {name: "translation.z", node:  new Object3D(), color: blue, alignment: [0, 0, 1]},
			};
			this.rotationHandles = {
				"rotation.x": {name: "rotation.x", node:  new Object3D(), color: red, alignment: [1, 0, 0]},
				"rotation.y": {name: "rotation.y", node:  new Object3D(), color: green, alignment: [0, 1, 0]},
				"rotation.z": {name: "rotation.z", node:  new Object3D(), color: blue, alignment: [0, 0, 1]},
			};
			this.handles = Object.assign({}, this.scaleHandles, this.focusHandles, this.translationHandles, this.rotationHandles);
			this.pickVolumes = [];

			this.initializeScaleHandles();
			this.initializeFocusHandles();
			this.initializeTranslationHandles();
			this.initializeRotationHandles();


			let boxFrameGeometry = new Geometry();
			{
				// bottom
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5));
				// top
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5));
				// sides
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, 0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(0.5, 0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, -0.5, -0.5));
				boxFrameGeometry.vertices.push(new Vector3(-0.5, 0.5, -0.5));
			}
			this.frame = new LineSegments(boxFrameGeometry, new LineBasicMaterial({color: 0xffff00}));
			this.scene.add(this.frame);

			
		}

		initializeScaleHandles(){
			let sgSphere = new SphereGeometry(1, 32, 32);
			let sgLowPolySphere = new SphereGeometry(1, 16, 16);

			for(let handleName of Object.keys(this.scaleHandles)){
				let handle = this.scaleHandles[handleName];
				let node = handle.node;
				this.scene.add(node);
				node.position.set(...handle.alignment).multiplyScalar(0.5);

				let material = new MeshBasicMaterial({
					color: handle.color,
					opacity: 0.4,
					transparent: true
					});

				let outlineMaterial = new MeshBasicMaterial({
					color: 0x000000, 
					side: BackSide,
					opacity: 0.4,
					transparent: true});

				let pickMaterial = new MeshNormalMaterial({
					opacity: 0.2,
					transparent: true,
					visible: this.showPickVolumes});

				let sphere = new Mesh(sgSphere, material);
				sphere.scale.set(1.3, 1.3, 1.3);
				sphere.name = `${handleName}.handle`;
				node.add(sphere);
				
				let outline = new Mesh(sgSphere, outlineMaterial);
				outline.scale.set(1.4, 1.4, 1.4);
				outline.name = `${handleName}.outline`;
				sphere.add(outline);

				let pickSphere = new Mesh(sgLowPolySphere, pickMaterial);
				pickSphere.name = `${handleName}.pick_volume`;
				pickSphere.scale.set(3, 3, 3);
				sphere.add(pickSphere);
				pickSphere.handle = handleName;
				this.pickVolumes.push(pickSphere);

				node.setOpacity = (target) => {
					let opacity = {x: material.opacity};
					let t = new TWEEN.Tween(opacity).to({x: target}, 100);
					t.onUpdate(() => {
						sphere.visible = opacity.x > 0;
						pickSphere.visible = opacity.x > 0;
						material.opacity = opacity.x;
						outlineMaterial.opacity = opacity.x;
						pickSphere.material.opacity = opacity.x * 0.5;
					});
					t.start();
				};

				pickSphere.addEventListener("drag", (e) => this.dragScaleHandle(e));
				pickSphere.addEventListener("drop", (e) => this.dropScaleHandle(e));

				pickSphere.addEventListener("mouseover", e => {
					//node.setOpacity(1);
				});

				pickSphere.addEventListener("click", e => {
					e.consume();
				});

				pickSphere.addEventListener("mouseleave", e => {
					//node.setOpacity(0.4);
				});
			}
		}

		initializeFocusHandles(){
			//let sgBox = new THREE.BoxGeometry(1, 1, 1);
			let sgPlane = new PlaneGeometry(4, 4, 1, 1);
			let sgLowPolySphere = new SphereGeometry(1, 16, 16);

			let texture = new TextureLoader().load(`${exports.resourcePath}/icons/eye_2.png`);

			for(let handleName of Object.keys(this.focusHandles)){
				let handle = this.focusHandles[handleName];
				let node = handle.node;
				this.scene.add(node);
				let align = handle.alignment;

				//node.lookAt(new THREE.Vector3().addVectors(node.position, new THREE.Vector3(...align)));
				node.lookAt(new Vector3(...align));

				let off = 0.8;
				if(align[0] === 1){
					node.position.set(1, off, -off).multiplyScalar(0.5);
					node.rotation.z = Math.PI / 2;
				}else if(align[0] === -1){
					node.position.set(-1, -off, -off).multiplyScalar(0.5);
					node.rotation.z = Math.PI / 2;
				}else if(align[1] === 1){
					node.position.set(-off, 1, -off).multiplyScalar(0.5);
					node.rotation.set(Math.PI / 2, Math.PI, 0.0);
				}else if(align[1] === -1){
					node.position.set(off, -1, -off).multiplyScalar(0.5);
					node.rotation.set(Math.PI / 2, 0.0, 0.0);
				}else if(align[2] === 1){
					node.position.set(off, off, 1).multiplyScalar(0.5);
				}else if(align[2] === -1){
					node.position.set(-off, off, -1).multiplyScalar(0.5);
				}

				let material = new MeshBasicMaterial({
					color: handle.color,
					opacity: 0,
					transparent: true,
					map: texture
				});

				//let outlineMaterial = new THREE.MeshBasicMaterial({
				//	color: 0x000000, 
				//	side: THREE.BackSide,
				//	opacity: 0,
				//	transparent: true});

				let pickMaterial = new MeshNormalMaterial({
					//opacity: 0,
					transparent: true,
					visible: this.showPickVolumes});

				let box = new Mesh(sgPlane, material);
				box.name = `${handleName}.handle`;
				box.scale.set(1.5, 1.5, 1.5);
				box.position.set(0, 0, 0);
				box.visible = false;
				node.add(box);
				//handle.focusNode = box;
				
				//let outline = new THREE.Mesh(sgPlane, outlineMaterial);
				//outline.scale.set(1.4, 1.4, 1.4);
				//outline.name = `${handleName}.outline`;
				//box.add(outline);

				let pickSphere = new Mesh(sgLowPolySphere, pickMaterial);
				pickSphere.name = `${handleName}.pick_volume`;
				pickSphere.scale.set(3, 3, 3);
				box.add(pickSphere);
				pickSphere.handle = handleName;
				this.pickVolumes.push(pickSphere);

				node.setOpacity = (target) => {
					let opacity = {x: material.opacity};
					let t = new TWEEN.Tween(opacity).to({x: target}, 100);
					t.onUpdate(() => {
						pickSphere.visible = opacity.x > 0;
						box.visible = opacity.x > 0;
						material.opacity = opacity.x;
						//outlineMaterial.opacity = opacity.x;
						pickSphere.material.opacity = opacity.x * 0.5;
					});
					t.start();
				};

				pickSphere.addEventListener("drag", e => {});

				pickSphere.addEventListener("mouseup", e => {
					e.consume();
				});

				pickSphere.addEventListener("mousedown", e => {
					e.consume();
				});

				pickSphere.addEventListener("click", e => {
					e.consume();

					let selected = this.selection[0];
					let maxScale = Math.max(...selected.scale.toArray());
					let minScale = Math.min(...selected.scale.toArray());
					let handleLength = Math.abs(selected.scale.dot(new Vector3(...handle.alignment)));
					let alignment = new Vector3(...handle.alignment).multiplyScalar(2 * maxScale / handleLength);
					alignment.applyMatrix4(selected.matrixWorld);
					let newCamPos = alignment;
					let newCamTarget = selected.getWorldPosition(new Vector3());

					Utils.moveTo(this.viewer.scene, newCamPos, newCamTarget);
				});

				pickSphere.addEventListener("mouseover", e => {
					//box.setOpacity(1);
				});

				pickSphere.addEventListener("mouseleave", e => {
					//box.setOpacity(0.4);
				});
			}
		}

		initializeTranslationHandles(){
			let boxGeometry = new BoxGeometry(1, 1, 1);

			for(let handleName of Object.keys(this.translationHandles)){
				let handle = this.handles[handleName];
				let node = handle.node;
				this.scene.add(node);

				let material = new MeshBasicMaterial({
					color: handle.color,
					opacity: 0.4,
					transparent: true});

				let outlineMaterial = new MeshBasicMaterial({
					color: 0x000000, 
					side: BackSide,
					opacity: 0.4,
					transparent: true});

				let pickMaterial = new MeshNormalMaterial({
					opacity: 0.2,
					transparent: true,
					visible: this.showPickVolumes
				});

				let box = new Mesh(boxGeometry, material);
				box.name = `${handleName}.handle`;
				box.scale.set(0.2, 0.2, 40);
				box.lookAt(new Vector3(...handle.alignment));
				box.renderOrder = 10;
				node.add(box);
				handle.translateNode = box;

				let outline = new Mesh(boxGeometry, outlineMaterial);
				outline.name = `${handleName}.outline`;
				outline.scale.set(3, 3, 1.03);
				outline.renderOrder = 0;
				box.add(outline);

				let pickVolume = new Mesh(boxGeometry, pickMaterial);
				pickVolume.name = `${handleName}.pick_volume`;
				pickVolume.scale.set(12, 12, 1.1);
				pickVolume.handle = handleName;
				box.add(pickVolume);
				this.pickVolumes.push(pickVolume);

				node.setOpacity = (target) => {
					let opacity = {x: material.opacity};
					let t = new TWEEN.Tween(opacity).to({x: target}, 100);
					t.onUpdate(() => {
						box.visible = opacity.x > 0;
						pickVolume.visible = opacity.x > 0;
						material.opacity = opacity.x;
						outlineMaterial.opacity = opacity.x;
						pickMaterial.opacity = opacity.x * 0.5;
					});
					t.start();
				};

				pickVolume.addEventListener("drag", (e) => {this.dragTranslationHandle(e);});
				pickVolume.addEventListener("drop", (e) => {this.dropTranslationHandle(e);});
			}
		}

		initializeRotationHandles(){
			let adjust = 0.5;
			let torusGeometry = new TorusGeometry(1, adjust * 0.015, 8, 64, Math.PI / 2);
			let outlineGeometry = new TorusGeometry(1, adjust * 0.04, 8, 64, Math.PI / 2);
			let pickGeometry = new TorusGeometry(1, adjust * 0.1, 6, 4, Math.PI / 2);

			for(let handleName of Object.keys(this.rotationHandles)){
				let handle = this.handles[handleName];
				let node = handle.node;
				this.scene.add(node);

				let material = new MeshBasicMaterial({
					color: handle.color,
					opacity: 0.4,
					transparent: true});

				let outlineMaterial = new MeshBasicMaterial({
					color: 0x000000, 
					side: BackSide,
					opacity: 0.4,
					transparent: true});

				let pickMaterial = new MeshNormalMaterial({
					opacity: 0.2,
					transparent: true,
					visible: this.showPickVolumes
				});

				let box = new Mesh(torusGeometry, material);
				box.name = `${handleName}.handle`;
				box.scale.set(20, 20, 20);
				box.lookAt(new Vector3(...handle.alignment));
				node.add(box);
				handle.translateNode = box;

				let outline = new Mesh(outlineGeometry, outlineMaterial);
				outline.name = `${handleName}.outline`;
				outline.scale.set(1, 1, 1);
				outline.renderOrder = 0;
				box.add(outline);

				let pickVolume = new Mesh(pickGeometry, pickMaterial);
				pickVolume.name = `${handleName}.pick_volume`;
				pickVolume.scale.set(1, 1, 1);
				pickVolume.handle = handleName;
				box.add(pickVolume);
				this.pickVolumes.push(pickVolume);

				node.setOpacity = (target) => {
					let opacity = {x: material.opacity};
					let t = new TWEEN.Tween(opacity).to({x: target}, 100);
					t.onUpdate(() => {
						box.visible = opacity.x > 0;
						pickVolume.visible = opacity.x > 0;
						material.opacity = opacity.x;
						outlineMaterial.opacity = opacity.x;
						pickMaterial.opacity = opacity.x * 0.5;
					});
					t.start();
				};


				//pickVolume.addEventListener("mouseover", (e) => {
				//	//let a = this.viewer.scene.getActiveCamera().getWorldDirection(new THREE.Vector3()).dot(pickVolume.getWorldDirection(new THREE.Vector3()));
				//	console.log(pickVolume.getWorldDirection(new THREE.Vector3()));
				//});
				
				pickVolume.addEventListener("drag", (e) => {this.dragRotationHandle(e);});
				pickVolume.addEventListener("drop", (e) => {this.dropRotationHandle(e);});
			}
		}

		dragRotationHandle(e){
			let drag = e.drag;
			let handle = this.activeHandle;
			let camera = this.viewer.scene.getActiveCamera();

			if(!handle){
				return
			};

			let localNormal = new Vector3(...handle.alignment);
			let n = new Vector3();
			n.copy(new Vector4(...localNormal.toArray(), 0).applyMatrix4(handle.node.matrixWorld));
			n.normalize();

			if (!drag.intersectionStart){

				//this.viewer.scene.scene.remove(this.debug);
				//this.debug = new THREE.Object3D();
				//this.viewer.scene.scene.add(this.debug);
				//Utils.debugSphere(this.debug, drag.location, 3, 0xaaaaaa);
				//let debugEnd = drag.location.clone().add(n.clone().multiplyScalar(20));
				//Utils.debugLine(this.debug, drag.location, debugEnd, 0xff0000);

				drag.intersectionStart = drag.location;
				drag.objectStart = drag.object.getWorldPosition(new Vector3());
				drag.handle = handle;

				let plane = new Plane().setFromNormalAndCoplanarPoint(n, drag.intersectionStart);

				drag.dragPlane = plane;
				drag.pivot = drag.intersectionStart;
			}else {
				handle = drag.handle;
			}

			this.dragging = true;

			let mouse = drag.end;
			let domElement = this.viewer.renderer.domElement;
			let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);
			
			let I = ray.intersectPlane(drag.dragPlane, new Vector3());

			if (I) {
				let center = this.scene.getWorldPosition(new Vector3());
				let from = drag.pivot;
				let to = I;

				let v1 = from.clone().sub(center).normalize();
				let v2 = to.clone().sub(center).normalize();

				let angle = Math.acos(v1.dot(v2));
				let sign = Math.sign(v1.cross(v2).dot(n));
				angle = angle * sign;
				if (Number.isNaN(angle)) {
					return;
				}

				let normal = new Vector3(...handle.alignment);
				for (let selection of this.selection) {
					selection.rotateOnAxis(normal, angle);
					selection.dispatchEvent({
						type: "orientation_changed",
						object: selection
					});
				}

				drag.pivot = I;
			}
		}

		dropRotationHandle(e){
			this.dragging = false;
			this.setActiveHandle(null);
		}

		dragTranslationHandle(e){
			let drag = e.drag;
			let handle = this.activeHandle;
			let camera = this.viewer.scene.getActiveCamera();
				
			if(!drag.intersectionStart && handle){
				drag.intersectionStart = drag.location;
				drag.objectStart = drag.object.getWorldPosition(new Vector3());

				let start = drag.intersectionStart;
				let dir = new Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld);
				let end = new Vector3().addVectors(start, dir);
				let line = new Line3(start.clone(), end.clone());
				drag.line = line;

				let camOnLine = line.closestPointToPoint(camera.position, false, new Vector3());
				let normal = new Vector3().subVectors(camera.position, camOnLine);
				let plane = new Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart);
				drag.dragPlane = plane;
				drag.pivot = drag.intersectionStart;
			}else {
				handle = drag.handle;
			}

			this.dragging = true;

			{
				let mouse = drag.end;
				let domElement = this.viewer.renderer.domElement;
				let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);
				let I = ray.intersectPlane(drag.dragPlane, new Vector3());

				if (I) {
					let iOnLine = drag.line.closestPointToPoint(I, false, new Vector3());

					let diff = new Vector3().subVectors(iOnLine, drag.pivot);

					for (let selection of this.selection) {
						selection.position.add(diff);
						selection.dispatchEvent({
							type: "position_changed",
							object: selection
						});
					}

					drag.pivot = drag.pivot.add(diff);
				}
			}
		}

		dropTranslationHandle(e){
			this.dragging = false;
			this.setActiveHandle(null);
		}

		dropScaleHandle(e){
			this.dragging = false;
			this.setActiveHandle(null);
		}

		dragScaleHandle(e){
			let drag = e.drag;
			let handle = this.activeHandle;
			let camera = this.viewer.scene.getActiveCamera();

			if(!drag.intersectionStart){
				drag.intersectionStart = drag.location;
				drag.objectStart = drag.object.getWorldPosition(new Vector3());
				drag.handle = handle;

				let start = drag.intersectionStart;
				let dir = new Vector4(...handle.alignment, 0).applyMatrix4(this.scene.matrixWorld);
				let end = new Vector3().addVectors(start, dir);
				let line = new Line3(start.clone(), end.clone());
				drag.line = line;

				let camOnLine = line.closestPointToPoint(camera.position, false, new Vector3());
				let normal = new Vector3().subVectors(camera.position, camOnLine);
				let plane = new Plane().setFromNormalAndCoplanarPoint(normal, drag.intersectionStart);
				drag.dragPlane = plane;
				drag.pivot = drag.intersectionStart;

				//Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05);
			}else {
				handle = drag.handle;
			}

			this.dragging = true;

			{
				let mouse = drag.end;
				let domElement = this.viewer.renderer.domElement;
				let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);
				let I = ray.intersectPlane(drag.dragPlane, new Vector3());

				if (I) {
					let iOnLine = drag.line.closestPointToPoint(I, false, new Vector3());
					let direction = handle.alignment.reduce( (a, v) => a + v, 0);

					let toObjectSpace = this.selection[0].matrixWorld.clone().invert();
					let iOnLineOS = iOnLine.clone().applyMatrix4(toObjectSpace);
					let pivotOS = drag.pivot.clone().applyMatrix4(toObjectSpace);
					let diffOS = new Vector3().subVectors(iOnLineOS, pivotOS);
					let dragDirectionOS = diffOS.clone().normalize();
					if(iOnLine.distanceTo(drag.pivot) === 0){
						dragDirectionOS.set(0, 0, 0);
					}
					let dragDirection = dragDirectionOS.dot(new Vector3(...handle.alignment));

					let diff = new Vector3().subVectors(iOnLine, drag.pivot);
					let diffScale = new Vector3(...handle.alignment).multiplyScalar(diff.length() * direction * dragDirection);
					let diffPosition = diff.clone().multiplyScalar(0.5);

					for (let selection of this.selection) {
						selection.scale.add(diffScale);
						selection.scale.x = Math.max(0.1, selection.scale.x);
						selection.scale.y = Math.max(0.1, selection.scale.y);
						selection.scale.z = Math.max(0.1, selection.scale.z);
						selection.position.add(diffPosition);
						selection.dispatchEvent({
							type: "position_changed",
							object: selection
						});
						selection.dispatchEvent({
							type: "scale_changed",
							object: selection
						});
					}

					drag.pivot.copy(iOnLine);
					//Utils.debugSphere(viewer.scene.scene, drag.pivot, 0.05);
				}
			}
		}

		setActiveHandle(handle){
			if(this.dragging){
				return;
			}

			if(this.activeHandle === handle){
				return;
			}

			this.activeHandle = handle;

			if(handle === null){
				for(let handleName of Object.keys(this.handles)){
					let handle = this.handles[handleName];
					handle.node.setOpacity(0);
				}
			}

			for(let handleName of Object.keys(this.focusHandles)){
				let handle = this.focusHandles[handleName];

				if(this.activeHandle === handle){
					handle.node.setOpacity(1.0);
				}else {
					handle.node.setOpacity(0.4);
				}
			}

			for(let handleName of Object.keys(this.translationHandles)){
				let handle = this.translationHandles[handleName];

				if(this.activeHandle === handle){
					handle.node.setOpacity(1.0);
				}else {
					handle.node.setOpacity(0.4);
				}
			}

			for(let handleName of Object.keys(this.rotationHandles)){
				let handle = this.rotationHandles[handleName];

				//if(this.activeHandle === handle){
				//	handle.node.setOpacity(1.0);
				//}else{
				//	handle.node.setOpacity(0.4)
				//}

				handle.node.setOpacity(0.4);
			}

			for(let handleName of Object.keys(this.scaleHandles)){
				let handle = this.scaleHandles[handleName];

				if(this.activeHandle === handle){
					handle.node.setOpacity(1.0);

					let relatedFocusHandle = this.focusHandles[handle.name.replace("scale", "focus")];
					let relatedFocusNode = relatedFocusHandle.node;
					relatedFocusNode.setOpacity(0.4);

					for(let translationHandleName of Object.keys(this.translationHandles)){
						let translationHandle = this.translationHandles[translationHandleName];
						translationHandle.node.setOpacity(0.4);
					}

					//let relatedTranslationHandle = this.translationHandles[
					//	handle.name.replace("scale", "translation").replace(/[+-]/g, "")];
					//let relatedTranslationNode = relatedTranslationHandle.node;
					//relatedTranslationNode.setOpacity(0.4);


				}else {
					handle.node.setOpacity(0.4);
				}
			}

			



			if(handle){
				handle.node.setOpacity(1.0);
			}

			
		}

		update () {

			if(this.selection.length === 1){

				this.scene.visible = true;

				this.scene.updateMatrix();
				this.scene.updateMatrixWorld();

				let selected = this.selection[0];
				let world = selected.matrixWorld;
				let camera = this.viewer.scene.getActiveCamera();
				let domElement = this.viewer.renderer.domElement;
				let mouse = this.viewer.inputHandler.mouse;

				let center = selected.boundingBox.getCenter(new Vector3()).clone().applyMatrix4(selected.matrixWorld);

				this.scene.scale.copy(selected.boundingBox.getSize(new Vector3()).multiply(selected.scale));
				this.scene.position.copy(center);
				this.scene.rotation.copy(selected.rotation);

				this.scene.updateMatrixWorld();

				{
					// adjust scale of components
					for(let handleName of Object.keys(this.handles)){
						let handle = this.handles[handleName];
						let node = handle.node;

						let handlePos = node.getWorldPosition(new Vector3());
						let distance = handlePos.distanceTo(camera.position);
						let pr = Utils.projectedRadius(1, camera, distance, domElement.clientWidth, domElement.clientHeight);

						let ws = node.parent.getWorldScale(new Vector3());

						let s = (7 / pr);
						let scale = new Vector3(s, s, s).divide(ws);

						let rot = new Matrix4().makeRotationFromEuler(node.rotation);
						let rotInv = rot.clone().invert();

						scale.applyMatrix4(rotInv);
						scale.x = Math.abs(scale.x);
						scale.y = Math.abs(scale.y);
						scale.z = Math.abs(scale.z);

						node.scale.copy(scale);
					}

					// adjust rotation handles
					if(!this.dragging){
						let tWorld = this.scene.matrixWorld;
						let tObject = tWorld.clone().invert();
						let camObjectPos = camera.getWorldPosition(new Vector3()).applyMatrix4(tObject);

						let x = this.rotationHandles["rotation.x"].node.rotation;
						let y = this.rotationHandles["rotation.y"].node.rotation;
						let z = this.rotationHandles["rotation.z"].node.rotation;

						x.order = "ZYX";
						y.order = "ZYX";

						let above = camObjectPos.z > 0;
						let below = !above;
						let PI_HALF = Math.PI / 2;

						if(above){
							if(camObjectPos.x > 0 && camObjectPos.y > 0){
								x.x = 1 * PI_HALF;
								y.y = 3 * PI_HALF;
								z.z = 0 * PI_HALF;
							}else if(camObjectPos.x < 0 && camObjectPos.y > 0){
								x.x = 1 * PI_HALF;
								y.y = 2 * PI_HALF;
								z.z = 1 * PI_HALF;
							}else if(camObjectPos.x < 0 && camObjectPos.y < 0){
								x.x = 2 * PI_HALF;
								y.y = 2 * PI_HALF;
								z.z = 2 * PI_HALF;
							}else if(camObjectPos.x > 0 && camObjectPos.y < 0){
								x.x = 2 * PI_HALF;
								y.y = 3 * PI_HALF;
								z.z = 3 * PI_HALF;
							}
						}else if(below){
							if(camObjectPos.x > 0 && camObjectPos.y > 0){
								x.x = 0 * PI_HALF;
								y.y = 0 * PI_HALF;
								z.z = 0 * PI_HALF;
							}else if(camObjectPos.x < 0 && camObjectPos.y > 0){
								x.x = 0 * PI_HALF;
								y.y = 1 * PI_HALF;
								z.z = 1 * PI_HALF;
							}else if(camObjectPos.x < 0 && camObjectPos.y < 0){
								x.x = 3 * PI_HALF;
								y.y = 1 * PI_HALF;
								z.z = 2 * PI_HALF;
							}else if(camObjectPos.x > 0 && camObjectPos.y < 0){
								x.x = 3 * PI_HALF;
								y.y = 0 * PI_HALF;
								z.z = 3 * PI_HALF;
							}
						}
					}

					{
						let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);
						let raycaster = new Raycaster(ray.origin, ray.direction);
						let intersects = raycaster.intersectObjects(this.pickVolumes.filter(v => v.visible), true);

						if(intersects.length > 0){
							let I = intersects[0];
							let handleName = I.object.handle;
							this.setActiveHandle(this.handles[handleName]);
						}else {
							this.setActiveHandle(null);
						}
					}

					// 
					for(let handleName of Object.keys(this.scaleHandles)){
						let handle = this.handles[handleName];
						let node = handle.node;
						let alignment = handle.alignment;

						

					}
				}

			}else {
				this.scene.visible = false;
			}
			
		}

	};

	class VolumeTool extends EventDispatcher{
		constructor (viewer) {
			super();

			this.viewer = viewer;
			this.renderer = viewer.renderer;

			this.addEventListener('start_inserting_volume', e => {
				this.viewer.dispatchEvent({
					type: 'cancel_insertions'
				});
			});

			this.scene = new Scene();
			this.scene.name = 'scene_volume';

			this.viewer.inputHandler.registerInteractiveScene(this.scene);

			this.onRemove = e => {
				this.scene.remove(e.volume);
			};

			this.onAdd = e => {
				this.scene.add(e.volume);
			};

			for(let volume of viewer.scene.volumes){
				this.onAdd({volume: volume});
			}

			this.viewer.inputHandler.addEventListener('delete', e => {
				let volumes = e.selection.filter(e => (e instanceof Volume));
				volumes.forEach(e => this.viewer.scene.removeVolume(e));
			});

			viewer.addEventListener("update", this.update.bind(this));
			viewer.addEventListener("render.pass.scene", e => this.render(e));
			viewer.addEventListener("scene_changed", this.onSceneChange.bind(this));

			viewer.scene.addEventListener('volume_added', this.onAdd);
			viewer.scene.addEventListener('volume_removed', this.onRemove);
		}

		onSceneChange(e){
			if(e.oldScene){
				e.oldScene.removeEventListeners('volume_added', this.onAdd);
				e.oldScene.removeEventListeners('volume_removed', this.onRemove);
			}

			e.scene.addEventListener('volume_added', this.onAdd);
			e.scene.addEventListener('volume_removed', this.onRemove);
		}

		startInsertion (args = {}) {
			let volume;
			if(args.type){
				volume = new args.type();
			}else {
				volume = new BoxVolume();
			}
			
			volume.clip = args.clip || false;
			volume.name = args.name || 'Volume';

			this.dispatchEvent({
				type: 'start_inserting_volume',
				volume: volume
			});

			this.viewer.scene.addVolume(volume);
			this.scene.add(volume);

			let cancel = {
				callback: null
			};

			let drag = e => {
				let camera = this.viewer.scene.getActiveCamera();
				
				let I = Utils.getMousePointCloudIntersection(
					e.drag.end, 
					this.viewer.scene.getActiveCamera(), 
					this.viewer, 
					this.viewer.scene.pointclouds, 
					{pickClipped: false});

				if (I) {
					volume.position.copy(I.location);

					let wp = volume.getWorldPosition(new Vector3()).applyMatrix4(camera.matrixWorldInverse);
					// let pp = new THREE.Vector4(wp.x, wp.y, wp.z).applyMatrix4(camera.projectionMatrix);
					let w = Math.abs((wp.z / 5));
					volume.scale.set(w, w, w);
				}
			};

			let drop = e => {
				volume.removeEventListener('drag', drag);
				volume.removeEventListener('drop', drop);

				cancel.callback();
			};

			cancel.callback = e => {
				volume.removeEventListener('drag', drag);
				volume.removeEventListener('drop', drop);
				this.viewer.removeEventListener('cancel_insertions', cancel.callback);
			};

			volume.addEventListener('drag', drag);
			volume.addEventListener('drop', drop);
			this.viewer.addEventListener('cancel_insertions', cancel.callback);

			this.viewer.inputHandler.startDragging(volume);

			return volume;
		}

		update(){
			if (!this.viewer.scene) {
				return;
			}
			
			let camera = this.viewer.scene.getActiveCamera();
			let renderAreaSize = this.viewer.renderer.getSize(new Vector2());
			let clientWidth = renderAreaSize.width;
			let clientHeight = renderAreaSize.height;

			let volumes = this.viewer.scene.volumes;
			for (let volume of volumes) {
				let label = volume.label;
				
				{

					let distance = label.position.distanceTo(camera.position);
					let pr = Utils.projectedRadius(1, camera, distance, clientWidth, clientHeight);

					let scale = (70 / pr);
					label.scale.set(scale, scale, scale);
				}

				let calculatedVolume = volume.getVolume();
				calculatedVolume = calculatedVolume / Math.pow(this.viewer.lengthUnit.unitspermeter, 3) * Math.pow(this.viewer.lengthUnitDisplay.unitspermeter, 3);  //convert to cubic meters then to the cubic display unit
				let text = Utils.addCommas(calculatedVolume.toFixed(3)) + ' ' + this.viewer.lengthUnitDisplay.code + '\u00B3';
				label.setText(text);
			}
		}

		render(params){
			const renderer = this.viewer.renderer;

			const oldTarget = renderer.getRenderTarget();
			
			if(params.renderTarget){
				renderer.setRenderTarget(params.renderTarget);
			}
			renderer.render(this.scene, this.viewer.scene.getActiveCamera());
			renderer.setRenderTarget(oldTarget);
		}

	}

	class Compass{

		constructor(viewer){
			this.viewer = viewer;

			this.visible = false;
			this.dom = this.createElement();

			viewer.addEventListener("update", () => {
				const direction = viewer.scene.view.direction.clone();
				direction.z = 0;
				direction.normalize();

				const camera = viewer.scene.getActiveCamera();

				const p1 = camera.getWorldPosition(new Vector3());
				const p2 = p1.clone().add(direction);

				const projection = viewer.getProjection();
				const azimuth = Utils.computeAzimuth(p1, p2, projection);
				
				this.dom.css("transform", `rotateZ(${-azimuth}rad)`);
			});

			this.dom.click( () => {
				viewer.setTopView();
			});

			const renderArea = $(viewer.renderArea);
			renderArea.append(this.dom);

			this.setVisible(this.visible);
		}

		setVisible(visible){
			this.visible = visible;

			const value = visible ? "" : "none";
			this.dom.css("display", value);
		}

		isVisible(){
			return this.visible;
		}

		createElement(){
			const style = `style="position: absolute; top: 10px; right: 10px; z-index: 10000; width: 64px;"`;
			const img = $(`<img src="${Potree.resourcePath}/images/compas.svg" ${style} />`);

			return img;
		}

	};

	class PotreeRenderer {

		constructor (viewer) {
			this.viewer = viewer;
			this.renderer = viewer.renderer;

			{
				let dummyScene = new Scene();
				let geometry = new SphereGeometry(0.001, 2, 2);
				let mesh = new Mesh(geometry, new MeshBasicMaterial());
				mesh.position.set(36453, 35163, 764712);
				dummyScene.add(mesh);

				this.dummyMesh = mesh;
				this.dummyScene = dummyScene;
			}
		}

		clearTargets(){

		}

		clear(){
			let {viewer, renderer} = this;


			// render skybox
			if(viewer.background === "skybox"){
				renderer.setClearColor(0xff0000, 1);
			}else if(viewer.background === "gradient"){
				renderer.setClearColor(0x00ff00, 1);
			}else if(viewer.background === "black"){
				renderer.setClearColor(0x000000, 1);
			}else if(viewer.background === "white"){
				renderer.setClearColor(0xFFFFFF, 1);
			}else {
				renderer.setClearColor(0x000000, 0);
			}

			renderer.clear();
		}
	 
		render(params){
			let {viewer, renderer} = this;

			const camera = params.camera ? params.camera : viewer.scene.getActiveCamera();

			viewer.dispatchEvent({type: "render.pass.begin",viewer: viewer});

			const renderAreaSize = renderer.getSize(new Vector2());
			const width = params.viewport ? params.viewport[2] : renderAreaSize.x;
			const height = params.viewport ? params.viewport[3] : renderAreaSize.y;

			// render skybox
			if(viewer.background === "skybox"){
				viewer.skybox.camera.rotation.copy(viewer.scene.cameraP.rotation);
				viewer.skybox.camera.fov = viewer.scene.cameraP.fov;
				viewer.skybox.camera.aspect = viewer.scene.cameraP.aspect;
				
				viewer.skybox.parent.rotation.x = 0;
				viewer.skybox.parent.updateMatrixWorld();

				viewer.skybox.camera.updateProjectionMatrix();
				renderer.render(viewer.skybox.scene, viewer.skybox.camera);
			}else if(viewer.background === "gradient"){
				renderer.render(viewer.scene.sceneBG, viewer.scene.cameraBG);
			}
			
			for(let pointcloud of this.viewer.scene.pointclouds){
				const {material} = pointcloud;
				material.useEDL = false;
			}
			
			viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, null, {
				clipSpheres: viewer.scene.volumes.filter(v => (v instanceof Potree.SphereVolume)),
			});
			
			// render scene
			renderer.render(viewer.scene.scene, camera);

			viewer.dispatchEvent({type: "render.pass.scene",viewer: viewer});
			
			viewer.clippingTool.update();
			renderer.render(viewer.clippingTool.sceneMarker, viewer.scene.cameraScreenSpace); //viewer.scene.cameraScreenSpace);
			renderer.render(viewer.clippingTool.sceneVolume, camera);

			renderer.render(viewer.controls.sceneControls, camera);
			
			renderer.clearDepth();
			
			viewer.transformationTool.update();
			
			viewer.dispatchEvent({type: "render.pass.perspective_overlay",viewer: viewer});

			// renderer.render(viewer.controls.sceneControls, camera);
			// renderer.render(viewer.clippingTool.sceneVolume, camera);
			// renderer.render(viewer.transformationTool.scene, camera);
			
			// renderer.setViewport(width - viewer.navigationCube.width, 
			// 							height - viewer.navigationCube.width, 
			// 							viewer.navigationCube.width, viewer.navigationCube.width);
			// renderer.render(viewer.navigationCube, viewer.navigationCube.camera);		
			// renderer.setViewport(0, 0, width, height);
			
			viewer.dispatchEvent({type: "render.pass.end",viewer: viewer});
		}

	}

	class EDLRenderer{
		constructor(viewer){
			this.viewer = viewer;

			this.edlMaterial = null;

			this.rtRegular;
			this.rtEDL;

			this.gl = viewer.renderer.getContext();

			this.shadowMap = new PointCloudSM(this.viewer.pRenderer);
		}

		initEDL(){
			if (this.edlMaterial != null) {
				return;
			}

			this.edlMaterial = new EyeDomeLightingMaterial();
			this.edlMaterial.depthTest = true;
			this.edlMaterial.depthWrite = true;
			this.edlMaterial.transparent = true;

			this.rtEDL = new WebGLRenderTarget(1024, 1024, {
				minFilter: NearestFilter,
				magFilter: NearestFilter,
				format: RGBAFormat,
				type: FloatType,
				depthTexture: new DepthTexture(undefined, undefined, UnsignedIntType)
			});

			this.rtRegular = new WebGLRenderTarget(1024, 1024, {
				minFilter: NearestFilter,
				magFilter: NearestFilter,
				format: RGBAFormat,
				depthTexture: new DepthTexture(undefined, undefined, UnsignedIntType)
			});
		};

		resize(width, height){
			if(this.screenshot){
				width = this.screenshot.target.width;
				height = this.screenshot.target.height;
			}

			this.rtEDL.setSize(width , height);
			this.rtRegular.setSize(width , height);
		}

		makeScreenshot(camera, size, callback){

			if(camera === undefined || camera === null){
				camera = this.viewer.scene.getActiveCamera();
			}

			if(size === undefined || size === null){
				size = this.viewer.renderer.getSize(new Vector2());
			}

			let {width, height} = size;

			//let maxTextureSize = viewer.renderer.capabilities.maxTextureSize;
			//if(width * 4 < 
			width = 2 * width;
			height = 2 * height;

			let target = new WebGLRenderTarget(width, height, {
				format: RGBAFormat,
			});

			this.screenshot = {
				target: target
			};

			// HACK? removed because of error, was this important?
			//this.viewer.renderer.clearTarget(target, true, true, true);

			this.render();

			let pixelCount = width * height;
			let buffer = new Uint8Array(4 * pixelCount);

			this.viewer.renderer.readRenderTargetPixels(target, 0, 0, width, height, buffer);

			// flip vertically
			let bytesPerLine = width * 4;
			for(let i = 0; i < parseInt(height / 2); i++){
				let j = height - i - 1;

				let lineI = buffer.slice(i * bytesPerLine, i * bytesPerLine + bytesPerLine);
				let lineJ = buffer.slice(j * bytesPerLine, j * bytesPerLine + bytesPerLine);
				buffer.set(lineJ, i * bytesPerLine);
				buffer.set(lineI, j * bytesPerLine);
			}

			this.screenshot.target.dispose();
			delete this.screenshot;

			return {
				width: width,
				height: height,
				buffer: buffer
			};
		}

		clearTargets(){
			const viewer = this.viewer;
			const {renderer} = viewer;

			const oldTarget = renderer.getRenderTarget();

			renderer.setRenderTarget( this.rtEDL );
			renderer.clear( true, true, true );

			renderer.setRenderTarget( this.rtRegular );
			renderer.clear( true, true, false );

			renderer.setRenderTarget(oldTarget);
		}

		clear(){
			this.initEDL();
			const viewer = this.viewer;

			const {renderer, background} = viewer;

			if(background === "skybox"){
				renderer.setClearColor(0x000000, 0);
			} else if (background === 'gradient') {
				renderer.setClearColor(0x000000, 0);
			} else if (background === 'black') {
				renderer.setClearColor(0x000000, 1);
			} else if (background === 'white') {
				renderer.setClearColor(0xFFFFFF, 1);
			} else {
				renderer.setClearColor(0x000000, 0);
			}
			
			renderer.clear();

			this.clearTargets();
		}

		renderShadowMap(visiblePointClouds, camera, lights){

			const {viewer} = this;

			const doShadows = lights.length > 0 && !(lights[0].disableShadowUpdates);
			if(doShadows){
				let light = lights[0];

				this.shadowMap.setLight(light);

				let originalAttributes = new Map();
				for(let pointcloud of viewer.scene.pointclouds){
					// TODO IMPORTANT !!! check
					originalAttributes.set(pointcloud, pointcloud.material.activeAttributeName);
					pointcloud.material.disableEvents();
					pointcloud.material.activeAttributeName = "depth";
					//pointcloud.material.pointColorType = PointColorType.DEPTH;
				}

				this.shadowMap.render(viewer.scene.scenePointCloud, camera);

				for(let pointcloud of visiblePointClouds){
					let originalAttribute = originalAttributes.get(pointcloud);
					// TODO IMPORTANT !!! check
					pointcloud.material.activeAttributeName = originalAttribute;
					pointcloud.material.enableEvents();
				}

				viewer.shadowTestCam.updateMatrixWorld();
				viewer.shadowTestCam.matrixWorldInverse.copy(viewer.shadowTestCam.matrixWorld).invert();
				viewer.shadowTestCam.updateProjectionMatrix();
			}

		}

		render(params){
			this.initEDL();

			const viewer = this.viewer;
			let camera = params.camera ? params.camera : viewer.scene.getActiveCamera();
			const {width, height} = this.viewer.renderer.getSize(new Vector2());


			viewer.dispatchEvent({type: "render.pass.begin",viewer: viewer});
			
			this.resize(width, height);

			const visiblePointClouds = viewer.scene.pointclouds.filter(pc => pc.visible);

			if(this.screenshot){
				let oldBudget = Potree.pointBudget;
				Potree.pointBudget = Math.max(10 * 1000 * 1000, 2 * oldBudget);
				let result = Potree.updatePointClouds(
					viewer.scene.pointclouds, 
					camera, 
					viewer.renderer);
				Potree.pointBudget = oldBudget;
			}

			let lights = [];
			viewer.scene.scene.traverse(node => {
				if(node.type === "SpotLight"){
					lights.push(node);
				}
			});

			if(viewer.background === "skybox"){
				viewer.skybox.camera.rotation.copy(viewer.scene.cameraP.rotation);
				viewer.skybox.camera.fov = viewer.scene.cameraP.fov;
				viewer.skybox.camera.aspect = viewer.scene.cameraP.aspect;

				viewer.skybox.parent.rotation.x = 0;
				viewer.skybox.parent.updateMatrixWorld();

				viewer.skybox.camera.updateProjectionMatrix();
				viewer.renderer.render(viewer.skybox.scene, viewer.skybox.camera);
			} else if (viewer.background === 'gradient') {
				viewer.renderer.render(viewer.scene.sceneBG, viewer.scene.cameraBG);
			} 

			//TODO adapt to multiple lights
			this.renderShadowMap(visiblePointClouds, camera, lights);

			{ // COLOR & DEPTH PASS
				for (let pointcloud of visiblePointClouds) {
					let octreeSize = pointcloud.pcoGeometry.boundingBox.getSize(new Vector3()).x;

					let material = pointcloud.material;
					material.weighted = false;
					material.useLogarithmicDepthBuffer = false;
					material.useEDL = true;

					material.screenWidth = width;
					material.screenHeight = height;
					material.uniforms.visibleNodes.value = pointcloud.material.visibleNodesTexture;
					material.uniforms.octreeSize.value = octreeSize;
					material.spacing = pointcloud.pcoGeometry.spacing; // * Math.max(pointcloud.scale.x, pointcloud.scale.y, pointcloud.scale.z);
				}
				
				// TODO adapt to multiple lights
				viewer.renderer.setRenderTarget(this.rtEDL);
				
				if(lights.length > 0){
					viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtEDL, {
						clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)),
						shadowMaps: [this.shadowMap],
						transparent: false,
					});
				}else {

					
					// let test = camera.clone();
					// test.matrixAutoUpdate = false;

					// //test.updateMatrixWorld = () => {};

					// let mat = new THREE.Matrix4().set(
					// 	1, 0, 0, 0,
					// 	0, 0, 1, 0,
					// 	0, -1, 0, 0,
					// 	0, 0, 0, 1,
					// );
					// mat.invert()

					// test.matrix.multiplyMatrices(mat, test.matrix);
					// test.updateMatrixWorld();

					//test.matrixWorld.multiplyMatrices(mat, test.matrixWorld);
					//test.matrixWorld.multiply(mat);
					//test.matrixWorldInverse.invert(test.matrixWorld);
					//test.matrixWorldInverse.multiplyMatrices(test.matrixWorldInverse, mat);
					

					viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtEDL, {
						clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)),
						transparent: false,
					});
				}

				
			}

			viewer.dispatchEvent({type: "render.pass.scene", viewer: viewer, renderTarget: this.rtRegular});
			viewer.renderer.setRenderTarget(null);
			viewer.renderer.render(viewer.scene.scene, camera);

			{ // EDL PASS

				const uniforms = this.edlMaterial.uniforms;

				uniforms.screenWidth.value = width;
				uniforms.screenHeight.value = height;

				let proj = camera.projectionMatrix;
				let projArray = new Float32Array(16);
				projArray.set(proj.elements);

				uniforms.uNear.value = camera.near;
				uniforms.uFar.value = camera.far;
				uniforms.uEDLColor.value = this.rtEDL.texture;
				uniforms.uEDLDepth.value = this.rtEDL.depthTexture;
				uniforms.uProj.value = projArray;

				uniforms.edlStrength.value = viewer.edlStrength;
				uniforms.radius.value = viewer.edlRadius;
				uniforms.opacity.value = viewer.edlOpacity; // HACK
				
				Utils.screenPass.render(viewer.renderer, this.edlMaterial);

				if(this.screenshot){
					Utils.screenPass.render(viewer.renderer, this.edlMaterial, this.screenshot.target);
				}

			}

			viewer.dispatchEvent({type: "render.pass.scene", viewer: viewer});

			viewer.renderer.clearDepth();

			viewer.transformationTool.update();

			viewer.dispatchEvent({type: "render.pass.perspective_overlay",viewer: viewer});

			viewer.renderer.render(viewer.controls.sceneControls, camera);
			viewer.renderer.render(viewer.clippingTool.sceneVolume, camera);
			viewer.renderer.render(viewer.transformationTool.scene, camera);
			
			viewer.dispatchEvent({type: "render.pass.end",viewer: viewer});

		}
	}

	class HQSplatRenderer{
		
		constructor(viewer){
			this.viewer = viewer;

			this.depthMaterials = new Map();
			this.attributeMaterials = new Map();
			this.normalizationMaterial = null;

			this.rtDepth = null;
			this.rtAttribute = null;
			this.gl = viewer.renderer.getContext();

			this.initialized = false;
		}

		init(){
			if (this.initialized) {
				return;
			}

			this.normalizationMaterial = new NormalizationMaterial();
			this.normalizationMaterial.depthTest = true;
			this.normalizationMaterial.depthWrite = true;
			this.normalizationMaterial.transparent = true;

			this.normalizationEDLMaterial = new NormalizationEDLMaterial();
			this.normalizationEDLMaterial.depthTest = true;
			this.normalizationEDLMaterial.depthWrite = true;
			this.normalizationEDLMaterial.transparent = true;

			this.rtDepth = new WebGLRenderTarget(1024, 1024, {
				minFilter: NearestFilter,
				magFilter: NearestFilter,
				format: RGBAFormat,
				type: FloatType,
				depthTexture: new DepthTexture(undefined, undefined, UnsignedIntType)
			});

			this.rtAttribute = new WebGLRenderTarget(1024, 1024, {
				minFilter: NearestFilter,
				magFilter: NearestFilter,
				format: RGBAFormat,
				type: FloatType,
				depthTexture: this.rtDepth.depthTexture,
			});

			this.initialized = true;
		};

		resize(width, height){
			this.rtDepth.setSize(width, height);
			this.rtAttribute.setSize(width, height);
		}

		clearTargets(){
			const viewer = this.viewer;
			const {renderer} = viewer;

			const oldTarget = renderer.getRenderTarget();

			renderer.setClearColor(0x000000, 0);

			renderer.setRenderTarget( this.rtDepth );
			renderer.clear( true, true, true );

			renderer.setRenderTarget( this.rtAttribute );
			renderer.clear( true, true, true );

			renderer.setRenderTarget(oldTarget);
		}


		clear(){
			this.init();

			const {renderer, background} = this.viewer;

			if(background === "skybox"){
				renderer.setClearColor(0x000000, 0);
			} else if (background === 'gradient') {
				renderer.setClearColor(0x000000, 0);
			} else if (background === 'black') {
				renderer.setClearColor(0x000000, 1);
			} else if (background === 'white') {
				renderer.setClearColor(0xFFFFFF, 1);
			} else {
				renderer.setClearColor(0x000000, 0);
			}

			renderer.clear();

			this.clearTargets();
		}

		render (params) {
			this.init();

			const viewer = this.viewer;
			const camera = params.camera ? params.camera : viewer.scene.getActiveCamera();
			const {width, height} = this.viewer.renderer.getSize(new Vector2());

			viewer.dispatchEvent({type: "render.pass.begin",viewer: viewer});

			this.resize(width, height);

			const visiblePointClouds = viewer.scene.pointclouds.filter(pc => pc.visible);
			const originalMaterials = new Map();

			for(let pointcloud of visiblePointClouds){
				originalMaterials.set(pointcloud, pointcloud.material);

				if(!this.attributeMaterials.has(pointcloud)){
					let attributeMaterial = new PointCloudMaterial$1();
					this.attributeMaterials.set(pointcloud, attributeMaterial);
				}

				if(!this.depthMaterials.has(pointcloud)){
					let depthMaterial = new PointCloudMaterial$1();

					depthMaterial.setDefine("depth_pass", "#define hq_depth_pass");
					depthMaterial.setDefine("use_edl", "#define use_edl");

					this.depthMaterials.set(pointcloud, depthMaterial);
				}
			}

			{ // DEPTH PASS
				for (let pointcloud of visiblePointClouds) {
					let octreeSize = pointcloud.pcoGeometry.boundingBox.getSize(new Vector3()).x;

					let material = originalMaterials.get(pointcloud);
					let depthMaterial = this.depthMaterials.get(pointcloud);

					depthMaterial.size = material.size;
					depthMaterial.minSize = material.minSize;
					depthMaterial.maxSize = material.maxSize;

					depthMaterial.pointSizeType = material.pointSizeType;
					depthMaterial.visibleNodesTexture = material.visibleNodesTexture;
					depthMaterial.weighted = false;
					depthMaterial.screenWidth = width;
					depthMaterial.shape = PointShape.CIRCLE;
					depthMaterial.screenHeight = height;
					depthMaterial.uniforms.visibleNodes.value = material.visibleNodesTexture;
					depthMaterial.uniforms.octreeSize.value = octreeSize;
					depthMaterial.spacing = pointcloud.pcoGeometry.spacing; // * Math.max(...pointcloud.scale.toArray());
					depthMaterial.classification = material.classification;
					depthMaterial.uniforms.classificationLUT.value.image.data = material.uniforms.classificationLUT.value.image.data;
					depthMaterial.classificationTexture.needsUpdate = true;

					depthMaterial.uniforms.uFilterReturnNumberRange.value = material.uniforms.uFilterReturnNumberRange.value;
					depthMaterial.uniforms.uFilterNumberOfReturnsRange.value = material.uniforms.uFilterNumberOfReturnsRange.value;
					depthMaterial.uniforms.uFilterGPSTimeClipRange.value = material.uniforms.uFilterGPSTimeClipRange.value;
					depthMaterial.uniforms.uFilterPointSourceIDClipRange.value = material.uniforms.uFilterPointSourceIDClipRange.value;

					depthMaterial.clipTask = material.clipTask;
					depthMaterial.clipMethod = material.clipMethod;
					depthMaterial.setClipBoxes(material.clipBoxes);
					depthMaterial.setClipPolygons(material.clipPolygons);

					pointcloud.material = depthMaterial;
				}
				
				viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtDepth, {
					clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)),
				});
			}

			{ // ATTRIBUTE PASS
				for (let pointcloud of visiblePointClouds) {
					let octreeSize = pointcloud.pcoGeometry.boundingBox.getSize(new Vector3()).x;

					let material = originalMaterials.get(pointcloud);
					let attributeMaterial = this.attributeMaterials.get(pointcloud);

					attributeMaterial.size = material.size;
					attributeMaterial.minSize = material.minSize;
					attributeMaterial.maxSize = material.maxSize;

					attributeMaterial.pointSizeType = material.pointSizeType;
					attributeMaterial.activeAttributeName = material.activeAttributeName;
					attributeMaterial.visibleNodesTexture = material.visibleNodesTexture;
					attributeMaterial.weighted = true;
					attributeMaterial.screenWidth = width;
					attributeMaterial.screenHeight = height;
					attributeMaterial.shape = PointShape.CIRCLE;
					attributeMaterial.uniforms.visibleNodes.value = material.visibleNodesTexture;
					attributeMaterial.uniforms.octreeSize.value = octreeSize;
					attributeMaterial.spacing = pointcloud.pcoGeometry.spacing; // * Math.max(...pointcloud.scale.toArray());
					attributeMaterial.classification = material.classification;
					attributeMaterial.uniforms.classificationLUT.value.image.data = material.uniforms.classificationLUT.value.image.data;
					attributeMaterial.classificationTexture.needsUpdate = true;

					attributeMaterial.uniforms.uFilterReturnNumberRange.value = material.uniforms.uFilterReturnNumberRange.value;
					attributeMaterial.uniforms.uFilterNumberOfReturnsRange.value = material.uniforms.uFilterNumberOfReturnsRange.value;
					attributeMaterial.uniforms.uFilterGPSTimeClipRange.value = material.uniforms.uFilterGPSTimeClipRange.value;
					attributeMaterial.uniforms.uFilterPointSourceIDClipRange.value = material.uniforms.uFilterPointSourceIDClipRange.value;

					attributeMaterial.elevationGradientRepeat = material.elevationGradientRepeat;
					attributeMaterial.elevationRange = material.elevationRange;
					attributeMaterial.gradient = material.gradient;
					attributeMaterial.matcap = material.matcap;

					attributeMaterial.intensityRange = material.intensityRange;
					attributeMaterial.intensityGamma = material.intensityGamma;
					attributeMaterial.intensityContrast = material.intensityContrast;
					attributeMaterial.intensityBrightness = material.intensityBrightness;

					attributeMaterial.rgbGamma = material.rgbGamma;
					attributeMaterial.rgbContrast = material.rgbContrast;
					attributeMaterial.rgbBrightness = material.rgbBrightness;

					attributeMaterial.weightRGB = material.weightRGB;
					attributeMaterial.weightIntensity = material.weightIntensity;
					attributeMaterial.weightElevation = material.weightElevation;
					attributeMaterial.weightRGB = material.weightRGB;
					attributeMaterial.weightClassification = material.weightClassification;
					attributeMaterial.weightReturnNumber = material.weightReturnNumber;
					attributeMaterial.weightSourceID = material.weightSourceID;

					attributeMaterial.color = material.color;

					attributeMaterial.clipTask = material.clipTask;
					attributeMaterial.clipMethod = material.clipMethod;
					attributeMaterial.setClipBoxes(material.clipBoxes);
					attributeMaterial.setClipPolygons(material.clipPolygons);

					pointcloud.material = attributeMaterial;
				}
				
				let gl = this.gl;

				viewer.renderer.setRenderTarget(null);
				viewer.pRenderer.render(viewer.scene.scenePointCloud, camera, this.rtAttribute, {
					clipSpheres: viewer.scene.volumes.filter(v => (v instanceof SphereVolume)),
					//material: this.attributeMaterial,
					blendFunc: [gl.SRC_ALPHA, gl.ONE],
					//depthTest: false,
					depthWrite: false
				});
			}

			for(let [pointcloud, material] of originalMaterials){
				pointcloud.material = material;
			}

			viewer.renderer.setRenderTarget(null);
			if(viewer.background === "skybox"){
				viewer.renderer.setClearColor(0x000000, 0);
				viewer.renderer.clear();
				viewer.skybox.camera.rotation.copy(viewer.scene.cameraP.rotation);
				viewer.skybox.camera.fov = viewer.scene.cameraP.fov;
				viewer.skybox.camera.aspect = viewer.scene.cameraP.aspect;
				
				viewer.skybox.parent.rotation.x = 0;
				viewer.skybox.parent.updateMatrixWorld();

				viewer.skybox.camera.updateProjectionMatrix();
				viewer.renderer.render(viewer.skybox.scene, viewer.skybox.camera);
			} else if (viewer.background === 'gradient') {
				viewer.renderer.setClearColor(0x000000, 0);
				viewer.renderer.clear();
				viewer.renderer.render(viewer.scene.sceneBG, viewer.scene.cameraBG);
			} else if (viewer.background === 'black') {
				viewer.renderer.setClearColor(0x000000, 1);
				viewer.renderer.clear();
			} else if (viewer.background === 'white') {
				viewer.renderer.setClearColor(0xFFFFFF, 1);
				viewer.renderer.clear();
			} else {
				viewer.renderer.setClearColor(0x000000, 0);
				viewer.renderer.clear();
			}

			{ // NORMALIZATION PASS
				let normalizationMaterial = this.useEDL ? this.normalizationEDLMaterial : this.normalizationMaterial;

				if(this.useEDL){
					normalizationMaterial.uniforms.edlStrength.value = viewer.edlStrength;
					normalizationMaterial.uniforms.radius.value = viewer.edlRadius;
					normalizationMaterial.uniforms.screenWidth.value = width;
					normalizationMaterial.uniforms.screenHeight.value = height;
					normalizationMaterial.uniforms.uEDLMap.value = this.rtDepth.texture;
				}

				normalizationMaterial.uniforms.uWeightMap.value = this.rtAttribute.texture;
				normalizationMaterial.uniforms.uDepthMap.value = this.rtAttribute.depthTexture;
				
				Utils.screenPass.render(viewer.renderer, normalizationMaterial);
			}

			viewer.renderer.render(viewer.scene.scene, camera);

			viewer.dispatchEvent({type: "render.pass.scene", viewer: viewer});

			viewer.renderer.clearDepth();

			viewer.transformationTool.update();

			viewer.dispatchEvent({type: "render.pass.perspective_overlay",viewer: viewer});

			viewer.renderer.render(viewer.controls.sceneControls, camera);
			viewer.renderer.render(viewer.clippingTool.sceneVolume, camera);
			viewer.renderer.render(viewer.transformationTool.scene, camera);

			viewer.renderer.setViewport(width - viewer.navigationCube.width, 
										height - viewer.navigationCube.width, 
										viewer.navigationCube.width, viewer.navigationCube.width);
			viewer.renderer.render(viewer.navigationCube, viewer.navigationCube.camera);		
			viewer.renderer.setViewport(0, 0, width, height);
			
			viewer.dispatchEvent({type: "render.pass.end",viewer: viewer});

		}

	}

	class View{
		constructor () {
			this.position = new Vector3(0, 0, 0);

			this.yaw = Math.PI / 4;
			this._pitch = -Math.PI / 4;
			this.radius = 1;

			this.maxPitch = Math.PI / 2;
			this.minPitch = -Math.PI / 2;
		}

		clone () {
			let c = new View();
			c.yaw = this.yaw;
			c._pitch = this.pitch;
			c.radius = this.radius;
			c.maxPitch = this.maxPitch;
			c.minPitch = this.minPitch;

			return c;
		}

		get pitch () {
			return this._pitch;
		}

		set pitch (angle) {
			this._pitch = Math.max(Math.min(angle, this.maxPitch), this.minPitch);
		}

		get direction () {
			let dir = new Vector3(0, 1, 0);

			dir.applyAxisAngle(new Vector3(1, 0, 0), this.pitch);
			dir.applyAxisAngle(new Vector3(0, 0, 1), this.yaw);

			return dir;
		}

		set direction (dir) {

			//if(dir.x === dir.y){
			if(dir.x === 0 && dir.y === 0){
				this.pitch = Math.PI / 2 * Math.sign(dir.z);
			}else {
				let yaw = Math.atan2(dir.y, dir.x) - Math.PI / 2;
				let pitch = Math.atan2(dir.z, Math.sqrt(dir.x * dir.x + dir.y * dir.y));

				this.yaw = yaw;
				this.pitch = pitch;
			}
			
		}

		lookAt(t){
			let V;
			if(arguments.length === 1){
				V = new Vector3().subVectors(t, this.position);
			}else if(arguments.length === 3){
				V = new Vector3().subVectors(new Vector3(...arguments), this.position);
			}

			let radius = V.length();
			let dir = V.normalize();

			this.radius = radius;
			this.direction = dir;
		}

		getPivot () {
			return new Vector3().addVectors(this.position, this.direction.multiplyScalar(this.radius));
		}

		getSide () {
			let side = new Vector3(1, 0, 0);
			side.applyAxisAngle(new Vector3(0, 0, 1), this.yaw);

			return side;
		}

		pan (x, y) {
			let dir = new Vector3(0, 1, 0);
			dir.applyAxisAngle(new Vector3(1, 0, 0), this.pitch);
			dir.applyAxisAngle(new Vector3(0, 0, 1), this.yaw);

			// let side = new THREE.Vector3(1, 0, 0);
			// side.applyAxisAngle(new THREE.Vector3(0, 0, 1), this.yaw);

			let side = this.getSide();

			let up = side.clone().cross(dir);

			let pan = side.multiplyScalar(x).add(up.multiplyScalar(y));

			this.position = this.position.add(pan);
			// this.target = this.target.add(pan);
		}

		translate (x, y, z) {
			let dir = new Vector3(0, 1, 0);
			dir.applyAxisAngle(new Vector3(1, 0, 0), this.pitch);
			dir.applyAxisAngle(new Vector3(0, 0, 1), this.yaw);

			let side = new Vector3(1, 0, 0);
			side.applyAxisAngle(new Vector3(0, 0, 1), this.yaw);

			let up = side.clone().cross(dir);

			let t = side.multiplyScalar(x)
				.add(dir.multiplyScalar(y))
				.add(up.multiplyScalar(z));

			this.position = this.position.add(t);
		}

		translateWorld (x, y, z) {
			this.position.x += x;
			this.position.y += y;
			this.position.z += z;
		}

		setView(position, target, duration = 0, callback = null){

			let endPosition = null;
			if(position instanceof Array){
				endPosition = new Vector3(...position);
			}else if(position.x != null){
				endPosition = position.clone();
			}

			let endTarget = null;
			if(target instanceof Array){
				endTarget = new Vector3(...target);
			}else if(target.x != null){
				endTarget = target.clone();
			}
			
			const startPosition = this.position.clone();
			const startTarget = this.getPivot();

			//const endPosition = position.clone();
			//const endTarget = target.clone();

			let easing = TWEEN.Easing.Quartic.Out;

			if(duration === 0){
				this.position.copy(endPosition);
				this.lookAt(endTarget);
			}else {
				let value = {x: 0};
				let tween = new TWEEN.Tween(value).to({x: 1}, duration);
				tween.easing(easing);
				//this.tweens.push(tween);

				tween.onUpdate(() => {
					let t = value.x;

					//console.log(t);

					const pos = new Vector3(
						(1 - t) * startPosition.x + t * endPosition.x,
						(1 - t) * startPosition.y + t * endPosition.y,
						(1 - t) * startPosition.z + t * endPosition.z,
					);

					const target = new Vector3(
						(1 - t) * startTarget.x + t * endTarget.x,
						(1 - t) * startTarget.y + t * endTarget.y,
						(1 - t) * startTarget.z + t * endTarget.z,
					);

					this.position.copy(pos);
					this.lookAt(target);

				});

				tween.start();

				tween.onComplete(() => {
					if(callback){
						callback();
					}
				});
			}

		}

	};

	class Scene$1 extends EventDispatcher{

		constructor(){
			super();

			this.annotations = new Annotation();
			
			this.scene = new Scene();
			this.sceneBG = new Scene();
			this.scenePointCloud = new Scene();

			this.cameraP = new PerspectiveCamera(this.fov, 1, 0.1, 1000*1000);
			this.cameraO = new OrthographicCamera(-1, 1, 1, -1, 0.1, 1000*1000);
			this.cameraVR = new PerspectiveCamera();
			this.cameraBG = new Camera();
			this.cameraScreenSpace = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10);
			this.cameraMode = CameraMode.PERSPECTIVE;
			this.overrideCamera = null;
			this.pointclouds = [];

			this.measurements = [];
			this.profiles = [];
			this.volumes = [];
			this.polygonClipVolumes = [];
			this.cameraAnimations = [];
			this.orientedImages = [];
			this.images360 = [];
			this.geopackages = [];
			
			this.fpControls = null;
			this.orbitControls = null;
			this.earthControls = null;
			this.geoControls = null;
			this.deviceControls = null;
			this.inputHandler = null;

			this.view = new View();

			this.directionalLight = null;

			this.initialize();
		}

		estimateHeightAt (position) {
			let height = null;
			let fromSpacing = Infinity;

			for (let pointcloud of this.pointclouds) {
				if (pointcloud.root.geometryNode === undefined) {
					continue;
				}

				let pHeight = null;
				let pFromSpacing = Infinity;

				let lpos = position.clone().sub(pointcloud.position);
				lpos.z = 0;
				let ray = new Ray(lpos, new Vector3(0, 0, 1));

				let stack = [pointcloud.root];
				while (stack.length > 0) {
					let node = stack.pop();
					let box = node.getBoundingBox();

					let inside = ray.intersectBox(box);

					if (!inside) {
						continue;
					}

					let h = node.geometryNode.mean.z +
						pointcloud.position.z +
						node.geometryNode.boundingBox.min.z;

					if (node.geometryNode.spacing <= pFromSpacing) {
						pHeight = h;
						pFromSpacing = node.geometryNode.spacing;
					}

					for (let index of Object.keys(node.children)) {
						let child = node.children[index];
						if (child.geometryNode) {
							stack.push(node.children[index]);
						}
					}
				}

				if (height === null || pFromSpacing < fromSpacing) {
					height = pHeight;
					fromSpacing = pFromSpacing;
				}
			}

			return height;
		}
		
		getBoundingBox(pointclouds = this.pointclouds){
			let box = new Box3();

			this.scenePointCloud.updateMatrixWorld(true);
			this.referenceFrame.updateMatrixWorld(true);

			for (let pointcloud of pointclouds) {
				pointcloud.updateMatrixWorld(true);

				let pointcloudBox = pointcloud.pcoGeometry.tightBoundingBox ? pointcloud.pcoGeometry.tightBoundingBox : pointcloud.boundingBox;
				let boxWorld = Utils.computeTransformedBoundingBox(pointcloudBox, pointcloud.matrixWorld);
				box.union(boxWorld);
			}

			return box;
		}

		addPointCloud (pointcloud) {
			this.pointclouds.push(pointcloud);
			this.scenePointCloud.add(pointcloud);

			this.dispatchEvent({
				type: 'pointcloud_added',
				pointcloud: pointcloud
			});
		}

		addVolume (volume) {
			this.volumes.push(volume);
			this.dispatchEvent({
				'type': 'volume_added',
				'scene': this,
				'volume': volume
			});
		}

		addOrientedImages(images){
			this.orientedImages.push(images);
			this.scene.add(images.node);

			this.dispatchEvent({
				'type': 'oriented_images_added',
				'scene': this,
				'images': images
			});
		};

		removeOrientedImages(images){
			let index = this.orientedImages.indexOf(images);
			if (index > -1) {
				this.orientedImages.splice(index, 1);

				this.dispatchEvent({
					'type': 'oriented_images_removed',
					'scene': this,
					'images': images
				});
			}
		};

		add360Images(images){
			this.images360.push(images);
			this.scene.add(images.node);

			this.dispatchEvent({
				'type': '360_images_added',
				'scene': this,
				'images': images
			});
		}

		remove360Images(images){
			let index = this.images360.indexOf(images);
			if (index > -1) {
				this.images360.splice(index, 1);

				this.dispatchEvent({
					'type': '360_images_removed',
					'scene': this,
					'images': images
				});
			}
		}

		addGeopackage(geopackage){
			this.geopackages.push(geopackage);
			this.scene.add(geopackage.node);

			this.dispatchEvent({
				'type': 'geopackage_added',
				'scene': this,
				'geopackage': geopackage
			});
		};

		removeGeopackage(geopackage){
			let index = this.geopackages.indexOf(geopackage);
			if (index > -1) {
				this.geopackages.splice(index, 1);

				this.dispatchEvent({
					'type': 'geopackage_removed',
					'scene': this,
					'geopackage': geopackage
				});
			}
		};

		removeVolume (volume) {
			let index = this.volumes.indexOf(volume);
			if (index > -1) {
				this.volumes.splice(index, 1);

				this.dispatchEvent({
					'type': 'volume_removed',
					'scene': this,
					'volume': volume
				});
			}
		};

		addCameraAnimation(animation) {
			this.cameraAnimations.push(animation);
			this.dispatchEvent({
				'type': 'camera_animation_added',
				'scene': this,
				'animation': animation
			});
		};

		removeCameraAnimation(animation){
			let index = this.cameraAnimations.indexOf(volume);
			if (index > -1) {
				this.cameraAnimations.splice(index, 1);

				this.dispatchEvent({
					'type': 'camera_animation_removed',
					'scene': this,
					'animation': animation
				});
			}
		};

		addPolygonClipVolume(volume){
			this.polygonClipVolumes.push(volume);
			this.dispatchEvent({
				"type": "polygon_clip_volume_added",
				"scene": this,
				"volume": volume
			});
		};
		
		removePolygonClipVolume(volume){
			let index = this.polygonClipVolumes.indexOf(volume);
			if (index > -1) {
				this.polygonClipVolumes.splice(index, 1);
				this.dispatchEvent({
					"type": "polygon_clip_volume_removed",
					"scene": this,
					"volume": volume
				});
			}
		};
		
		addMeasurement(measurement){
			measurement.lengthUnit = this.lengthUnit;
			measurement.lengthUnitDisplay = this.lengthUnitDisplay;
			this.measurements.push(measurement);
			this.dispatchEvent({
				'type': 'measurement_added',
				'scene': this,
				'measurement': measurement
			});
		};

		removeMeasurement (measurement) {
			let index = this.measurements.indexOf(measurement);
			if (index > -1) {
				this.measurements.splice(index, 1);
				this.dispatchEvent({
					'type': 'measurement_removed',
					'scene': this,
					'measurement': measurement
				});
			}
		}

		addProfile (profile) {
			this.profiles.push(profile);
			this.dispatchEvent({
				'type': 'profile_added',
				'scene': this,
				'profile': profile
			});
		}

		removeProfile (profile) {
			let index = this.profiles.indexOf(profile);
			if (index > -1) {
				this.profiles.splice(index, 1);
				this.dispatchEvent({
					'type': 'profile_removed',
					'scene': this,
					'profile': profile
				});
			}
		}

		removeAllMeasurements () {
			while (this.measurements.length > 0) {
				this.removeMeasurement(this.measurements[0]);
			}

			while (this.profiles.length > 0) {
				this.removeProfile(this.profiles[0]);
			}

			while (this.volumes.length > 0) {
				this.removeVolume(this.volumes[0]);
			}
		}

		removeAllClipVolumes(){
			let clipVolumes = this.volumes.filter(volume => volume.clip === true);
			for(let clipVolume of clipVolumes){
				this.removeVolume(clipVolume);
			}

			while(this.polygonClipVolumes.length > 0){
				this.removePolygonClipVolume(this.polygonClipVolumes[0]);
			}
		}

		getActiveCamera() {

			if(this.overrideCamera){
				return this.overrideCamera;
			}

			if(this.cameraMode === CameraMode.PERSPECTIVE){
				return this.cameraP;
			}else if(this.cameraMode === CameraMode.ORTHOGRAPHIC){
				return this.cameraO;
			}else if(this.cameraMode === CameraMode.VR){
				return this.cameraVR;
			}

			return null;
		}
		
		initialize(){
			
			this.referenceFrame = new Object3D();
			this.referenceFrame.matrixAutoUpdate = false;
			this.scenePointCloud.add(this.referenceFrame);

			this.cameraP.up.set(0, 0, 1);
			this.cameraP.position.set(1000, 1000, 1000);
			this.cameraO.up.set(0, 0, 1);
			this.cameraO.position.set(1000, 1000, 1000);
			//this.camera.rotation.y = -Math.PI / 4;
			//this.camera.rotation.x = -Math.PI / 6;
			this.cameraScreenSpace.lookAt(new Vector3(0, 0, 0), new Vector3(0, 0, -1), new Vector3(0, 1, 0));
			
			this.directionalLight = new DirectionalLight( 0xffffff, 0.5 );
			this.directionalLight.position.set( 10, 10, 10 );
			this.directionalLight.lookAt( new Vector3(0, 0, 0));
			this.scenePointCloud.add( this.directionalLight );
			
			let light = new AmbientLight( 0x555555 ); // soft white light
			this.scenePointCloud.add( light );

			{ // background
				let texture = Utils.createBackgroundTexture(512, 512);

				texture.minFilter = texture.magFilter = NearestFilter;
				texture.minFilter = texture.magFilter = LinearFilter;
				let bg = new Mesh(
					new PlaneBufferGeometry(2, 2, 1),
					new MeshBasicMaterial({
						map: texture
					})
				);
				bg.material.depthTest = false;
				bg.material.depthWrite = false;
				this.sceneBG.add(bg);
			}

			// { // lights
			// 	{
			// 		let light = new THREE.DirectionalLight(0xffffff);
			// 		light.position.set(10, 10, 1);
			// 		light.target.position.set(0, 0, 0);
			// 		this.scene.add(light);
			// 	}

			// 	{
			// 		let light = new THREE.DirectionalLight(0xffffff);
			// 		light.position.set(-10, 10, 1);
			// 		light.target.position.set(0, 0, 0);
			// 		this.scene.add(light);
			// 	}

			// 	{
			// 		let light = new THREE.DirectionalLight(0xffffff);
			// 		light.position.set(0, -10, 20);
			// 		light.target.position.set(0, 0, 0);
			// 		this.scene.add(light);
			// 	}
			// }
		}
		
		addAnnotation(position, args = {}){		
			if(position instanceof Array){
				args.position = new Vector3().fromArray(position);
			} else if (position.x != null) {
				args.position = position;
			}
			let annotation = new Annotation(args);
			this.annotations.add(annotation);

			return annotation;
		}

		getAnnotations () {
			return this.annotations;
		};

		removeAnnotation(annotationToRemove) {
			this.annotations.remove(annotationToRemove);
		}
	};

	// http://epsg.io/
	proj4.defs([
		['UTM10N', '+proj=utm +zone=10 +ellps=GRS80 +datum=NAD83 +units=m +no_defs'],
		['EPSG:6339', '+proj=utm +zone=10 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:6340', '+proj=utm +zone=11 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:6341', '+proj=utm +zone=12 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:6342', '+proj=utm +zone=13 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:6343', '+proj=utm +zone=14 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:6344', '+proj=utm +zone=15 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:6345', '+proj=utm +zone=16 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:6346', '+proj=utm +zone=17 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:6347', '+proj=utm +zone=18 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:6348', '+proj=utm +zone=19 +ellps=GRS80 +units=m +no_defs'],
		['EPSG:26910', '+proj=utm +zone=10 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
		['EPSG:26911', '+proj=utm +zone=11 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
		['EPSG:26912', '+proj=utm +zone=12 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
		['EPSG:26913', '+proj=utm +zone=13 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
		['EPSG:26914', '+proj=utm +zone=14 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
		['EPSG:26915', '+proj=utm +zone=15 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
		['EPSG:26916', '+proj=utm +zone=16 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
		['EPSG:26917', '+proj=utm +zone=17 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
		['EPSG:26918', '+proj=utm +zone=18 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
		['EPSG:26919', '+proj=utm +zone=19 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs '],
	]);

	class MapView{

		constructor (viewer) {
			this.viewer = viewer;

			this.webMapService = 'WMTS';
			this.mapProjectionName = 'EPSG:3857';
			this.mapProjection = proj4.defs(this.mapProjectionName);
			this.sceneProjection = null;

			this.extentsLayer = null;
			this.cameraLayer = null;
			this.toolLayer = null;
			this.sourcesLayer = null;
			this.sourcesLabelLayer = null;
			this.images360Layer = null;
			this.enabled = false;

			this.createAnnotationStyle = (text) => {
				return [
					new ol.style.Style({
						image: new ol.style.Circle({
							radius: 10,
							stroke: new ol.style.Stroke({
								color: [255, 255, 255, 0.5],
								width: 2
							}),
							fill: new ol.style.Fill({
								color: [0, 0, 0, 0.5]
							})
						})
					})
				];
			};

			this.createLabelStyle = (text) => {
				let style = new ol.style.Style({
					image: new ol.style.Circle({
						radius: 6,
						stroke: new ol.style.Stroke({
							color: 'white',
							width: 2
						}),
						fill: new ol.style.Fill({
							color: 'green'
						})
					}),
					text: new ol.style.Text({
						font: '12px helvetica,sans-serif',
						text: text,
						fill: new ol.style.Fill({
							color: '#000'
						}),
						stroke: new ol.style.Stroke({
							color: '#fff',
							width: 2
						})
					})
				});

				return style;
			};
		}

		showSources (show) {
			this.sourcesLayer.setVisible(show);
			this.sourcesLabelLayer.setVisible(show);
		}

		init () {

			if(typeof ol === "undefined"){
				return;
			}

			this.elMap = $('#potree_map');
			this.elMap.draggable({ handle: $('#potree_map_header') });
			this.elMap.resizable();

			this.elTooltip = $(`<div style="position: relative; z-index: 100"></div>`);
			this.elMap.append(this.elTooltip);

			let extentsLayer = this.getExtentsLayer();
			let cameraLayer = this.getCameraLayer();
			this.getToolLayer();
			let sourcesLayer = this.getSourcesLayer();
			this.images360Layer = this.getImages360Layer();
			this.getSourcesLabelLayer();
			this.getAnnotationsLayer();

			let mousePositionControl = new ol.control.MousePosition({
				coordinateFormat: ol.coordinate.createStringXY(5),
				projection: 'EPSG:4326',
				undefinedHTML: '&nbsp;'
			});

			let _this = this;
			let DownloadSelectionControl = function (optOptions) {
				let options = optOptions || {};

				// TOGGLE TILES
				let btToggleTiles = document.createElement('button');
				btToggleTiles.innerHTML = 'T';
				btToggleTiles.addEventListener('click', () => {
					let visible = sourcesLayer.getVisible();
					_this.showSources(!visible);
				}, false);
				btToggleTiles.style.float = 'left';
				btToggleTiles.title = 'show / hide tiles';

				// DOWNLOAD SELECTED TILES
				let link = document.createElement('a');
				link.href = '#';
				link.download = 'list.txt';
				link.style.float = 'left';

				let button = document.createElement('button');
				button.innerHTML = 'D';
				link.appendChild(button);

				let handleDownload = (e) => {
					let features = selectedFeatures.getArray();

					let url = [document.location.protocol, '//', document.location.host, document.location.pathname].join('');

					if (features.length === 0) {
						alert('No tiles were selected. Select area with ctrl + left mouse button!');
						e.preventDefault();
						e.stopImmediatePropagation();
						return false;
					} else if (features.length === 1) {
						let feature = features[0];

						if (feature.source) {
							let cloudjsurl = feature.pointcloud.pcoGeometry.url;
							let sourceurl = new URL(url + '/../' + cloudjsurl + '/../source/' + feature.source.name);
							link.href = sourceurl.href;
							link.download = feature.source.name;
						}
					} else {
						let content = '';
						for (let i = 0; i < features.length; i++) {
							let feature = features[i];

							if (feature.source) {
								let cloudjsurl = feature.pointcloud.pcoGeometry.url;
								let sourceurl = new URL(url + '/../' + cloudjsurl + '/../source/' + feature.source.name);
								content += sourceurl.href + '\n';
							}
						}

						let uri = 'data:application/octet-stream;base64,' + btoa(content);
						link.href = uri;
						link.download = 'list_of_files.txt';
					}
				};

				button.addEventListener('click', handleDownload, false);

				// assemble container
				let element = document.createElement('div');
				element.className = 'ol-unselectable ol-control';
				element.appendChild(link);
				element.appendChild(btToggleTiles);
				element.style.bottom = '0.5em';
				element.style.left = '0.5em';
				element.title = 'Download file or list of selected tiles. Select tile with left mouse button or area using ctrl + left mouse.';

				ol.control.Control.call(this, {
					element: element,
					target: options.target
				});
			};
			ol.inherits(DownloadSelectionControl, ol.control.Control);

			this.map = new ol.Map({
				controls: ol.control.defaults({
					attributionOptions: ({
						collapsible: false
					})
				}).extend([
					// this.controls.zoomToExtent,
					new DownloadSelectionControl(),
					mousePositionControl
				]),
				layers: [
					new ol.layer.Tile({source: new ol.source.OSM()}),
					this.toolLayer,
					this.annotationsLayer,
					this.sourcesLayer,
					this.sourcesLabelLayer,
					this.images360Layer,
					extentsLayer,
					cameraLayer
				],
				target: 'potree_map_content',
				view: new ol.View({
					center: this.olCenter,
					zoom: 9
				})
			});

			// DRAGBOX / SELECTION
			this.dragBoxLayer = new ol.layer.Vector({
				source: new ol.source.Vector({}),
				style: new ol.style.Style({
					stroke: new ol.style.Stroke({
						color: 'rgba(0, 0, 255, 1)',
						width: 2
					})
				})
			});
			this.map.addLayer(this.dragBoxLayer);

			let select = new ol.interaction.Select();
			this.map.addInteraction(select);

			let selectedFeatures = select.getFeatures();

			let dragBox = new ol.interaction.DragBox({
				condition: ol.events.condition.platformModifierKeyOnly
			});

			this.map.addInteraction(dragBox);

			// this.map.on('pointermove', evt => {
			// 	let pixel = evt.pixel;
			// 	let feature = this.map.forEachFeatureAtPixel(pixel, function (feature) {
			// 		return feature;
			// 	});

			// 	// console.log(feature);
			// 	// this.elTooltip.css("display", feature ? '' : 'none');
			// 	this.elTooltip.css('display', 'none');
			// 	if (feature && feature.onHover) {
			// 		feature.onHover(evt);
			// 		// overlay.setPosition(evt.coordinate);
			// 		// tooltip.innerHTML = feature.get('name');
			// 	}
			// });

			this.map.on('click', evt => {
				let pixel = evt.pixel;
				let feature = this.map.forEachFeatureAtPixel(pixel, function (feature) {
					return feature;
				});

				if (feature && feature.onClick) {
					feature.onClick(evt);
				}
			});

			dragBox.on('boxend', (e) => {
				// features that intersect the box are added to the collection of
				// selected features, and their names are displayed in the "info"
				// div
				let extent = dragBox.getGeometry().getExtent();
				this.getSourcesLayer().getSource().forEachFeatureIntersectingExtent(extent, (feature) => {
					selectedFeatures.push(feature);
				});
			});

			// clear selection when drawing a new box and when clicking on the map
			dragBox.on('boxstart', (e) => {
				selectedFeatures.clear();
			});
			this.map.on('click', () => {
				selectedFeatures.clear();
			});

			this.viewer.addEventListener('scene_changed', e => {
				this.setScene(e.scene);
			});

			this.onPointcloudAdded = e => {
				this.load(e.pointcloud);
			};

			this.on360ImagesAdded = e => {
				this.addImages360(e.images);
			};

			this.onAnnotationAdded = e => {
				if (!this.sceneProjection) {
					return;
				}

				let annotation = e.annotation;
				let position = annotation.position;
				let mapPos = this.toMap.forward([position.x, position.y]);
				let feature = new ol.Feature({
					geometry: new ol.geom.Point(mapPos),
					name: annotation.title
				});
				feature.setStyle(this.createAnnotationStyle(annotation.title));

				feature.onHover = evt => {
					let coordinates = feature.getGeometry().getCoordinates();
					let p = this.map.getPixelFromCoordinate(coordinates);

					this.elTooltip.html(annotation.title);
					this.elTooltip.css('display', '');
					this.elTooltip.css('left', `${p[0]}px`);
					this.elTooltip.css('top', `${p[1]}px`);
				};

				feature.onClick = evt => {
					annotation.clickTitle();
				};

				this.getAnnotationsLayer().getSource().addFeature(feature);
			};

			this.setScene(this.viewer.scene);
		}

		setScene (scene) {
			if (this.scene === scene) {
				return;
			};

			if (this.scene) {
				this.scene.removeEventListener('pointcloud_added', this.onPointcloudAdded);
				this.scene.removeEventListener('360_images_added', this.on360ImagesAdded);
				this.scene.annotations.removeEventListener('annotation_added', this.onAnnotationAdded);
			}

			this.scene = scene;

			this.scene.addEventListener('pointcloud_added', this.onPointcloudAdded);
			this.scene.addEventListener('360_images_added', this.on360ImagesAdded);
			this.scene.annotations.addEventListener('annotation_added', this.onAnnotationAdded);

			for (let pointcloud of this.viewer.scene.pointclouds) {
				this.load(pointcloud);
			}

			this.viewer.scene.annotations.traverseDescendants(annotation => {
				this.onAnnotationAdded({annotation: annotation});
			});

			for(let images of this.viewer.scene.images360){
				this.on360ImagesAdded({images: images});
			}
		}

		getExtentsLayer () {
			if (this.extentsLayer) {
				return this.extentsLayer;
			}

			this.gExtent = new ol.geom.LineString([[0, 0], [0, 0]]);

			let feature = new ol.Feature(this.gExtent);
			let featureVector = new ol.source.Vector({
				features: [feature]
			});

			this.extentsLayer = new ol.layer.Vector({
				source: featureVector,
				style: new ol.style.Style({
					fill: new ol.style.Fill({
						color: 'rgba(255, 255, 255, 0.2)'
					}),
					stroke: new ol.style.Stroke({
						color: '#0000ff',
						width: 2
					}),
					image: new ol.style.Circle({
						radius: 3,
						fill: new ol.style.Fill({
							color: '#0000ff'
						})
					})
				})
			});

			return this.extentsLayer;
		}

		getAnnotationsLayer () {
			if (this.annotationsLayer) {
				return this.annotationsLayer;
			}

			this.annotationsLayer = new ol.layer.Vector({
				source: new ol.source.Vector({
				}),
				style: new ol.style.Style({
					fill: new ol.style.Fill({
						color: 'rgba(255, 0, 0, 1)'
					}),
					stroke: new ol.style.Stroke({
						color: 'rgba(255, 0, 0, 1)',
						width: 2
					})
				})
			});

			return this.annotationsLayer;
		}

		getCameraLayer () {
			if (this.cameraLayer) {
				return this.cameraLayer;
			}

			// CAMERA LAYER
			this.gCamera = new ol.geom.LineString([[0, 0], [0, 0], [0, 0], [0, 0]]);
			let feature = new ol.Feature(this.gCamera);
			let featureVector = new ol.source.Vector({
				features: [feature]
			});

			this.cameraLayer = new ol.layer.Vector({
				source: featureVector,
				style: new ol.style.Style({
					stroke: new ol.style.Stroke({
						color: '#0000ff',
						width: 2
					})
				})
			});

			return this.cameraLayer;
		}

		getToolLayer () {
			if (this.toolLayer) {
				return this.toolLayer;
			}

			this.toolLayer = new ol.layer.Vector({
				source: new ol.source.Vector({
				}),
				style: new ol.style.Style({
					fill: new ol.style.Fill({
						color: 'rgba(255, 0, 0, 1)'
					}),
					stroke: new ol.style.Stroke({
						color: 'rgba(255, 0, 0, 1)',
						width: 2
					})
				})
			});

			return this.toolLayer;
		}

		getImages360Layer(){
			if(this.images360Layer){
				return this.images360Layer;
			}

			let style = new ol.style.Style({
				image: new ol.style.Circle({
					radius: 4,
					stroke: new ol.style.Stroke({
						color: [255, 0, 0, 1],
						width: 2
					}),
					fill: new ol.style.Fill({
						color: [255, 100, 100, 1]
					})
				})
			});
			
			let layer = new ol.layer.Vector({
				source: new ol.source.Vector({}),
				style: style,
			});

			this.images360Layer = layer;

			return this.images360Layer;
		}

		getSourcesLayer () {
			if (this.sourcesLayer) {
				return this.sourcesLayer;
			}

			this.sourcesLayer = new ol.layer.Vector({
				source: new ol.source.Vector({}),
				style: new ol.style.Style({
					fill: new ol.style.Fill({
						color: 'rgba(0, 0, 150, 0.1)'
					}),
					stroke: new ol.style.Stroke({
						color: 'rgba(0, 0, 150, 1)',
						width: 1
					})
				})
			});

			return this.sourcesLayer;
		}

		getSourcesLabelLayer () {
			if (this.sourcesLabelLayer) {
				return this.sourcesLabelLayer;
			}

			this.sourcesLabelLayer = new ol.layer.Vector({
				source: new ol.source.Vector({
				}),
				style: new ol.style.Style({
					fill: new ol.style.Fill({
						color: 'rgba(255, 0, 0, 0.1)'
					}),
					stroke: new ol.style.Stroke({
						color: 'rgba(255, 0, 0, 1)',
						width: 2
					})
				}),
				minResolution: 0.01,
				maxResolution: 20
			});

			return this.sourcesLabelLayer;
		}

		setSceneProjection (sceneProjection) {
			this.sceneProjection = sceneProjection;
			this.toMap = proj4(this.sceneProjection, this.mapProjection);
			this.toScene = proj4(this.mapProjection, this.sceneProjection);
		};

		getMapExtent () {
			let bb = this.viewer.getBoundingBox();

			let bottomLeft = this.toMap.forward([bb.min.x, bb.min.y]);
			let bottomRight = this.toMap.forward([bb.max.x, bb.min.y]);
			let topRight = this.toMap.forward([bb.max.x, bb.max.y]);
			let topLeft = this.toMap.forward([bb.min.x, bb.max.y]);

			let extent = {
				bottomLeft: bottomLeft,
				bottomRight: bottomRight,
				topRight: topRight,
				topLeft: topLeft
			};

			return extent;
		};

		getMapCenter () {
			let mapExtent = this.getMapExtent();

			let mapCenter = [
				(mapExtent.bottomLeft[0] + mapExtent.topRight[0]) / 2,
				(mapExtent.bottomLeft[1] + mapExtent.topRight[1]) / 2
			];

			return mapCenter;
		};

		updateToolDrawings () {
			this.toolLayer.getSource().clear();

			let profiles = this.viewer.profileTool.profiles;
			for (let i = 0; i < profiles.length; i++) {
				let profile = profiles[i];
				let coordinates = [];

				for (let j = 0; j < profile.points.length; j++) {
					let point = profile.points[j];
					let pointMap = this.toMap.forward([point.x, point.y]);
					coordinates.push(pointMap);
				}

				let line = new ol.geom.LineString(coordinates);
				let feature = new ol.Feature(line);
				this.toolLayer.getSource().addFeature(feature);
			}

			let measurements = this.viewer.measuringTool.measurements;
			for (let i = 0; i < measurements.length; i++) {
				let measurement = measurements[i];
				let coordinates = [];

				for (let j = 0; j < measurement.points.length; j++) {
					let point = measurement.points[j].position;
					let pointMap = this.toMap.forward([point.x, point.y]);
					coordinates.push(pointMap);
				}

				if (measurement.closed && measurement.points.length > 0) {
					coordinates.push(coordinates[0]);
				}

				let line = new ol.geom.LineString(coordinates);
				let feature = new ol.Feature(line);
				this.toolLayer.getSource().addFeature(feature);
			}
		}

		addImages360(images){
			let transform = this.toMap.forward;
			let layer = this.getImages360Layer();

			for(let image of images.images){

				let p = transform([image.position[0], image.position[1]]);

				let feature = new ol.Feature({
					'geometry': new ol.geom.Point(p),
				});

				feature.onClick = () => {
					images.focus(image);
				};

				layer.getSource().addFeature(feature);
			}
		}

		async load (pointcloud) {
			if (!pointcloud) {
				return;
			}

			if (!pointcloud.projection) {
				return;
			}

			if (!this.sceneProjection) {
				try {
					this.setSceneProjection(pointcloud.projection);
				}catch (e) {
					console.log('Failed projection:', e);

					if (pointcloud.fallbackProjection) {
						try {
							console.log('Trying fallback projection...');
							this.setSceneProjection(pointcloud.fallbackProjection);
							console.log('Set projection from fallback');
						}catch (e) {
							console.log('Failed fallback projection:', e);
							return;
						}
					}else {
						return;
					};
				}
			}

			let mapExtent = this.getMapExtent();
			let mapCenter = this.getMapCenter();

			let view = this.map.getView();
			view.setCenter(mapCenter);

			this.gExtent.setCoordinates([
				mapExtent.bottomLeft,
				mapExtent.bottomRight,
				mapExtent.topRight,
				mapExtent.topLeft,
				mapExtent.bottomLeft
			]);

			view.fit(this.gExtent, [300, 300], {
				constrainResolution: false
			});

			if (pointcloud.pcoGeometry.type == 'ept'){ 
				return;
			}

			let url = `${pointcloud.pcoGeometry.url}/../sources.json`;
			//let response = await fetch(url);

			fetch(url).then(async (response) => {
				let data = await response.json();
			
				let sources = data.sources;

				for (let i = 0; i < sources.length; i++) {
					let source = sources[i];
					let name = source.name;
					let bounds = source.bounds;

					let mapBounds = {
						min: this.toMap.forward([bounds.min[0], bounds.min[1]]),
						max: this.toMap.forward([bounds.max[0], bounds.max[1]])
					};
					let mapCenter = [
						(mapBounds.min[0] + mapBounds.max[0]) / 2,
						(mapBounds.min[1] + mapBounds.max[1]) / 2
					];

					let p1 = this.toMap.forward([bounds.min[0], bounds.min[1]]);
					let p2 = this.toMap.forward([bounds.max[0], bounds.min[1]]);
					let p3 = this.toMap.forward([bounds.max[0], bounds.max[1]]);
					let p4 = this.toMap.forward([bounds.min[0], bounds.max[1]]);

					// let feature = new ol.Feature({
					//	'geometry': new ol.geom.LineString([p1, p2, p3, p4, p1])
					// });
					let feature = new ol.Feature({
						'geometry': new ol.geom.Polygon([[p1, p2, p3, p4, p1]])
					});
					feature.source = source;
					feature.pointcloud = pointcloud;
					this.getSourcesLayer().getSource().addFeature(feature);

					feature = new ol.Feature({
						geometry: new ol.geom.Point(mapCenter),
						name: name
					});
					feature.setStyle(this.createLabelStyle(name));
					this.sourcesLabelLayer.getSource().addFeature(feature);
				}
			}).catch(() => {
				
			});

		}

		toggle () {
			if (this.elMap.is(':visible')) {
				this.elMap.css('display', 'none');
				this.enabled = false;
			} else {
				this.elMap.css('display', 'block');
				this.enabled = true;
			}
		}

		update (delta) {
			if (!this.sceneProjection) {
				return;
			}

			let pm = $('#potree_map');

			if (!this.enabled) {
				return;
			}

			// resize
			let mapSize = this.map.getSize();
			let resized = (pm.width() !== mapSize[0] || pm.height() !== mapSize[1]);
			if (resized) {
				this.map.updateSize();
			}

			//
			let camera = this.viewer.scene.getActiveCamera();

			let scale = this.map.getView().getResolution();
			let campos = camera.position;
			let camdir = camera.getWorldDirection(new Vector3());
			let sceneLookAt = camdir.clone().multiplyScalar(30 * scale).add(campos);
			let geoPos = camera.position;
			let geoLookAt = sceneLookAt;
			let mapPos = new Vector2().fromArray(this.toMap.forward([geoPos.x, geoPos.y]));
			let mapLookAt = new Vector2().fromArray(this.toMap.forward([geoLookAt.x, geoLookAt.y]));
			let mapDir = new Vector2().subVectors(mapLookAt, mapPos).normalize();

			mapLookAt = mapPos.clone().add(mapDir.clone().multiplyScalar(30 * scale));
			let mapLength = mapPos.distanceTo(mapLookAt);
			let mapSide = new Vector2(-mapDir.y, mapDir.x);

			let p1 = mapPos.toArray();
			let p2 = mapLookAt.clone().sub(mapSide.clone().multiplyScalar(0.3 * mapLength)).toArray();
			let p3 = mapLookAt.clone().add(mapSide.clone().multiplyScalar(0.3 * mapLength)).toArray();

			this.gCamera.setCoordinates([p1, p2, p3, p1]);
		}

		get sourcesVisible () {
			return this.getSourcesLayer().getVisible();
		}

		set sourcesVisible (value) {
			this.getSourcesLayer().setVisible(value);
		}

	}

	class CSVExporter {
		static toString (points) {
			let string = '';

			let attributes = Object.keys(points.data)
				.filter(a => a !== 'normal')
				.sort((a, b) => {
					if (a === 'position') return -1;
					if (b === 'position') return 1;
					if (a === 'rgba') return -1;
					if (b === 'rgba') return 1;
				});

			let headerValues = [];
			for (let attribute of attributes) {
				let itemSize = points.data[attribute].length / points.numPoints;

				if (attribute === 'position') {
					headerValues = headerValues.concat(['x', 'y', 'z']);
				} else if (attribute === 'rgba') {
					headerValues = headerValues.concat(['r', 'g', 'b', 'a']);
				} else if (itemSize > 1) {
					for (let i = 0; i < itemSize; i++) {
						headerValues.push(`${attribute}_${i}`);
					}
				} else {
					headerValues.push(attribute);
				}
			}
			string = headerValues.join(', ') + '\n';

			for (let i = 0; i < points.numPoints; i++) {
				let values = [];

				for (let attribute of attributes) {
					let itemSize = points.data[attribute].length / points.numPoints;
					let value = points.data[attribute]
						.subarray(itemSize * i, itemSize * i + itemSize)
						.join(', ');
					values.push(value);
				}

				string += values.join(', ') + '\n';
			}

			return string;
		}
	};

	class LASExporter {
		static toLAS (points) {
			// TODO Unused: let string = '';

			let boundingBox = points.boundingBox;
			let offset = boundingBox.min.clone();
			let diagonal = boundingBox.min.distanceTo(boundingBox.max);
			let scale = new Vector3(0.001, 0.001, 0.001);
			if (diagonal > 1000 * 1000) {
				scale = new Vector3(0.01, 0.01, 0.01);
			} else {
				scale = new Vector3(0.001, 0.001, 0.001);
			}

			let setString = function (string, offset, buffer) {
				let view = new Uint8Array(buffer);

				for (let i = 0; i < string.length; i++) {
					let charCode = string.charCodeAt(i);
					view[offset + i] = charCode;
				}
			};

			let buffer = new ArrayBuffer(227 + 28 * points.numPoints);
			let view = new DataView(buffer);
			let u8View = new Uint8Array(buffer);
			// let u16View = new Uint16Array(buffer);

			setString('LASF', 0, buffer);
			u8View[24] = 1;
			u8View[25] = 2;

			// system identifier o:26 l:32

			// generating software o:58 l:32
			setString('Potree 1.7', 58, buffer);

			// file creation day of year o:90 l:2
			// file creation year o:92 l:2

			// header size o:94 l:2
			view.setUint16(94, 227, true);

			// offset to point data o:96 l:4
			view.setUint32(96, 227, true);

			// number of letiable length records o:100 l:4

			// point data record format 104 1
			u8View[104] = 2;

			// point data record length 105 2
			view.setUint16(105, 28, true);

			// number of point records 107 4
			view.setUint32(107, points.numPoints, true);

			// number of points by return 111 20

			// x scale factor 131 8
			view.setFloat64(131, scale.x, true);

			// y scale factor 139 8
			view.setFloat64(139, scale.y, true);

			// z scale factor 147 8
			view.setFloat64(147, scale.z, true);

			// x offset 155 8
			view.setFloat64(155, offset.x, true);

			// y offset 163 8
			view.setFloat64(163, offset.y, true);

			// z offset 171 8
			view.setFloat64(171, offset.z, true);

			// max x 179 8
			view.setFloat64(179, boundingBox.max.x, true);

			// min x 187 8
			view.setFloat64(187, boundingBox.min.x, true);

			// max y 195 8
			view.setFloat64(195, boundingBox.max.y, true);

			// min y 203 8
			view.setFloat64(203, boundingBox.min.y, true);

			// max z 211 8
			view.setFloat64(211, boundingBox.max.z, true);

			// min z 219 8
			view.setFloat64(219, boundingBox.min.z, true);

			let boffset = 227;
			for (let i = 0; i < points.numPoints; i++) {

				let px = points.data.position[3 * i + 0];
				let py = points.data.position[3 * i + 1];
				let pz = points.data.position[3 * i + 2];

				let ux = parseInt((px - offset.x) / scale.x);
				let uy = parseInt((py - offset.y) / scale.y);
				let uz = parseInt((pz - offset.z) / scale.z);

				view.setUint32(boffset + 0, ux, true);
				view.setUint32(boffset + 4, uy, true);
				view.setUint32(boffset + 8, uz, true);

				if (points.data.intensity) {
					view.setUint16(boffset + 12, (points.data.intensity[i]), true);
				}

				let rt = 0;
				if (points.data.returnNumber) {
					rt += points.data.returnNumber[i];
				}
				if (points.data.numberOfReturns) {
					rt += (points.data.numberOfReturns[i] << 3);
				}
				view.setUint8(boffset + 14, rt);

				if (points.data.classification) {
					view.setUint8(boffset + 15, points.data.classification[i]);
				}
				// scan angle rank
				// user data
				// point source id
				if (points.data.pointSourceID) {
					view.setUint16(boffset + 18, points.data.pointSourceID[i]);
				}

				if (points.data.rgba) {
					let rgba = points.data.rgba;
					view.setUint16(boffset + 20, (rgba[4 * i + 0] * 255), true);
					view.setUint16(boffset + 22, (rgba[4 * i + 1] * 255), true);
					view.setUint16(boffset + 24, (rgba[4 * i + 2] * 255), true);
				}

				boffset += 28;
			}

			return buffer;
		}
		
	}

	function copyMaterial(source, target){

		for(let name of Object.keys(target.uniforms)){
			target.uniforms[name].value = source.uniforms[name].value;
		}

		target.gradientTexture = source.gradientTexture;
		target.visibleNodesTexture = source.visibleNodesTexture;
		target.classificationTexture = source.classificationTexture;
		target.matcapTexture = source.matcapTexture;

		target.activeAttributeName = source.activeAttributeName;
		target.ranges = source.ranges;

		//target.updateShaderSource();
	}


	class Batch{

		constructor(geometry, material){
			this.geometry = geometry;
			this.material = material;

			this.sceneNode = new Points(geometry, material);

			this.geometryNode = {
				estimatedSpacing: 1.0,
				geometry: geometry,
			};
		}

		getLevel(){
			return 0;
		}

	}

	class ProfileFakeOctree extends PointCloudTree{

		constructor(octree){
			super();

			this.trueOctree = octree;
			this.pcoGeometry = octree.pcoGeometry;
			this.points = [];
			this.visibleNodes = [];
			
			//this.material = this.trueOctree.material;
			this.material = new PointCloudMaterial$1();
			//this.material.copy(this.trueOctree.material);
			copyMaterial(this.trueOctree.material, this.material);
			this.material.pointSizeType = PointSizeType.FIXED;

			this.batchSize = 100 * 1000;
			this.currentBatch = null;
		}

		getAttribute(name){
			return this.trueOctree.getAttribute(name);
		}

		dispose(){
			for(let node of this.visibleNodes){
				node.geometry.dispose();
			}

			this.visibleNodes = [];
			this.currentBatch = null;
			this.points = [];
		}

		addPoints(data){
			// since each call to addPoints can deliver very very few points,
			// we're going to batch them into larger buffers for efficiency.

			if(this.currentBatch === null){
				this.currentBatch = this.createNewBatch(data);
			}

			this.points.push(data);


			let updateRange = {
				start: this.currentBatch.geometry.drawRange.count,
				count: 0
			};
			let projectedBox = new Box3();

			let truePos = new Vector3();

			for(let i = 0; i < data.numPoints; i++){

				if(updateRange.start + updateRange.count >= this.batchSize){
					// current batch full, start new batch

					for(let key of Object.keys(this.currentBatch.geometry.attributes)){
						let attribute = this.currentBatch.geometry.attributes[key];
						attribute.updateRange.offset = updateRange.start;
						attribute.updateRange.count = updateRange.count;
						attribute.needsUpdate = true;
					}

					this.currentBatch.geometry.computeBoundingBox();
					this.currentBatch.geometry.computeBoundingSphere();

					this.currentBatch = this.createNewBatch(data);
					updateRange = {
						start: 0,
						count: 0
					};
				}

				truePos.set(
					data.data.position[3 * i + 0] + this.trueOctree.position.x,
					data.data.position[3 * i + 1] + this.trueOctree.position.y,
					data.data.position[3 * i + 2] + this.trueOctree.position.z,
				);

				let x = data.data.mileage[i];
				let y = 0;
				let z = truePos.z;

				projectedBox.expandByPoint(new Vector3(x, y, z));

				let index = updateRange.start + updateRange.count;
				let geometry = this.currentBatch.geometry;

				for(let attributeName of Object.keys(data.data)){
					let source = data.data[attributeName];
					let target = geometry.attributes[attributeName];
					let numElements = target.itemSize;
					
					for(let item = 0; item < numElements; item++){
						target.array[numElements * index + item] = source[numElements * i + item];
					}
				}

				{
					let position = geometry.attributes.position;

					position.array[3 * index + 0] = x;
					position.array[3 * index + 1] = y;
					position.array[3 * index + 2] = z;
				}

				updateRange.count++;
				this.currentBatch.geometry.drawRange.count++;
			}

			for(let key of Object.keys(this.currentBatch.geometry.attributes)){
				let attribute = this.currentBatch.geometry.attributes[key];
				attribute.updateRange.offset = updateRange.start;
				attribute.updateRange.count = updateRange.count;
				attribute.needsUpdate = true;
			}

			data.projectedBox = projectedBox;

			this.projectedBox = this.points.reduce( (a, i) => a.union(i.projectedBox), new Box3());
		}

		createNewBatch(data){
			let geometry = new BufferGeometry();

			// create new batches with batch_size elements of the same type as the attribute
			for(let attributeName of Object.keys(data.data)){
				let buffer = data.data[attributeName];
				let numElements = buffer.length / data.numPoints; // 3 for pos, 4 for col, 1 for scalars
				let constructor = buffer.constructor;
				let normalized = false;
				
				if(this.trueOctree.root.sceneNode){
					if(this.trueOctree.root.sceneNode.geometry.attributes[attributeName]){
						normalized = this.trueOctree.root.sceneNode.geometry.attributes[attributeName].normalized;
					}
				}
				

				let batchBuffer = new constructor(numElements * this.batchSize);

				let bufferAttribute = new BufferAttribute(batchBuffer, numElements, normalized);
				bufferAttribute.potree = {
					range: [0, 1],
				};

				geometry.setAttribute(attributeName, bufferAttribute);
			}

			geometry.drawRange.start = 0;
			geometry.drawRange.count = 0;

			let batch = new Batch(geometry, this.material);

			this.visibleNodes.push(batch);

			return batch;
		}
		
		computeVisibilityTextureData(){
			let data = new Uint8Array(this.visibleNodes.length * 4);
			let offsets = new Map();

			for(let i = 0; i < this.visibleNodes.length; i++){
				let node = this.visibleNodes[i];

				offsets[node] = i;
			}


			return {
				data: data,
				offsets: offsets,
			};
		}

	}

	class ProfileWindow extends EventDispatcher {
		constructor (viewer) {
			super();

			this.viewer = viewer;
			this.elRoot = $('#profile_window');
			this.renderArea = this.elRoot.find('#profileCanvasContainer');
			this.svg = d3.select('svg#profileSVG');
			this.mouseIsDown = false;

			this.projectedBox = new Box3();
			this.pointclouds = new Map();
			this.numPoints = 0;
			this.lastAddPointsTimestamp = undefined;

			this.mouse = new Vector2(0, 0);
			this.scale = new Vector3(1, 1, 1);

			this.autoFitEnabled = true; // completely disable/enable
			this.autoFit = false; // internal

			let cwIcon = `${exports.resourcePath}/icons/arrow_cw.svg`;
			$('#potree_profile_rotate_cw').attr('src', cwIcon);

			let ccwIcon = `${exports.resourcePath}/icons/arrow_ccw.svg`;
			$('#potree_profile_rotate_ccw').attr('src', ccwIcon);
			
			let forwardIcon = `${exports.resourcePath}/icons/arrow_up.svg`;
			$('#potree_profile_move_forward').attr('src', forwardIcon);

			let backwardIcon = `${exports.resourcePath}/icons/arrow_down.svg`;
			$('#potree_profile_move_backward').attr('src', backwardIcon);

			let csvIcon = `${exports.resourcePath}/icons/file_csv_2d.svg`;
			$('#potree_download_csv_icon').attr('src', csvIcon);

			let lasIcon = `${exports.resourcePath}/icons/file_las_3d.svg`;
			$('#potree_download_las_icon').attr('src', lasIcon);

			let closeIcon = `${exports.resourcePath}/icons/close.svg`;
			$('#closeProfileContainer').attr("src", closeIcon);

			this.initTHREE();
			this.initSVG();
			this.initListeners();

			this.pRenderer = new Renderer(this.renderer);

			this.elRoot.i18n();
		}

		initListeners () {
			$(window).resize(() => {
				if (this.enabled) {
				this.render();
				}
			});

			this.renderArea.mousedown(e => {
				this.mouseIsDown = true;
			});

			this.renderArea.mouseup(e => {
				this.mouseIsDown = false;
			});

			let viewerPickSphereSizeHandler = () => {
				let camera = this.viewer.scene.getActiveCamera();
				let domElement = this.viewer.renderer.domElement;
				let distance = this.viewerPickSphere.position.distanceTo(camera.position);
				let pr = Utils.projectedRadius(1, camera, distance, domElement.clientWidth, domElement.clientHeight);
				let scale = (10 / pr);
				this.viewerPickSphere.scale.set(scale, scale, scale);
			};

			this.renderArea.mousemove(e => {
				if (this.pointclouds.size === 0) {
					return;
				}

				let rect = this.renderArea[0].getBoundingClientRect();
				let x = e.clientX - rect.left;
				let y = e.clientY - rect.top;

				let newMouse = new Vector2(x, y);

				if (this.mouseIsDown) {
					// DRAG
					this.autoFit = false;
					this.lastDrag = new Date().getTime();

					let cPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)];
					let ncPos = [this.scaleX.invert(newMouse.x), this.scaleY.invert(newMouse.y)];

					this.camera.position.x -= ncPos[0] - cPos[0];
					this.camera.position.z -= ncPos[1] - cPos[1];

					this.render();
				} else if (this.pointclouds.size > 0) {
					// FIND HOVERED POINT
					let radius = Math.abs(this.scaleX.invert(0) - this.scaleX.invert(40));
					let mileage = this.scaleX.invert(newMouse.x);
					let elevation = this.scaleY.invert(newMouse.y);

					let closest = this.selectPoint(mileage, elevation, radius);

					if (closest) {
						let point = closest.point;

						let position = new Float64Array([
							point.position[0] + closest.pointcloud.position.x,
							point.position[1] + closest.pointcloud.position.y,
							point.position[2] + closest.pointcloud.position.z
						]);

						this.elRoot.find('#profileSelectionProperties').fadeIn(200);
						this.pickSphere.visible = true;
						this.pickSphere.scale.set(0.5 * radius, 0.5 * radius, 0.5 * radius);
						this.pickSphere.position.set(point.mileage, 0, position[2]);

						this.viewerPickSphere.position.set(...position);
						
						if(!this.viewer.scene.scene.children.includes(this.viewerPickSphere)){
							this.viewer.scene.scene.add(this.viewerPickSphere);
							if(!this.viewer.hasEventListener("update", viewerPickSphereSizeHandler)){
								this.viewer.addEventListener("update", viewerPickSphereSizeHandler);
							}
						}
						

						let info = this.elRoot.find('#profileSelectionProperties');
						let html = '<table>';

						for (let attributeName of Object.keys(point)) {

							let value = point[attributeName];
							let attribute = closest.pointcloud.getAttribute(attributeName);

							let transform = value => value;
							if(attribute && attribute.type.size > 4){
								let range = attribute.initialRange;
								let scale = 1 / (range[1] - range[0]);
								let offset = range[0];
								transform = value => value / scale + offset;
							}

							

							

							if (attributeName === 'position') {
								let values = [...position].map(v => Utils.addCommas(v.toFixed(3)));
								html += `
								<tr>
									<td>x</td>
									<td>${values[0]}</td>
								</tr>
								<tr>
									<td>y</td>
									<td>${values[1]}</td>
								</tr>
								<tr>
									<td>z</td>
									<td>${values[2]}</td>
								</tr>`;
							} else if (attributeName === 'rgba') {
								html += `
								<tr>
									<td>${attributeName}</td>
									<td>${value.join(', ')}</td>
								</tr>`;
							} else if (attributeName === 'normal') {
								continue;
							} else if (attributeName === 'mileage') {
								html += `
								<tr>
									<td>${attributeName}</td>
									<td>${value.toFixed(3)}</td>
								</tr>`;
							} else {
								html += `
								<tr>
									<td>${attributeName}</td>
									<td>${transform(value)}</td>
								</tr>`;
							}
						}
						html += '</table>';
						info.html(html);

						this.selectedPoint = point;
					} else {
						// this.pickSphere.visible = false;
						// this.selectedPoint = null;

						this.viewer.scene.scene.add(this.viewerPickSphere);

						let index = this.viewer.scene.scene.children.indexOf(this.viewerPickSphere);
						if(index >= 0){
							this.viewer.scene.scene.children.splice(index, 1);
						}
						this.viewer.removeEventListener("update", viewerPickSphereSizeHandler);
						

					}
					this.render();
				}

				this.mouse.copy(newMouse);
			});

			let onWheel = e => {
				this.autoFit = false;

				let delta = 0;
				if (e.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9
					delta = e.wheelDelta;
				} else if (e.detail !== undefined) { // Firefox
					delta = -e.detail;
				}

				let ndelta = Math.sign(delta);

				let cPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)];

				if (ndelta > 0) {
					// + 10%
					this.scale.multiplyScalar(1.1);
				} else {
					// - 10%
					this.scale.multiplyScalar(100 / 110);
				}

				this.updateScales();
				let ncPos = [this.scaleX.invert(this.mouse.x), this.scaleY.invert(this.mouse.y)];

				this.camera.position.x -= ncPos[0] - cPos[0];
				this.camera.position.z -= ncPos[1] - cPos[1];

				this.render();
				this.updateScales();
			};
			$(this.renderArea)[0].addEventListener('mousewheel', onWheel, false);
			$(this.renderArea)[0].addEventListener('DOMMouseScroll', onWheel, false); // Firefox

			$('#closeProfileContainer').click(() => {
				this.hide();
			});

			let getProfilePoints = () => {
				let points = new Points$1();
				
				for(let [pointcloud, entry] of this.pointclouds){
					for(let pointSet of entry.points){

						let originPos = pointSet.data.position;
						let trueElevationPosition = new Float32Array(originPos);
						for(let i = 0; i < pointSet.numPoints; i++){
							trueElevationPosition[3 * i + 2] += pointcloud.position.z;
						}

						pointSet.data.position = trueElevationPosition;
						points.add(pointSet);
						pointSet.data.position = originPos;
					}
				}

				return points;
			};

			$('#potree_download_csv_icon').click(() => {
				
				let points = getProfilePoints();

				let string = CSVExporter.toString(points);

				let blob = new Blob([string], {type: "text/string"});
				$('#potree_download_profile_ortho_link').attr('href', URL.createObjectURL(blob));
			});

			$('#potree_download_las_icon').click(() => {

				let points = getProfilePoints();

				let buffer = LASExporter.toLAS(points);

				let blob = new Blob([buffer], {type: "application/octet-binary"});
				$('#potree_download_profile_link').attr('href', URL.createObjectURL(blob));
			});
		}

		selectPoint (mileage, elevation, radius) {
			let closest = {
				distance: Infinity,
				pointcloud: null,
				points: null,
				index: null
			};

			let pointBox = new Box2(
				new Vector2(mileage - radius, elevation - radius),
				new Vector2(mileage + radius, elevation + radius));

			let numTested = 0;
			let numSkipped = 0;
			let numTestedPoints = 0;
			let numSkippedPoints = 0;

			for (let [pointcloud, entry] of this.pointclouds) {
				for(let points of entry.points){

					let collisionBox = new Box2(
						new Vector2(points.projectedBox.min.x, points.projectedBox.min.z),
						new Vector2(points.projectedBox.max.x, points.projectedBox.max.z)
					);

					let intersects = collisionBox.intersectsBox(pointBox);

					if(!intersects){
						numSkipped++;
						numSkippedPoints += points.numPoints;
						continue;
					}

					numTested++;
					numTestedPoints += points.numPoints;

					for (let i = 0; i < points.numPoints; i++) {

						let m = points.data.mileage[i] - mileage;
						let e = points.data.position[3 * i + 2] - elevation + pointcloud.position.z;
						let r = Math.sqrt(m * m + e * e);

						const withinDistance = r < radius && r < closest.distance;
						let unfilteredClass = true;

						if(points.data.classification){
							const classification = pointcloud.material.classification;

							const pointClassID = points.data.classification[i];
							const pointClassValue = classification[pointClassID];

							if(pointClassValue && (!pointClassValue.visible || pointClassValue.color.w === 0)){
								unfilteredClass = false;
							}
						}

						if (withinDistance && unfilteredClass) {
							closest = {
								distance: r,
								pointcloud: pointcloud,
								points: points,
								index: i
							};
						}
					}
				}
			}


			//console.log(`nodes: ${numTested}, ${numSkipped} || points: ${numTestedPoints}, ${numSkippedPoints}`);

			if (closest.distance < Infinity) {
				let points = closest.points;

				let point = {};

				let attributes = Object.keys(points.data);
				for (let attribute of attributes) {
					let attributeData = points.data[attribute];
					let itemSize = attributeData.length / points.numPoints;
					let value = attributeData.subarray(itemSize * closest.index, itemSize * closest.index + itemSize);

					if (value.length === 1) {
						point[attribute] = value[0];
					} else {
						point[attribute] = value;
					}
				}

				closest.point = point;

				return closest;
			} else {
				return null;
			}
		}

		initTHREE () {
			this.renderer = new WebGLRenderer({alpha: true, premultipliedAlpha: false});
			this.renderer.setClearColor(0x000000, 0);
			this.renderer.setSize(10, 10);
			this.renderer.autoClear = false;
			this.renderArea.append($(this.renderer.domElement));
			this.renderer.domElement.tabIndex = '2222';
			$(this.renderer.domElement).css('width', '100%');
			$(this.renderer.domElement).css('height', '100%');


			{
				let gl = this.renderer.getContext();

				if(gl.createVertexArray == null){
					let extVAO = gl.getExtension('OES_vertex_array_object');

					if(!extVAO){
						throw new Error("OES_vertex_array_object extension not supported");
					}

					gl.createVertexArray = extVAO.createVertexArrayOES.bind(extVAO);
					gl.bindVertexArray = extVAO.bindVertexArrayOES.bind(extVAO);
				}
				
			}

			this.camera = new OrthographicCamera(-1000, 1000, 1000, -1000, -1000, 1000);
			this.camera.up.set(0, 0, 1);
			this.camera.rotation.order = "ZXY";
			this.camera.rotation.x = Math.PI / 2.0;
		

			this.scene = new Scene();
			this.profileScene = new Scene();

			let sg = new SphereGeometry(1, 16, 16);
			let sm = new MeshNormalMaterial();
			this.pickSphere = new Mesh(sg, sm);
			this.scene.add(this.pickSphere);

			{
				const sg = new SphereGeometry(2);
				const sm = new MeshNormalMaterial();
				const s = new Mesh(sg, sm);

				s.position.set(589530.450, 231398.860, 769.735);

				this.scene.add(s);
			}

			this.viewerPickSphere = new Mesh(sg, sm);
		}

		initSVG () {
			let width = this.renderArea[0].clientWidth;
			let height = this.renderArea[0].clientHeight;
			let marginLeft = this.renderArea[0].offsetLeft;

			this.svg.selectAll('*').remove();

			this.scaleX = d3.scale.linear()
				.domain([this.camera.left + this.camera.position.x, this.camera.right + this.camera.position.x])
				.range([0, width]);
			this.scaleY = d3.scale.linear()
				.domain([this.camera.bottom + this.camera.position.z, this.camera.top + this.camera.position.z])
				.range([height, 0]);

			this.xAxis = d3.svg.axis()
				.scale(this.scaleX)
				.orient('bottom')
				.innerTickSize(-height)
				.outerTickSize(1)
				.tickPadding(10)
				.ticks(width / 50);

			this.yAxis = d3.svg.axis()
				.scale(this.scaleY)
				.orient('left')
				.innerTickSize(-width)
				.outerTickSize(1)
				.tickPadding(10)
				.ticks(height / 20);

			this.elXAxis = this.svg.append('g')
				.attr('class', 'x axis')
				.attr('transform', `translate(${marginLeft}, ${height})`)
				.call(this.xAxis);

			this.elYAxis = this.svg.append('g')
				.attr('class', 'y axis')
				.attr('transform', `translate(${marginLeft}, 0)`)
				.call(this.yAxis);
		}

		addPoints (pointcloud, points) {

			if(points.numPoints === 0){
				return;
			}

			let entry = this.pointclouds.get(pointcloud);
			if(!entry){
				entry = new ProfileFakeOctree(pointcloud);
				this.pointclouds.set(pointcloud, entry);
				this.profileScene.add(entry);

				let materialChanged = () => {
					this.render();
				};

				materialChanged();

				pointcloud.material.addEventListener('material_property_changed', materialChanged);
				this.addEventListener("on_reset_once", () => {
					pointcloud.material.removeEventListener('material_property_changed', materialChanged);
				});
			}

			entry.addPoints(points);
			this.projectedBox.union(entry.projectedBox);

			if (this.autoFit && this.autoFitEnabled) { 
				let width = this.renderArea[0].clientWidth;
				let height = this.renderArea[0].clientHeight;

				let size = this.projectedBox.getSize(new Vector3());

				let sx = width / size.x;
				let sy = height / size.z;
				let scale = Math.min(sx, sy);

				let center = this.projectedBox.getCenter(new Vector3());
				this.scale.set(scale, scale, 1);
				this.camera.position.copy(center);

				//console.log("camera: ", this.camera.position.toArray().join(", "));
			}

			//console.log(entry);

			this.render();

			let numPoints = 0;
			for (let [key, value] of this.pointclouds.entries()) {
				numPoints += value.points.reduce( (a, i) => a + i.numPoints, 0);
			}
			$(`#profile_num_points`).html(Utils.addCommas(numPoints));

		}

		reset () {
			this.lastReset = new Date().getTime();

			this.dispatchEvent({type: "on_reset_once"});
			this.removeEventListeners("on_reset_once");

			this.autoFit = true;
			this.projectedBox = new Box3();

			for(let [key, entry] of this.pointclouds){
				entry.dispose();
			}

			this.pointclouds.clear();
			this.mouseIsDown = false;
			this.mouse.set(0, 0);

			if(this.autoFitEnabled){
				this.scale.set(1, 1, 1);
			}
			this.pickSphere.visible = false;

			this.elRoot.find('#profileSelectionProperties').hide();

			this.render();
		}

		show () {
			this.elRoot.fadeIn();
			this.enabled = true;
		}

		hide () {
			this.elRoot.fadeOut();
			this.enabled = false;
		}

		updateScales () {

			let width = this.renderArea[0].clientWidth;
			let height = this.renderArea[0].clientHeight;

			let left = (-width / 2) / this.scale.x;
			let right = (+width / 2) / this.scale.x;
			let top = (+height / 2) / this.scale.y;
			let bottom = (-height / 2) / this.scale.y;

			this.camera.left = left;
			this.camera.right = right;
			this.camera.top = top;
			this.camera.bottom = bottom;
			this.camera.updateProjectionMatrix();

			this.scaleX.domain([this.camera.left + this.camera.position.x, this.camera.right + this.camera.position.x])
				.range([0, width]);
			this.scaleY.domain([this.camera.bottom + this.camera.position.z, this.camera.top + this.camera.position.z])
				.range([height, 0]);

			let marginLeft = this.renderArea[0].offsetLeft;

			this.xAxis.scale(this.scaleX)
				.orient('bottom')
				.innerTickSize(-height)
				.outerTickSize(1)
				.tickPadding(10)
				.ticks(width / 50);
			this.yAxis.scale(this.scaleY)
				.orient('left')
				.innerTickSize(-width)
				.outerTickSize(1)
				.tickPadding(10)
				.ticks(height / 20);


			this.elXAxis
				.attr('transform', `translate(${marginLeft}, ${height})`)
				.call(this.xAxis);
			this.elYAxis
				.attr('transform', `translate(${marginLeft}, 0)`)
				.call(this.yAxis);
		}

		requestScaleUpdate(){

			let threshold = 100;
			let allowUpdate = ((this.lastReset === undefined) || (this.lastScaleUpdate === undefined)) 
				|| ((new Date().getTime() - this.lastReset) > threshold && (new Date().getTime() - this.lastScaleUpdate) > threshold);

			if(allowUpdate){

				this.updateScales();

				this.lastScaleUpdate = new Date().getTime();

				

				this.scaleUpdatePending = false;
			}else if(!this.scaleUpdatePending) {
				setTimeout(this.requestScaleUpdate.bind(this), 100);
				this.scaleUpdatePending = true;
			}
			
		}

		render () {
			let width = this.renderArea[0].clientWidth;
			let height = this.renderArea[0].clientHeight;

			let {renderer, pRenderer, camera, profileScene, scene} = this;
			let {scaleX, pickSphere} = this;

			renderer.setSize(width, height);

			renderer.setClearColor(0x000000, 0);
			renderer.clear(true, true, false);

			for(let pointcloud of this.pointclouds.keys()){
				let source = pointcloud.material;
				let target = this.pointclouds.get(pointcloud).material;
				
				copyMaterial(source, target);
				target.size = 2;
			}
			
			pRenderer.render(profileScene, camera, null);

			let radius = Math.abs(scaleX.invert(0) - scaleX.invert(5));

			if (radius === 0) {
				pickSphere.visible = false;
			} else {
				pickSphere.scale.set(radius, radius, radius);
				pickSphere.visible = true;
			}
			
			renderer.render(scene, camera);

			this.requestScaleUpdate();
		}
	};

	class ProfileWindowController {
		constructor (viewer) {
			this.viewer = viewer;
			this.profileWindow = viewer.profileWindow;
			this.profile = null;
			this.numPoints = 0;
			this.threshold = 60 * 1000;
			this.rotateAmount = 10;

			this.scheduledRecomputeTime = null;

			this.enabled = true;

			this.requests = [];

			this._recompute = () => { this.recompute(); };

			this.viewer.addEventListener("scene_changed", e => {
				e.oldScene.removeEventListener("pointcloud_added", this._recompute);
				e.scene.addEventListener("pointcloud_added", this._recompute);
			});
			this.viewer.scene.addEventListener("pointcloud_added", this._recompute);

			$("#potree_profile_rotate_amount").val(parseInt(this.rotateAmount));
			$("#potree_profile_rotate_amount").on("input", (e) => {
				const str = $("#potree_profile_rotate_amount").val();

				if(!isNaN(str)){
					const value = parseFloat(str);
					this.rotateAmount = value;
					$("#potree_profile_rotate_amount").css("background-color", "");
				}else {
					$("#potree_profile_rotate_amount").css("background-color", "#ff9999");
				}

			});

			const rotate = (radians) => {
				const profile = this.profile;
				const points = profile.points;
				const start = points[0];
				const end = points[points.length - 1];
				const center = start.clone().add(end).multiplyScalar(0.5);

				const mMoveOrigin = new Matrix4().makeTranslation(-center.x, -center.y, -center.z);
				const mRotate = new Matrix4().makeRotationZ(radians);
				const mMoveBack = new Matrix4().makeTranslation(center.x, center.y, center.z);
				//const transform = mMoveOrigin.multiply(mRotate).multiply(mMoveBack);
				const transform = mMoveBack.multiply(mRotate).multiply(mMoveOrigin);

				const rotatedPoints = points.map( point => point.clone().applyMatrix4(transform) );

				this.profileWindow.autoFitEnabled = false;

				for(let i = 0; i < points.length; i++){
					profile.setPosition(i, rotatedPoints[i]);
				}
			};

			$("#potree_profile_rotate_cw").click( () => {
				const radians = MathUtils.degToRad(this.rotateAmount);
				rotate(-radians);
			});

			$("#potree_profile_rotate_ccw").click( () => {
				const radians = MathUtils.degToRad(this.rotateAmount);
				rotate(radians);
			});

			$("#potree_profile_move_forward").click( () => {
				const profile = this.profile;
				const points = profile.points;
				const start = points[0];
				const end = points[points.length - 1];

				const dir = end.clone().sub(start).normalize();
				const up = new Vector3(0, 0, 1);
				const forward = up.cross(dir);
				const move = forward.clone().multiplyScalar(profile.width / 2);

				this.profileWindow.autoFitEnabled = false;

				for(let i = 0; i < points.length; i++){
					profile.setPosition(i, points[i].clone().add(move));
				}
			});

			$("#potree_profile_move_backward").click( () => {
				const profile = this.profile;
				const points = profile.points;
				const start = points[0];
				const end = points[points.length - 1];

				const dir = end.clone().sub(start).normalize();
				const up = new Vector3(0, 0, 1);
				const forward = up.cross(dir);
				const move = forward.clone().multiplyScalar(-profile.width / 2);

				this.profileWindow.autoFitEnabled = false;

				for(let i = 0; i < points.length; i++){
					profile.setPosition(i, points[i].clone().add(move));
				}
			});
		}

		setProfile (profile) {
			if (this.profile !== null && this.profile !== profile) {
				this.profile.removeEventListener('marker_moved', this._recompute);
				this.profile.removeEventListener('marker_added', this._recompute);
				this.profile.removeEventListener('marker_removed', this._recompute);
				this.profile.removeEventListener('width_changed', this._recompute);
			}

			this.profile = profile;

			{
				this.profile.addEventListener('marker_moved', this._recompute);
				this.profile.addEventListener('marker_added', this._recompute);
				this.profile.addEventListener('marker_removed', this._recompute);
				this.profile.addEventListener('width_changed', this._recompute);
			}

			this.recompute();
		}

		reset () {
			this.profileWindow.reset();

			this.numPoints = 0;

			if (this.profile) {
				for (let request of this.requests) {
					request.cancel();
				}
			}
		}

		progressHandler (pointcloud, progress) {
			for (let segment of progress.segments) {
				this.profileWindow.addPoints(pointcloud, segment.points);
				this.numPoints += segment.points.numPoints;
			}
		}

		cancel () {
			for (let request of this.requests) {
				request.cancel();
				// request.finishLevelThenCancel();
			}

			this.requests = [];
		};

		finishLevelThenCancel(){
			for (let request of this.requests) {
				request.finishLevelThenCancel();
			}

			this.requests = [];
		}

		recompute () {
			if (!this.profile) {
				return;
			}

			if (this.scheduledRecomputeTime !== null && this.scheduledRecomputeTime > new Date().getTime()) {
				return;
			} else {
				this.scheduledRecomputeTime = new Date().getTime() + 100;
			}
			this.scheduledRecomputeTime = null;

			this.reset();

			for (let pointcloud of this.viewer.scene.pointclouds.filter(p => p.visible)) {
				let request = pointcloud.getPointsInProfile(this.profile, null, {
					'onProgress': (event) => {
						if (!this.enabled) {
							return;
						}

						this.progressHandler(pointcloud, event.points);

						if (this.numPoints > this.threshold) {
							this.finishLevelThenCancel();
						}
					},
					'onFinish': (event) => {
						if (!this.enabled) {

						}
					},
					'onCancel': () => {
						if (!this.enabled) {

						}
					}
				});

				this.requests.push(request);
			}
		}
	};

	/**
	 *
	 * @author sigeom sa / http://sigeom.ch
	 * @author Ioda-Net Sàrl / https://www.ioda-net.ch/
	 * @author Markus Schütz / http://potree.org
	 *
	 */

	class GeoJSONExporter{

		static measurementToFeatures (measurement) {
			let coords = measurement.points.map(e => e.position.toArray());

			let features = [];

			if (coords.length === 1) {
				let feature = {
					type: 'Feature',
					geometry: {
						type: 'Point',
						coordinates: coords[0]
					},
					properties: {
						name: measurement.name
					}
				};
				features.push(feature);
			} else if (coords.length > 1 && !measurement.closed) {
				let object = {
					'type': 'Feature',
					'geometry': {
						'type': 'LineString',
						'coordinates': coords
					},
					'properties': {
						name: measurement.name
					}
				};

				features.push(object);
			} else if (coords.length > 1 && measurement.closed) {
				let object = {
					'type': 'Feature',
					'geometry': {
						'type': 'Polygon',
						'coordinates': [[...coords, coords[0]]]
					},
					'properties': {
						name: measurement.name
					}
				};
				features.push(object);
			}

			if (measurement.showDistances) {
				measurement.edgeLabels.forEach((label) => {
					let labelPoint = {
						type: 'Feature',
						geometry: {
							type: 'Point',
							coordinates: label.position.toArray()
						},
						properties: {
							distance: label.text
						}
					};
					features.push(labelPoint);
				});
			}

			if (measurement.showArea) {
				let point = measurement.areaLabel.position;
				let labelArea = {
					type: 'Feature',
					geometry: {
						type: 'Point',
						coordinates: point.toArray()
					},
					properties: {
						area: measurement.areaLabel.text
					}
				};
				features.push(labelArea);
			}

			return features;
		}

		static toString (measurements) {
			if (!(measurements instanceof Array)) {
				measurements = [measurements];
			}

			measurements = measurements.filter(m => m instanceof Measure);

			let features = [];
			for (let measure of measurements) {
				let f = GeoJSONExporter.measurementToFeatures(measure);

				features = features.concat(f);
			}

			let geojson = {
				'type': 'FeatureCollection',
				'features': features
			};

			return JSON.stringify(geojson, null, '\t');
		}

	}

	/**
	 *
	 * @author sigeom sa / http://sigeom.ch
	 * @author Ioda-Net Sàrl / https://www.ioda-net.ch/
	 * @author Markus Schuetz / http://potree.org
	 *
	 */

	class DXFExporter {

		static measurementPointSection (measurement) {
			let position = measurement.points[0].position;

			if (!position) {
				return '';
			}

			let dxfSection = `0
CIRCLE
8
layer_point
10
${position.x}
20
${position.y}
30
${position.z}
40
1.0
`;

			return dxfSection;
		}

		static measurementPolylineSection (measurement) {
			// bit code for polygons/polylines:
			// https://www.autodesk.com/techpubs/autocad/acad2000/dxf/polyline_dxf_06.htm
			let geomCode = 8;
			if (measurement.closed) {
				geomCode += 1;
			}

			let dxfSection = `0
POLYLINE
8
layer_polyline
62
1
66
1
10
0.0
20
0.0
30
0.0
70
${geomCode}
`;

			let xMax = 0.0;
			let yMax = 0.0;
			let zMax = 0.0;
			for (let point of measurement.points) {
				point = point.position;
				xMax = Math.max(xMax, point.x);
				yMax = Math.max(yMax, point.y);
				zMax = Math.max(zMax, point.z);

				dxfSection += `0
VERTEX
8
0
10
${point.x}
20
${point.y}
30
${point.z}
70
32
`;
			}
			dxfSection += `0
SEQEND
`;

			return dxfSection;
		}

		static measurementSection (measurement) {
			// if(measurement.points.length <= 1){
			//	return "";
			// }

			if (measurement.points.length === 0) {
				return '';
			} else if (measurement.points.length === 1) {
				return DXFExporter.measurementPointSection(measurement);
			} else if (measurement.points.length >= 2) {
				return DXFExporter.measurementPolylineSection(measurement);
			}
		}

		static toString(measurements){
			if (!(measurements instanceof Array)) {
				measurements = [measurements];
			}
			measurements = measurements.filter(m => m instanceof Measure);

			let points = measurements.filter(m => (m instanceof Measure))
				.map(m => m.points)
				.reduce((a, v) => a.concat(v))
				.map(p => p.position);

			let min = new Vector3(Infinity, Infinity, Infinity);
			let max = new Vector3(-Infinity, -Infinity, -Infinity);
			for (let point of points) {
				min.min(point);
				max.max(point);
			}

			let dxfHeader = `999
DXF created from potree
0
SECTION
2
HEADER
9
$ACADVER
1
AC1006
9
$INSBASE
10
0.0
20
0.0
30
0.0
9
$EXTMIN
10
${min.x}
20
${min.y}
30
${min.z}
9
$EXTMAX
10
${max.x}
20
${max.y}
30
${max.z}
0
ENDSEC
`;

			let dxfBody = `0
SECTION
2
ENTITIES
`;

			for (let measurement of measurements) {
				dxfBody += DXFExporter.measurementSection(measurement);
			}

			dxfBody += `0
ENDSEC
`;

			let dxf = dxfHeader + dxfBody + '0\nEOF';

			return dxf;
		}

	}

	class MeasurePanel{

		constructor(viewer, measurement, propertiesPanel){
			this.viewer = viewer;
			this.measurement = measurement;
			this.propertiesPanel = propertiesPanel;

			this._update = () => { this.update(); };
		}

		createCoordinatesTable(points){
			let table = $(`
			<table class="measurement_value_table">
				<tr>
					<th>x</th>
					<th>y</th>
					<th>z</th>
					<th></th>
				</tr>
			</table>
		`);

			let copyIconPath = Potree.resourcePath + '/icons/copy.svg';

			for (let point of points) {
				let x = Utils.addCommas(point.x.toFixed(3));
				let y = Utils.addCommas(point.y.toFixed(3));
				let z = Utils.addCommas(point.z.toFixed(3));

				let row = $(`
				<tr>
					<td><span>${x}</span></td>
					<td><span>${y}</span></td>
					<td><span>${z}</span></td>
					<td align="right" style="width: 25%">
						<img name="copy" title="copy" class="button-icon" src="${copyIconPath}" style="width: 16px; height: 16px"/>
					</td>
				</tr>
			`);

				this.elCopy = row.find("img[name=copy]");
				this.elCopy.click( () => {
					let msg = point.toArray().map(c => c.toFixed(3)).join(", ");
					Utils.clipboardCopy(msg);

					this.viewer.postMessage(
						`Copied value to clipboard: <br>'${msg}'`,
						{duration: 3000});
				});

				table.append(row);
			}

			return table;
		};

		createAttributesTable(){
			let elTable = $('<table class="measurement_value_table"></table>');

			let point = this.measurement.points[0];
			
			for(let attributeName of Object.keys(point)){
				if(attributeName === "position"){
				
				}else if(attributeName === "rgba"){
					let color = point.rgba;
					let text = color.join(', ');

					elTable.append($(`
					<tr>
						<td>rgb</td>
						<td>${text}</td>
					</tr>
				`));
				}else {
					let value = point[attributeName];
					let text = value.join(', ');

					elTable.append($(`
					<tr>
						<td>${attributeName}</td>
						<td>${text}</td>
					</tr>
				`));
				}
			}

			return elTable;
		}

		update(){

		}
	};

	class DistancePanel extends MeasurePanel{
		constructor(viewer, measurement, propertiesPanel){
			super(viewer, measurement, propertiesPanel);

			let removeIconPath = Potree.resourcePath + '/icons/remove.svg';
			this.elContent = $(`
			<div class="measurement_content selectable">
				<span class="coordinates_table_container"></span>
				<br>
				<table id="distances_table" class="measurement_value_table"></table>

				<!-- ACTIONS -->
				<div style="display: flex; margin-top: 12px">
					<span>
						<input type="button" name="make_profile" value="profile from measure" />
					</span>
					<span style="flex-grow: 1"></span>
					<img name="remove" class="button-icon" src="${removeIconPath}" style="width: 16px; height: 16px"/>
				</div>
			</div>
		`);

			this.elRemove = this.elContent.find("img[name=remove]");
			this.elRemove.click( () => {
				this.viewer.scene.removeMeasurement(measurement);
			});
			
			this.elMakeProfile = this.elContent.find("input[name=make_profile]");
			this.elMakeProfile.click( () => {
				//measurement.points;
				const profile = new Profile();

				profile.name = measurement.name;
				profile.width = measurement.getTotalDistance() / 50;

				for(const point of measurement.points){
					profile.addMarker(point.position.clone());
				}

				this.viewer.scene.addProfile(profile);

			});

			this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update);

			this.update();
		}

		update(){
			let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
			elCoordiantesContainer.empty();
			elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position)));

			let positions = this.measurement.points.map(p => p.position);
			let distances = [];
			for (let i = 0; i < positions.length - 1; i++) {
				let d = positions[i].distanceTo(positions[i + 1]);
				distances.push(d.toFixed(3));
			}

			let totalDistance = this.measurement.getTotalDistance().toFixed(3);
			let elDistanceTable = this.elContent.find(`#distances_table`);
			elDistanceTable.empty();

			for (let i = 0; i < distances.length; i++) {
				let label = (i === 0) ? 'Distances: ' : '';
				let distance = distances[i];
				let elDistance = $(`
				<tr>
					<th>${label}</th>
					<td style="width: 100%; padding-left: 10px">${distance}</td>
				</tr>`);
				elDistanceTable.append(elDistance);
			}

			let elTotal = $(`
			<tr>
				<th>Total: </td><td style="width: 100%; padding-left: 10px">${totalDistance}</th>
			</tr>`);
			elDistanceTable.append(elTotal);
		}
	};

	class PointPanel extends MeasurePanel{
		constructor(viewer, measurement, propertiesPanel){
			super(viewer, measurement, propertiesPanel);

			let removeIconPath = Potree.resourcePath + '/icons/remove.svg';
			this.elContent = $(`
			<div class="measurement_content selectable">
				<span class="coordinates_table_container"></span>
				<br>
				<span class="attributes_table_container"></span>

				<!-- ACTIONS -->
				<div style="display: flex; margin-top: 12px">
					<span></span>
					<span style="flex-grow: 1"></span>
					<img name="remove" class="button-icon" src="${removeIconPath}" style="width: 16px; height: 16px"/>
				</div>
			</div>
		`);

			this.elRemove = this.elContent.find("img[name=remove]");
			this.elRemove.click( () => {
				this.viewer.scene.removeMeasurement(measurement);
			});

			this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update);

			this.update();
		}

		update(){
			let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
			elCoordiantesContainer.empty();
			elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position)));

			let elAttributesContainer = this.elContent.find('.attributes_table_container');
			elAttributesContainer.empty();
			elAttributesContainer.append(this.createAttributesTable());
		}
	};

	class AreaPanel extends MeasurePanel{
		constructor(viewer, measurement, propertiesPanel){
			super(viewer, measurement, propertiesPanel);

			let removeIconPath = Potree.resourcePath + '/icons/remove.svg';
			this.elContent = $(`
			<div class="measurement_content selectable">
				<span class="coordinates_table_container"></span>
				<br>
				<span style="font-weight: bold">Area: </span>
				<span id="measurement_area"></span>

				<!-- ACTIONS -->
				<div style="display: flex; margin-top: 12px">
					<span></span>
					<span style="flex-grow: 1"></span>
					<img name="remove" class="button-icon" src="${removeIconPath}" style="width: 16px; height: 16px"/>
				</div>
			</div>
		`);

			this.elRemove = this.elContent.find("img[name=remove]");
			this.elRemove.click( () => {
				this.viewer.scene.removeMeasurement(measurement);
			});

			this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update);

			this.update();
		}

		update(){
			let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
			elCoordiantesContainer.empty();
			elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position)));

			let elArea = this.elContent.find(`#measurement_area`);
			elArea.html(this.measurement.getArea().toFixed(3));
		}
	};

	class AnglePanel extends MeasurePanel{
		constructor(viewer, measurement, propertiesPanel){
			super(viewer, measurement, propertiesPanel);

			let removeIconPath = Potree.resourcePath + '/icons/remove.svg';
			this.elContent = $(`
			<div class="measurement_content selectable">
				<span class="coordinates_table_container"></span>
				<br>
				<table class="measurement_value_table">
					<tr>
						<th>\u03b1</th>
						<th>\u03b2</th>
						<th>\u03b3</th>
					</tr>
					<tr>
						<td align="center" id="angle_cell_alpha" style="width: 33%"></td>
						<td align="center" id="angle_cell_betta" style="width: 33%"></td>
						<td align="center" id="angle_cell_gamma" style="width: 33%"></td>
					</tr>
				</table>

				<!-- ACTIONS -->
				<div style="display: flex; margin-top: 12px">
					<span></span>
					<span style="flex-grow: 1"></span>
					<img name="remove" class="button-icon" src="${removeIconPath}" style="width: 16px; height: 16px"/>
				</div>
			</div>
		`);

			this.elRemove = this.elContent.find("img[name=remove]");
			this.elRemove.click( () => {
				this.viewer.scene.removeMeasurement(measurement);
			});

			this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update);

			this.update();
		}

		update(){
			let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
			elCoordiantesContainer.empty();
			elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position)));

			let angles = [];
			for(let i = 0; i < this.measurement.points.length; i++){
				angles.push(this.measurement.getAngle(i) * (180.0 / Math.PI));
			}
			angles = angles.map(a => a.toFixed(1) + '\u00B0');

			let elAlpha = this.elContent.find(`#angle_cell_alpha`);
			let elBetta = this.elContent.find(`#angle_cell_betta`);
			let elGamma = this.elContent.find(`#angle_cell_gamma`);

			elAlpha.html(angles[0]);
			elBetta.html(angles[1]);
			elGamma.html(angles[2]);
		}
	};

	class CirclePanel extends MeasurePanel{
		constructor(viewer, measurement, propertiesPanel){
			super(viewer, measurement, propertiesPanel);

			let removeIconPath = Potree.resourcePath + '/icons/remove.svg';
			this.elContent = $(`
			<div class="measurement_content selectable">
				<span class="coordinates_table_container"></span>
				<br>
				<table id="infos_table" class="measurement_value_table"></table>

				<!-- ACTIONS -->
				<div style="display: flex; margin-top: 12px">
					<span></span>
					<span style="flex-grow: 1"></span>
					<img name="remove" class="button-icon" src="${removeIconPath}" style="width: 16px; height: 16px"/>
				</div>
			</div>
		`);

			this.elRemove = this.elContent.find("img[name=remove]");
			this.elRemove.click( () => {
				this.viewer.scene.removeMeasurement(measurement);
			});

			this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update);

			this.update();
		}

		update(){
			let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
			elCoordiantesContainer.empty();
			elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position)));

			const elInfos = this.elContent.find(`#infos_table`);

			if(this.measurement.points.length !== 3){
				elInfos.empty();
				
				return;
			}

			const A = this.measurement.points[0].position;
			const B = this.measurement.points[1].position;
			const C = this.measurement.points[2].position;

			const center = Potree.Utils.computeCircleCenter(A, B, C);
			const radius = center.distanceTo(A);
			const circumference = 2 * Math.PI * radius;
			
			const format = (number) => {
				return Potree.Utils.addCommas(number.toFixed(3));
			};

			
			const txtCenter = `${format(center.x)} ${format(center.y)} ${format(center.z)}`;
			const txtRadius = format(radius);
			const txtCircumference = format(circumference);

			const thStyle = `style="text-align: left"`;
			const tdStyle = `style="width: 100%; padding: 5px;"`;
			
			elInfos.html(`
			<tr>
				<th ${thStyle}>Center: </th>
				<td ${tdStyle}></td>
			</tr>
			<tr>
				<td ${tdStyle} colspan="2">
					${txtCenter}
				</td>
			</tr>
			<tr>
				<th ${thStyle}>Radius: </th>
				<td ${tdStyle}>${txtRadius}</td>
			</tr>
			<tr>
				<th ${thStyle}>Circumference: </th>
				<td ${tdStyle}>${txtCircumference}</td>
			</tr>
		`);
		}
	};

	class HeightPanel extends MeasurePanel{
		constructor(viewer, measurement, propertiesPanel){
			super(viewer, measurement, propertiesPanel);

			let removeIconPath = Potree.resourcePath + '/icons/remove.svg';
			this.elContent = $(`
			<div class="measurement_content selectable">
				<span class="coordinates_table_container"></span>
				<br>
				<span id="height_label">Height: </span><br>

				<!-- ACTIONS -->
				<div style="display: flex; margin-top: 12px">
					<span></span>
					<span style="flex-grow: 1"></span>
					<img name="remove" class="button-icon" src="${removeIconPath}" style="width: 16px; height: 16px"/>
				</div>
			</div>
		`);

			this.elRemove = this.elContent.find("img[name=remove]");
			this.elRemove.click( () => {
				this.viewer.scene.removeMeasurement(measurement);
			});

			this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update);

			this.update();
		}

		update(){
			let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
			elCoordiantesContainer.empty();
			elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points.map(p => p.position)));

			{
				let points = this.measurement.points;

				let sorted = points.slice().sort((a, b) => a.position.z - b.position.z);
				let lowPoint = sorted[0].position.clone();
				let highPoint = sorted[sorted.length - 1].position.clone();
				let min = lowPoint.z;
				let max = highPoint.z;
				let height = max - min;
				height = height.toFixed(3);

				this.elHeightLabel = this.elContent.find(`#height_label`);
				this.elHeightLabel.html(`<b>Height:</b> ${height}`);
			}
		}
	};

	class VolumePanel extends MeasurePanel{
		constructor(viewer, measurement, propertiesPanel){
			super(viewer, measurement, propertiesPanel);

			let copyIconPath = Potree.resourcePath + '/icons/copy.svg';
			let removeIconPath = Potree.resourcePath + '/icons/remove.svg';

			let lblLengthText = new Map([
				[BoxVolume, "length"],
				[SphereVolume, "rx"],
			]).get(measurement.constructor);

			let lblWidthText = new Map([
				[BoxVolume, "width"],
				[SphereVolume, "ry"],
			]).get(measurement.constructor);

			let lblHeightText = new Map([
				[BoxVolume, "height"],
				[SphereVolume, "rz"],
			]).get(measurement.constructor);

			this.elContent = $(`
			<div class="measurement_content selectable">
				<span class="coordinates_table_container"></span>

				<table class="measurement_value_table">
					<tr>
						<th>\u03b1</th>
						<th>\u03b2</th>
						<th>\u03b3</th>
						<th></th>
					</tr>
					<tr>
						<td align="center" id="angle_cell_alpha" style="width: 33%"></td>
						<td align="center" id="angle_cell_betta" style="width: 33%"></td>
						<td align="center" id="angle_cell_gamma" style="width: 33%"></td>
						<td align="right" style="width: 25%">
							<img name="copyRotation" title="copy" class="button-icon" src="${copyIconPath}" style="width: 16px; height: 16px"/>
						</td>
					</tr>
				</table>

				<table class="measurement_value_table">
					<tr>
						<th>${lblLengthText}</th>
						<th>${lblWidthText}</th>
						<th>${lblHeightText}</th>
						<th></th>
					</tr>
					<tr>
						<td align="center" id="cell_length" style="width: 33%"></td>
						<td align="center" id="cell_width" style="width: 33%"></td>
						<td align="center" id="cell_height" style="width: 33%"></td>
						<td align="right" style="width: 25%">
							<img name="copyScale" title="copy" class="button-icon" src="${copyIconPath}" style="width: 16px; height: 16px"/>
						</td>
					</tr>
				</table>

				<br>
				<span style="font-weight: bold">Volume: </span>
				<span id="measurement_volume"></span>

				<!--
				<li>
					<label style="whitespace: nowrap">
						<input id="volume_show" type="checkbox"/>
						<span>show volume</span>
					</label>
				</li>-->

				<li>
					<label style="whitespace: nowrap">
						<input id="volume_clip" type="checkbox"/>
						<span>make clip volume</span>
					</label>
				</li>

				<li style="margin-top: 10px">
					<input name="download_volume" type="button" value="prepare download" style="width: 100%" />
					<div name="download_message"></div>
				</li>


				<!-- ACTIONS -->
				<li style="display: grid; grid-template-columns: auto auto; grid-column-gap: 5px; margin-top: 10px">
					<input id="volume_reset_orientation" type="button" value="reset orientation"/>
					<input id="volume_make_uniform" type="button" value="make uniform"/>
				</li>
				<div style="display: flex; margin-top: 12px">
					<span></span>
					<span style="flex-grow: 1"></span>
					<img name="remove" class="button-icon" src="${removeIconPath}" style="width: 16px; height: 16px"/>
				</div>
			</div>
		`);

			{ // download
				this.elDownloadButton = this.elContent.find("input[name=download_volume]");

				if(this.propertiesPanel.viewer.server){
					this.elDownloadButton.click(() => this.download());
				} else {
					this.elDownloadButton.hide();
				}
			}

			this.elCopyRotation = this.elContent.find("img[name=copyRotation]");
			this.elCopyRotation.click( () => {
				let rotation = this.measurement.rotation.toArray().slice(0, 3);
				let msg = rotation.map(c => c.toFixed(3)).join(", ");
				Utils.clipboardCopy(msg);

				this.viewer.postMessage(
						`Copied value to clipboard: <br>'${msg}'`,
						{duration: 3000});
			});

			this.elCopyScale = this.elContent.find("img[name=copyScale]");
			this.elCopyScale.click( () => {
				let scale = this.measurement.scale.toArray();
				let msg = scale.map(c => c.toFixed(3)).join(", ");
				Utils.clipboardCopy(msg);

				this.viewer.postMessage(
						`Copied value to clipboard: <br>'${msg}'`,
						{duration: 3000});
			});

			this.elRemove = this.elContent.find("img[name=remove]");
			this.elRemove.click( () => {
				this.viewer.scene.removeVolume(measurement);
			});

			this.elContent.find("#volume_reset_orientation").click(() => {
				measurement.rotation.set(0, 0, 0);
			});

			this.elContent.find("#volume_make_uniform").click(() => {
				let mean = (measurement.scale.x + measurement.scale.y + measurement.scale.z) / 3;
				measurement.scale.set(mean, mean, mean);
			});

			this.elCheckClip = this.elContent.find('#volume_clip');
			this.elCheckClip.click(event => {
				this.measurement.clip = event.target.checked;
			});

			this.elCheckShow = this.elContent.find('#volume_show');
			this.elCheckShow.click(event => {
				this.measurement.visible = event.target.checked;
			});

			this.propertiesPanel.addVolatileListener(measurement, "position_changed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "orientation_changed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "scale_changed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "clip_changed", this._update);

			this.update();
		}

		async download(){

			let clipBox = this.measurement;

			let regions = [];
			//for(let clipBox of boxes){
			{
				let toClip = clipBox.matrixWorld;

				let px = new Vector3(+0.5, 0, 0).applyMatrix4(toClip);
				let nx = new Vector3(-0.5, 0, 0).applyMatrix4(toClip);
				let py = new Vector3(0, +0.5, 0).applyMatrix4(toClip);
				let ny = new Vector3(0, -0.5, 0).applyMatrix4(toClip);
				let pz = new Vector3(0, 0, +0.5).applyMatrix4(toClip);
				let nz = new Vector3(0, 0, -0.5).applyMatrix4(toClip);

				let pxN = new Vector3().subVectors(nx, px).normalize();
				let nxN = pxN.clone().multiplyScalar(-1);
				let pyN = new Vector3().subVectors(ny, py).normalize();
				let nyN = pyN.clone().multiplyScalar(-1);
				let pzN = new Vector3().subVectors(nz, pz).normalize();
				let nzN = pzN.clone().multiplyScalar(-1);

				let planes = [
					new Plane().setFromNormalAndCoplanarPoint(pxN, px),
					new Plane().setFromNormalAndCoplanarPoint(nxN, nx),
					new Plane().setFromNormalAndCoplanarPoint(pyN, py),
					new Plane().setFromNormalAndCoplanarPoint(nyN, ny),
					new Plane().setFromNormalAndCoplanarPoint(pzN, pz),
					new Plane().setFromNormalAndCoplanarPoint(nzN, nz),
				];

				let planeQueryParts = [];
				for(let plane of planes){
					let part = [plane.normal.toArray(), plane.constant].join(",");
					part = `[${part}]`;
					planeQueryParts.push(part);
				}
				let region = "[" + planeQueryParts.join(",") + "]";
				regions.push(region);
			}

			let regionsArg = regions.join(",");

			let pointcloudArgs = [];
			for(let pointcloud of this.viewer.scene.pointclouds){
				if(!pointcloud.visible){
					continue;
				}

				let offset = pointcloud.pcoGeometry.offset.clone();
				let negateOffset = new Matrix4().makeTranslation(...offset.multiplyScalar(-1).toArray());
				let matrixWorld = pointcloud.matrixWorld;

				let transform = new Matrix4().multiplyMatrices(matrixWorld, negateOffset);

				let path = `${window.location.pathname}/../${pointcloud.pcoGeometry.url}`;

				let arg = {
					path: path,
					transform: transform.elements,
				};
				let argString = JSON.stringify(arg);

				pointcloudArgs.push(argString);
			}
			let pointcloudsArg = pointcloudArgs.join(",");

			let elMessage = this.elContent.find("div[name=download_message]");

			let error = (message) => {
				elMessage.html(`<div style="color: #ff0000">ERROR: ${message}</div>`);
			};

			let info = (message) => {
				elMessage.html(`${message}`);
			};

			let handle = null;
			{ // START FILTER
				let url = `${viewer.server}/create_regions_filter?pointclouds=[${pointcloudsArg}]&regions=[${regionsArg}]`;
				
				//console.log(url);

				info("estimating results ...");

				let response = await fetch(url);
				let jsResponse = await response.json();
				//console.log(jsResponse);

				if(!jsResponse.handle){
					error(jsResponse.message);
					return;
				}else {
					handle = jsResponse.handle;
				}
			}

			{ // WAIT, CHECK PROGRESS, HANDLE FINISH
				let url = `${viewer.server}/check_regions_filter?handle=${handle}`;

				let sleep = (function(duration){
					return new Promise( (res, rej) => {
						setTimeout(() => {
							res();
						}, duration);
					});
				});

				let handleFiltering = (jsResponse) => {
					let {progress, estimate} = jsResponse;

					let progressFract = progress["processed points"] / estimate.points;
					let progressPercents = parseInt(progressFract * 100);

					info(`progress: ${progressPercents}%`);
				};

				let handleFinish = (jsResponse) => {
					let message = "downloads ready: <br>";
					message += "<ul>";

					for(let i = 0; i < jsResponse.pointclouds.length; i++){
						let url = `${viewer.server}/download_regions_filter_result?handle=${handle}&index=${i}`;

						message += `<li><a href="${url}">result_${i}.las</a> </li>\n`;
					}

					let reportURL = `${viewer.server}/download_regions_filter_report?handle=${handle}`;
					message += `<li> <a href="${reportURL}">report.json</a> </li>\n`;
					message += "</ul>";

					info(message);
				};

				let handleUnexpected = (jsResponse) => {
					let message = `Unexpected Response. <br>status: ${jsResponse.status} <br>message: ${jsResponse.message}`;
					info(message);
				};

				let handleError = (jsResponse) => {
					let message = `ERROR: ${jsResponse.message}`;
					error(message);

					throw new Error(message);
				};

				let start = Date.now();

				while(true){
					let response = await fetch(url);
					let jsResponse = await response.json();

					if(jsResponse.status === "ERROR"){
						handleError(jsResponse);
					}else if(jsResponse.status === "FILTERING"){
						handleFiltering(jsResponse);
					}else if(jsResponse.status === "FINISHED"){
						handleFinish(jsResponse);

						break;
					}else {
						handleUnexpected(jsResponse);
					}

					let durationS = (Date.now() - start) / 1000;
					let sleepAmountMS = durationS < 10 ? 100 : 1000;

					await sleep(sleepAmountMS);
				}
			}

		}

		update(){
			let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
			elCoordiantesContainer.empty();
			elCoordiantesContainer.append(this.createCoordinatesTable([this.measurement.position]));

			{
				let angles = this.measurement.rotation.toVector3();
				angles = angles.toArray();
				//angles = [angles.z, angles.x, angles.y];
				angles = angles.map(v => 180 * v / Math.PI);
				angles = angles.map(a => a.toFixed(1) + '\u00B0');

				let elAlpha = this.elContent.find(`#angle_cell_alpha`);
				let elBetta = this.elContent.find(`#angle_cell_betta`);
				let elGamma = this.elContent.find(`#angle_cell_gamma`);

				elAlpha.html(angles[0]);
				elBetta.html(angles[1]);
				elGamma.html(angles[2]);
			}

			{
				let dimensions = this.measurement.scale.toArray();
				dimensions = dimensions.map(v => Utils.addCommas(v.toFixed(2)));

				let elLength = this.elContent.find(`#cell_length`);
				let elWidth = this.elContent.find(`#cell_width`);
				let elHeight = this.elContent.find(`#cell_height`);

				elLength.html(dimensions[0]);
				elWidth.html(dimensions[1]);
				elHeight.html(dimensions[2]);
			}

			{
				let elVolume = this.elContent.find(`#measurement_volume`);
				let volume = this.measurement.getVolume();
				elVolume.html(Utils.addCommas(volume.toFixed(2)));
			}

			this.elCheckClip.prop("checked", this.measurement.clip);
			this.elCheckShow.prop("checked", this.measurement.visible);

		}
	};

	class ProfilePanel extends MeasurePanel{
		constructor(viewer, measurement, propertiesPanel){
			super(viewer, measurement, propertiesPanel);

			let removeIconPath = Potree.resourcePath + '/icons/remove.svg';
			this.elContent = $(`
			<div class="measurement_content selectable">
				<span class="coordinates_table_container"></span>
				<br>
				<span style="display:flex">
					<span style="display:flex; align-items: center; padding-right: 10px">Width: </span>
					<input id="sldProfileWidth" name="sldProfileWidth" value="5.06" style="flex-grow: 1; width:100%">
				</span>
				<br>

				<li style="margin-top: 10px">
					<input name="download_profile" type="button" value="prepare download" style="width: 100%" />
					<div name="download_message"></div>
				</li>

				<br>

				<input type="button" id="show_2d_profile" value="show 2d profile" style="width: 100%"/>

				<!-- ACTIONS -->
				<div style="display: flex; margin-top: 12px">
					<span></span>
					<span style="flex-grow: 1"></span>
					<img name="remove" class="button-icon" src="${removeIconPath}" style="width: 16px; height: 16px"/>
				</div>
			</div>
		`);

			this.elRemove = this.elContent.find("img[name=remove]");
			this.elRemove.click( () => {
				this.viewer.scene.removeProfile(measurement);
			});

			{ // download
				this.elDownloadButton = this.elContent.find(`input[name=download_profile]`);

				if(this.propertiesPanel.viewer.server){
					this.elDownloadButton.click(() => this.download());
				} else {
					this.elDownloadButton.hide();
				}
			}

			{ // width spinner
				let elWidthSlider = this.elContent.find(`#sldProfileWidth`);

				elWidthSlider.spinner({
					min: 0, max: 10 * 1000 * 1000, step: 0.01,
					numberFormat: 'n',
					start: () => {},
					spin: (event, ui) => {
						let value = elWidthSlider.spinner('value');
						measurement.setWidth(value);
					},
					change: (event, ui) => {
						let value = elWidthSlider.spinner('value');
						measurement.setWidth(value);
					},
					stop: (event, ui) => {
						let value = elWidthSlider.spinner('value');
						measurement.setWidth(value);
					},
					incremental: (count) => {
						let value = elWidthSlider.spinner('value');
						let step = elWidthSlider.spinner('option', 'step');

						let delta = value * 0.05;
						let increments = Math.max(1, parseInt(delta / step));

						return increments;
					}
				});
				elWidthSlider.spinner('value', measurement.getWidth());
				elWidthSlider.spinner('widget').css('width', '100%');

				let widthListener = (event) => {
					let value = elWidthSlider.spinner('value');
					if (value !== measurement.getWidth()) {
						elWidthSlider.spinner('value', measurement.getWidth());
					}
				};
				this.propertiesPanel.addVolatileListener(measurement, "width_changed", widthListener);
			}

			let elShow2DProfile = this.elContent.find(`#show_2d_profile`);
			elShow2DProfile.click(() => {
				this.propertiesPanel.viewer.profileWindow.show();
				this.propertiesPanel.viewer.profileWindowController.setProfile(measurement);
			});

			this.propertiesPanel.addVolatileListener(measurement, "marker_added", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_removed", this._update);
			this.propertiesPanel.addVolatileListener(measurement, "marker_moved", this._update);

			this.update();
		}

		update(){
			let elCoordiantesContainer = this.elContent.find('.coordinates_table_container');
			elCoordiantesContainer.empty();
			elCoordiantesContainer.append(this.createCoordinatesTable(this.measurement.points));
		}

		async download(){

			let profile = this.measurement;

			let regions = [];
			{
				let segments = profile.getSegments();
				let width = profile.width;
				
				for(let segment of segments){
					let start = segment.start.clone().multiply(new Vector3(1, 1, 0));
					let end = segment.end.clone().multiply(new Vector3(1, 1, 0));
					let center = new Vector3().addVectors(start, end).multiplyScalar(0.5);
					
					let startEndDir = new Vector3().subVectors(end, start).normalize();
					let endStartDir = new Vector3().subVectors(start, end).normalize();
					let upDir = new Vector3(0, 0, 1);
					let rightDir = new Vector3().crossVectors(startEndDir, upDir);
					let leftDir = new Vector3().crossVectors(endStartDir, upDir);
					
					console.log(leftDir);
					
					let right = rightDir.clone().multiplyScalar(width * 0.5).add(center);
					let left = leftDir.clone().multiplyScalar(width * 0.5).add(center);
					
					let planes = [
						new Plane().setFromNormalAndCoplanarPoint(startEndDir, start),
						new Plane().setFromNormalAndCoplanarPoint(endStartDir, end),
						new Plane().setFromNormalAndCoplanarPoint(leftDir, right),
						new Plane().setFromNormalAndCoplanarPoint(rightDir, left),
					];
					
					let planeQueryParts = [];
					for(let plane of planes){
						let part = [plane.normal.toArray(), plane.constant].join(",");
						part = `[${part}]`;
						planeQueryParts.push(part);
					}
					let region = "[" + planeQueryParts.join(",") + "]";
					regions.push(region);
				}
			}

			let regionsArg = regions.join(",");

			let pointcloudArgs = [];
			for(let pointcloud of this.viewer.scene.pointclouds){
				if(!pointcloud.visible){
					continue;
				}

				let offset = pointcloud.pcoGeometry.offset.clone();
				let negateOffset = new Matrix4().makeTranslation(...offset.multiplyScalar(-1).toArray());
				let matrixWorld = pointcloud.matrixWorld;

				let transform = new Matrix4().multiplyMatrices(matrixWorld, negateOffset);

				let path = `${window.location.pathname}/../${pointcloud.pcoGeometry.url}`;

				let arg = {
					path: path,
					transform: transform.elements,
				};
				let argString = JSON.stringify(arg);

				pointcloudArgs.push(argString);
			}
			let pointcloudsArg = pointcloudArgs.join(",");

			let elMessage = this.elContent.find("div[name=download_message]");

			let error = (message) => {
				elMessage.html(`<div style="color: #ff0000">ERROR: ${message}</div>`);
			};

			let info = (message) => {
				elMessage.html(`${message}`);
			};

			let handle = null;
			{ // START FILTER
				let url = `${viewer.server}/create_regions_filter?pointclouds=[${pointcloudsArg}]&regions=[${regionsArg}]`;
				
				//console.log(url);

				info("estimating results ...");

				let response = await fetch(url);
				let jsResponse = await response.json();
				//console.log(jsResponse);

				if(!jsResponse.handle){
					error(jsResponse.message);
					return;
				}else {
					handle = jsResponse.handle;
				}
			}

			{ // WAIT, CHECK PROGRESS, HANDLE FINISH
				let url = `${viewer.server}/check_regions_filter?handle=${handle}`;

				let sleep = (function(duration){
					return new Promise( (res, rej) => {
						setTimeout(() => {
							res();
						}, duration);
					});
				});

				let handleFiltering = (jsResponse) => {
					let {progress, estimate} = jsResponse;

					let progressFract = progress["processed points"] / estimate.points;
					let progressPercents = parseInt(progressFract * 100);

					info(`progress: ${progressPercents}%`);
				};

				let handleFinish = (jsResponse) => {
					let message = "downloads ready: <br>";
					message += "<ul>";

					for(let i = 0; i < jsResponse.pointclouds.length; i++){
						let url = `${viewer.server}/download_regions_filter_result?handle=${handle}&index=${i}`;

						message += `<li><a href="${url}">result_${i}.las</a> </li>\n`;
					}

					let reportURL = `${viewer.server}/download_regions_filter_report?handle=${handle}`;
					message += `<li> <a href="${reportURL}">report.json</a> </li>\n`;
					message += "</ul>";

					info(message);
				};

				let handleUnexpected = (jsResponse) => {
					let message = `Unexpected Response. <br>status: ${jsResponse.status} <br>message: ${jsResponse.message}`;
					info(message);
				};

				let handleError = (jsResponse) => {
					let message = `ERROR: ${jsResponse.message}`;
					error(message);

					throw new Error(message);
				};

				let start = Date.now();

				while(true){
					let response = await fetch(url);
					let jsResponse = await response.json();

					if(jsResponse.status === "ERROR"){
						handleError(jsResponse);
					}else if(jsResponse.status === "FILTERING"){
						handleFiltering(jsResponse);
					}else if(jsResponse.status === "FINISHED"){
						handleFinish(jsResponse);

						break;
					}else {
						handleUnexpected(jsResponse);
					}

					let durationS = (Date.now() - start) / 1000;
					let sleepAmountMS = durationS < 10 ? 100 : 1000;

					await sleep(sleepAmountMS);
				}
			}

		}
	};

	class CameraPanel{
		constructor(viewer, propertiesPanel){
			this.viewer = viewer;
			this.propertiesPanel = propertiesPanel;

			this._update = () => { this.update(); };

			let copyIconPath = Potree.resourcePath + '/icons/copy.svg';
			this.elContent = $(`
		<div class="propertypanel_content">
			<table>
				<tr>
					<th colspan="3">position</th>
					<th></th>
				</tr>
				<tr>
					<td align="center" id="camera_position_x" style="width: 25%"></td>
					<td align="center" id="camera_position_y" style="width: 25%"></td>
					<td align="center" id="camera_position_z" style="width: 25%"></td>
					<td align="right" id="copy_camera_position" style="width: 25%">
						<img name="copyPosition" title="copy" class="button-icon" src="${copyIconPath}" style="width: 16px; height: 16px"/>
					</td>
				</tr>
				<tr>
					<th colspan="3">target</th>
					<th></th>
				</tr>
				<tr>
					<td align="center" id="camera_target_x" style="width: 25%"></td>
					<td align="center" id="camera_target_y" style="width: 25%"></td>
					<td align="center" id="camera_target_z" style="width: 25%"></td>
					<td align="right" id="copy_camera_target" style="width: 25%">
						<img name="copyTarget" title="copy" class="button-icon" src="${copyIconPath}" style="width: 16px; height: 16px"/>
					</td>
				</tr>
			</table>
		</div>
		`);

			this.elCopyPosition = this.elContent.find("img[name=copyPosition]");
			this.elCopyPosition.click( () => {
				let pos = this.viewer.scene.getActiveCamera().position.toArray();
				let msg = pos.map(c => c.toFixed(3)).join(", ");
				Utils.clipboardCopy(msg);

				this.viewer.postMessage(
						`Copied value to clipboard: <br>'${msg}'`,
						{duration: 3000});
			});

			this.elCopyTarget = this.elContent.find("img[name=copyTarget]");
			this.elCopyTarget.click( () => {
				let pos = this.viewer.scene.view.getPivot().toArray();
				let msg = pos.map(c => c.toFixed(3)).join(", ");
				Utils.clipboardCopy(msg);

				this.viewer.postMessage(
						`Copied value to clipboard: <br>'${msg}'`,
						{duration: 3000});
			});

			this.propertiesPanel.addVolatileListener(viewer, "camera_changed", this._update);

			this.update();
		}

		update(){
			//console.log("updating camera panel");

			let camera = this.viewer.scene.getActiveCamera();
			let view = this.viewer.scene.view;

			let pos = camera.position.toArray().map(c => Utils.addCommas(c.toFixed(3)));
			this.elContent.find("#camera_position_x").html(pos[0]);
			this.elContent.find("#camera_position_y").html(pos[1]);
			this.elContent.find("#camera_position_z").html(pos[2]);

			let target = view.getPivot().toArray().map(c => Utils.addCommas(c.toFixed(3)));
			this.elContent.find("#camera_target_x").html(target[0]);
			this.elContent.find("#camera_target_y").html(target[1]);
			this.elContent.find("#camera_target_z").html(target[2]);
		}
	};

	class AnnotationPanel{
		constructor(viewer, propertiesPanel, annotation){
			this.viewer = viewer;
			this.propertiesPanel = propertiesPanel;
			this.annotation = annotation;

			this._update = () => { this.update(); };

			let copyIconPath = `${Potree.resourcePath}/icons/copy.svg`;
			this.elContent = $(`
		<div class="propertypanel_content">
			<table>
				<tr>
					<th colspan="3">position</th>
					<th></th>
				</tr>
				<tr>
					<td align="center" id="annotation_position_x" style="width: 25%"></td>
					<td align="center" id="annotation_position_y" style="width: 25%"></td>
					<td align="center" id="annotation_position_z" style="width: 25%"></td>
					<td align="right" id="copy_annotation_position" style="width: 25%">
						<img name="copyPosition" title="copy" class="button-icon" src="${copyIconPath}" style="width: 16px; height: 16px"/>
					</td>
				</tr>

			</table>

			<div>

				<div class="heading">Title</div>
				<div id="annotation_title" contenteditable="true">
					Annotation Title
				</div>

				<div class="heading">Description</div>
				<div id="annotation_description" contenteditable="true">
					A longer description of this annotation. 
						Can be multiple lines long. TODO: the user should be able
						to modify title and description. 
				</div>

			</div>

		</div>
		`);

			this.elCopyPosition = this.elContent.find("img[name=copyPosition]");
			this.elCopyPosition.click( () => {
				let pos = this.annotation.position.toArray();
				let msg = pos.map(c => c.toFixed(3)).join(", ");
				Utils.clipboardCopy(msg);

				this.viewer.postMessage(
						`Copied value to clipboard: <br>'${msg}'`,
						{duration: 3000});
			});

			this.elTitle = this.elContent.find("#annotation_title").html(annotation.title);
			this.elDescription = this.elContent.find("#annotation_description").html(annotation.description);

			this.elTitle[0].addEventListener("input", () => {
				const title = this.elTitle.html();
				annotation.title = title;

			}, false);

			this.elDescription[0].addEventListener("input", () => {
				const description = this.elDescription.html();
				annotation.description = description;
			}, false);

			this.update();
		}

		update(){
			const {annotation, elContent, elTitle, elDescription} = this;

			let pos = annotation.position.toArray().map(c => Utils.addCommas(c.toFixed(3)));
			elContent.find("#annotation_position_x").html(pos[0]);
			elContent.find("#annotation_position_y").html(pos[1]);
			elContent.find("#annotation_position_z").html(pos[2]);

			elTitle.html(annotation.title);
			elDescription.html(annotation.description);


		}
	};

	class CameraAnimationPanel{
		constructor(viewer, propertiesPanel, animation){
			this.viewer = viewer;
			this.propertiesPanel = propertiesPanel;
			this.animation = animation;

			this.elContent = $(`
			<div class="propertypanel_content">
				<span id="animation_keyframes"></span>

				<span>

					<span style="display:flex">
						<span style="display:flex; align-items: center; padding-right: 10px">Duration: </span>
						<input name="spnDuration" value="5.0" style="flex-grow: 1; width:100%">
					</span>

					<span>Time: </span><span id="lblTime"></span> <div id="sldTime"></div>

					<input name="play" type="button" value="play"/>
				</span>
			</div>
		`);

			const elPlay = this.elContent.find("input[name=play]");
			elPlay.click( () => {
				animation.play();
			});

			const elSlider = this.elContent.find('#sldTime');
			elSlider.slider({
				value: 0,
				min: 0,
				max: 1,
				step: 0.001,
				slide: (event, ui) => { 
					animation.set(ui.value);
				}
			});

			let elDuration = this.elContent.find(`input[name=spnDuration]`);
			elDuration.spinner({
				min: 0, max: 300, step: 0.01,
				numberFormat: 'n',
				start: () => {},
				spin: (event, ui) => {
					let value = elDuration.spinner('value');
					animation.setDuration(value);
				},
				change: (event, ui) => {
					let value = elDuration.spinner('value');
					animation.setDuration(value);
				},
				stop: (event, ui) => {
					let value = elDuration.spinner('value');
					animation.setDuration(value);
				},
				incremental: (count) => {
					let value = elDuration.spinner('value');
					let step = elDuration.spinner('option', 'step');

					let delta = value * 0.05;
					let increments = Math.max(1, parseInt(delta / step));

					return increments;
				}
			});
			elDuration.spinner('value', animation.getDuration());
			elDuration.spinner('widget').css('width', '100%');

			const elKeyframes = this.elContent.find("#animation_keyframes");

			const updateKeyframes = () => {
				elKeyframes.empty();

				//let index = 0;

				// <span style="flex-grow: 0;">
				// 				<img name="add" src="${Potree.resourcePath}/icons/add.svg" style="width: 1.5em; height: 1.5em"/>
				// 			</span>

				const addNewKeyframeItem = (index) => {
					let elNewKeyframe = $(`
					<div style="display: flex; margin: 0.2em 0em">
						<span style="flex-grow: 1"></span>
						<input type="button" name="add" value="insert control point" />
						<span style="flex-grow: 1"></span>
					</div>
				`);

					const elAdd = elNewKeyframe.find("input[name=add]");
					elAdd.click( () => {
						animation.createControlPoint(index);
					});

					elKeyframes.append(elNewKeyframe);
				};

				const addKeyframeItem = (index) => {
					let elKeyframe = $(`
					<div style="display: flex; margin: 0.2em 0em">
						<span style="flex-grow: 0;">
							<img name="assign" src="${Potree.resourcePath}/icons/assign.svg" style="width: 1.5em; height: 1.5em"/>
						</span>
						<span style="flex-grow: 0;">
							<img name="move" src="${Potree.resourcePath}/icons/circled_dot.svg" style="width: 1.5em; height: 1.5em"/>
						</span>
						<span style="flex-grow: 0; width: 1.5em; height: 1.5em"></span>
						<span style="flex-grow: 0; font-size: 1.5em">keyframe</span>
						<span style="flex-grow: 1"></span>
						<span style="flex-grow: 0;">
							<img name="delete" src="${Potree.resourcePath}/icons/remove.svg" style="width: 1.5em; height: 1.5em"/>
						</span>
					</div>
				`);

					const elAssign = elKeyframe.find("img[name=assign]");
					const elMove = elKeyframe.find("img[name=move]");
					const elDelete = elKeyframe.find("img[name=delete]");

					elAssign.click( () => {
						const cp = animation.controlPoints[index];

						cp.position.copy(viewer.scene.view.position);
						cp.target.copy(viewer.scene.view.getPivot());
					});

					elMove.click( () => {
						const cp = animation.controlPoints[index];

						viewer.scene.view.position.copy(cp.position);
						viewer.scene.view.lookAt(cp.target);
					});

					elDelete.click( () => {
						const cp = animation.controlPoints[index];
						animation.removeControlPoint(cp);
					});

					elKeyframes.append(elKeyframe);
				};

				let index = 0;

				addNewKeyframeItem(index);

				for(const cp of animation.controlPoints){
					
					addKeyframeItem(index);
					index++;
					addNewKeyframeItem(index);

				}
			};

			updateKeyframes();

			animation.addEventListener("controlpoint_added", updateKeyframes);
			animation.addEventListener("controlpoint_removed", updateKeyframes);




			// this._update = () => { this.update(); };

			// this.update();
		}

		update(){
			
		}
	};

	class PropertiesPanel{

		constructor(container, viewer){
			this.container = container;
			this.viewer = viewer;
			this.object = null;
			this.cleanupTasks = [];
			this.scene = null;
		}

		setScene(scene){
			this.scene = scene;
		}

		set(object){
			if(this.object === object){
				return;
			}

			this.object = object;
			
			for(let task of this.cleanupTasks){
				task();
			}
			this.cleanupTasks = [];
			this.container.empty();

			if(object instanceof PointCloudTree){
				this.setPointCloud(object);
			}else if(object instanceof Measure || object instanceof Profile || object instanceof Volume){
				this.setMeasurement(object);
			}else if(object instanceof Camera){
				this.setCamera(object);
			}else if(object instanceof Annotation){
				this.setAnnotation(object);
			}else if(object instanceof CameraAnimation){
				this.setCameraAnimation(object);
			}
			
		}

		//
		// Used for events that should be removed when the property object changes.
		// This is for listening to materials, scene, point clouds, etc.
		// not required for DOM listeners, since they are automatically cleared by removing the DOM subtree.
		//
		addVolatileListener(target, type, callback){
			target.addEventListener(type, callback);
			this.cleanupTasks.push(() => {
				target.removeEventListener(type, callback);
			});
		}

		setPointCloud(pointcloud){

			let material = pointcloud.material;

			let panel = $(`
			<div class="scene_content selectable">
				<ul class="pv-menu-list">

				<li>
				<span data-i18n="appearance.point_size"></span>:&nbsp;<span id="lblPointSize"></span> <div id="sldPointSize"></div>
				</li>
				<li>
				<!--<span data-i18n="appearance.min_point_size"></span>:&nbsp;<span id="lblMinPointSize"></span> <div id="sldMinPointSize"></div>-->
				</li>

				<!-- SIZE TYPE -->
				<!--<li>
					<label for="optPointSizing" class="pv-select-label" data-i18n="appearance.point_size_type">Point Sizing </label>
					<select id="optPointSizing" name="optPointSizing">
						<option>FIXED</option>
						<option>ATTENUATED</option>
						<option>ADAPTIVE</option>
					</select>
				</li>-->

				<!-- SHAPE -->
				<!--<li>
					<label for="optShape" class="pv-select-label" data-i18n="appearance.point_shape"></label><br>
					<select id="optShape" name="optShape">
						<option>SQUARE</option>
						<option>CIRCLE</option>
						<option>PARABOLOID</option>
					</select>
				</li>-->

				<!--<li id="materials_backface_container">
				<label><input id="set_backface_culling" type="checkbox" /><span data-i18n="appearance.backface_culling"></span></label>
				</li>-->
				
				<!-- OPACITY -->
				<!--<li><span data-i18n="appearance.point_opacity"></span>:<span id="lblOpacity"></span><div id="sldOpacity"></div></li>-->

				<div class="divider">
					<span>Attribute</span>
				</div>

				<li>
					<select id="optMaterial" name="optMaterial"></select>
				</li>

				<div id="materials.composite_weight_container">
					<div class="divider">
						<span>Attribute Weights</span>
					</div>

					<li>RGB: <span id="lblWeightRGB"></span> <div id="sldWeightRGB"></div>	</li>
					<li>Intensity: <span id="lblWeightIntensity"></span> <div id="sldWeightIntensity"></div>	</li>
					<li>Elevation: <span id="lblWeightElevation"></span> <div id="sldWeightElevation"></div>	</li>
					<li>Classification: <span id="lblWeightClassification"></span> <div id="sldWeightClassification"></div>	</li>
					<!--<li>Return Number: <span id="lblWeightReturnNumber"></span> <div id="sldWeightReturnNumber"></div>	</li>
					<li>Source ID: <span id="lblWeightSourceID"></span> <div id="sldWeightSourceID"></div>	</li> -->
				</div>

				<div id="materials.rgb_container">
					<div class="divider">
						<span>RGB</span>
					</div>

					<li>Gamma: <span id="lblRGBGamma"></span> <div id="sldRGBGamma"></div>	</li>
					<li>Brightness: <span id="lblRGBBrightness"></span> <div id="sldRGBBrightness"></div>	</li>
					<li>Contrast: <span id="lblRGBContrast"></span> <div id="sldRGBContrast"></div>	</li>
				</div>

				<div id="materials.extra_container">
					<div class="divider">
						<span>Extra Attribute</span>
					</div>

					<li><span data-i18n="appearance.extra_range"></span>: <span id="lblExtraRange"></span> <div id="sldExtraRange"></div></li>

					<li>Gamma: <span id="lblExtraGamma"></span> <div id="sldExtraGamma"></div></li>
					<li>Brightness: <span id="lblExtraBrightness"></span> <div id="sldExtraBrightness"></div></li>
					<li>Contrast: <span id="lblExtraContrast"></span> <div id="sldExtraContrast"></div></li>
				</div>
				
				<div id="materials.matcap_container">
					<div class="divider">
						<span>MATCAP</span>
					</div>

					<li>
						<div id="matcap_scheme_selection" style="display: flex; flex-wrap: wrap;"> </div>
					</li>
				</div>

				<div id="materials.color_container">
					<div class="divider">
						<span>Color</span>
					</div>

					<input id="materials.color.picker" />
				</div>


				<div id="materials.elevation_container">
					<div class="divider">
						<span>Elevation</span>
					</div>

					<li><span data-i18n="appearance.elevation_range"></span>: <span id="lblHeightRange"></span> <div id="sldHeightRange"></div>	</li>

					<li>
						<selectgroup id="gradient_repeat_option">
							<option id="gradient_repeat_clamp" value="CLAMP">Clamp</option>
							<option id="gradient_repeat_repeat" value="REPEAT">Repeat</option>
							<option id="gradient_repeat_mirrored_repeat" value="MIRRORED_REPEAT">Mirrored Repeat</option>
						</selectgroup>
					</li>

					<li>
						<span>Gradient Scheme:</span>
						<div id="elevation_gradient_scheme_selection" style="display: flex; padding: 1em 0em">
						</div>
					</li>
				</div>

				<div id="materials.transition_container">
					<div class="divider">
						<span>Transition</span>
					</div>

					<li>transition: <span id="lblTransition"></span> <div id="sldTransition"></div>	</li>
				</div>

				<div id="materials.intensity_container">
					<div class="divider">
						<span>Intensity</span>
					</div>

					<li>Range: <span id="lblIntensityRange"></span> <div id="sldIntensityRange"></div>	</li>
					<li>Gamma: <span id="lblIntensityGamma"></span> <div id="sldIntensityGamma"></div>	</li>
					<li>Brightness: <span id="lblIntensityBrightness"></span> <div id="sldIntensityBrightness"></div>	</li>
					<li>Contrast: <span id="lblIntensityContrast"></span> <div id="sldIntensityContrast"></div>	</li>
				</div>

				<div id="materials.gpstime_container">
					<div class="divider">
						<span>GPS Time</span>
					</div>

				</div>
				
				<div id="materials.index_container">
					<div class="divider">
						<span>Indices</span>
					</div>
				</div>


				</ul>
			</div>
		`);

			panel.i18n();
			this.container.append(panel);

			{ // POINT SIZE
				let sldPointSize = panel.find(`#sldPointSize`);
				let lblPointSize = panel.find(`#lblPointSize`);

				sldPointSize.slider({
					value: material.size,
					min: 1,
					max: 10,
					step: 0.1,
					slide: function (event, ui) { material.size = ui.value; }
				});

				let update = (e) => {
					lblPointSize.html(material.size.toFixed(2));
					sldPointSize.slider({value: material.size});
				};
				this.addVolatileListener(material, "point_size_changed", update);
				
				update();
			}

			{ // MINIMUM POINT SIZE
				let sldMinPointSize = panel.find(`#sldMinPointSize`);
				let lblMinPointSize = panel.find(`#lblMinPointSize`);

				sldMinPointSize.slider({
					value: material.size,
					min: 1,
					max: 10,
					step: 0.1,
					slide: function (event, ui) { material.minSize = ui.value; }
				});

				let update = (e) => {
					lblMinPointSize.html(material.minSize.toFixed(2));
					sldMinPointSize.slider({value: material.minSize});
				};
				this.addVolatileListener(material, "point_size_changed", update);
				
				update();
			}

			{ // POINT SIZING
				let strSizeType = Object.keys(PointSizeType)[material.pointSizeType];

				let opt = panel.find(`#optPointSizing`);
				opt.selectmenu();
				opt.val(strSizeType).selectmenu('refresh');

				opt.selectmenu({
					change: (event, ui) => {
						material.pointSizeType = PointSizeType[ui.item.value];
					}
				});
			}

			{ // SHAPE
				let opt = panel.find(`#optShape`);

				opt.selectmenu({
					change: (event, ui) => {
						let value = ui.item.value;

						material.shape = PointShape[value];
					}
				});

				let update = () => {
					let typename = Object.keys(PointShape)[material.shape];

					opt.selectmenu().val(typename).selectmenu('refresh');
				};
				this.addVolatileListener(material, "point_shape_changed", update);

				update();
			}

			{ // BACKFACE CULLING
				
				let opt = panel.find(`#set_backface_culling`);
				opt.click(() => {
					material.backfaceCulling = opt.prop("checked");
				});
				let update = () => {
					let value = material.backfaceCulling;
					opt.prop("checked", value);
				};
				this.addVolatileListener(material, "backface_changed", update);
				update();

				let blockBackface = $('#materials_backface_container');
				blockBackface.css('display', 'none');

				const pointAttributes = pointcloud.pcoGeometry.pointAttributes;
				const hasNormals = pointAttributes.hasNormals ? pointAttributes.hasNormals() : false;
				if(hasNormals) {
					blockBackface.css('display', 'block');
				}
				/*
				opt.checkboxradio({
					clicked: (event, ui) => {
						// let value = ui.item.value;
						let value = ui.item.checked;
						console.log(value);
						material.backfaceCulling = value; // $('#set_freeze').prop("checked");
					}
				});
				*/
			}

			{ // OPACITY
				let sldOpacity = panel.find(`#sldOpacity`);
				let lblOpacity = panel.find(`#lblOpacity`);

				sldOpacity.slider({
					value: material.opacity,
					min: 0,
					max: 1,
					step: 0.001,
					slide: function (event, ui) { 
						material.opacity = ui.value;
					}
				});

				let update = (e) => {
					lblOpacity.html(material.opacity.toFixed(2));
					sldOpacity.slider({value: material.opacity});
				};
				this.addVolatileListener(material, "opacity_changed", update);

				update();
			}

			{

				const attributes = pointcloud.pcoGeometry.pointAttributes.attributes;

				let options = [];

				options.push(...attributes.map(a => a.name));

				const intensityIndex = options.indexOf("intensity");
				if(intensityIndex >= 0){
					options.splice(intensityIndex + 1, 0, "intensity gradient");
				}

				options.push(
					"elevation",
					"color",
					'matcap',
					'indices',
					'level of detail',
					'composite'
				);

				const blacklist = [
					"POSITION_CARTESIAN",
					"position",
				];

				options = options.filter(o => !blacklist.includes(o));

				let attributeSelection = panel.find('#optMaterial');
				for(let option of options){
					let elOption = $(`<option>${option}</option>`);
					attributeSelection.append(elOption);
				}

				let updateMaterialPanel = (event, ui) => {
					let selectedValue = attributeSelection.selectmenu().val();
					material.activeAttributeName = selectedValue;

					let attribute = pointcloud.getAttribute(selectedValue);

					if(selectedValue === "intensity gradient"){
						attribute = pointcloud.getAttribute("intensity");
					}

					const isIntensity = attribute ? ["intensity", "intensity gradient"].includes(attribute.name) : false;

					if(isIntensity){
						if(pointcloud.material.intensityRange[0] === Infinity){
							pointcloud.material.intensityRange = attribute.range;
						}

						const [min, max] = attribute.range;

						panel.find('#sldIntensityRange').slider({
							range: true,
							min: min, max: max, step: 0.01,
							values: [min, max],
							slide: (event, ui) => {
								let min = ui.values[0];
								let max = ui.values[1];
								material.intensityRange = [min, max];
							}
						});
					} else if(attribute){
						const [min, max] = attribute.range;

						let selectedRange = material.getRange(attribute.name);

						if(!selectedRange){
							selectedRange = [...attribute.range];
						}

						let minMaxAreNumbers = typeof min === "number" && typeof max === "number";

						if(minMaxAreNumbers){
							panel.find('#sldExtraRange').slider({
								range: true,
								min: min, 
								max: max, 
								step: 0.01,
								values: selectedRange,
								slide: (event, ui) => {
									let [a, b] = ui.values;

									material.setRange(attribute.name, [a, b]);
								}
							});
						}

					}

					let blockWeights = $('#materials\\.composite_weight_container');
					let blockElevation = $('#materials\\.elevation_container');
					let blockRGB = $('#materials\\.rgb_container');
					let blockExtra = $('#materials\\.extra_container');
					let blockColor = $('#materials\\.color_container');
					let blockIntensity = $('#materials\\.intensity_container');
					let blockIndex = $('#materials\\.index_container');
					let blockTransition = $('#materials\\.transition_container');
					let blockGps = $('#materials\\.gpstime_container');
					let blockMatcap = $('#materials\\.matcap_container');

					blockIndex.css('display', 'none');
					blockIntensity.css('display', 'none');
					blockElevation.css('display', 'none');
					blockRGB.css('display', 'none');
					blockExtra.css('display', 'none');
					blockColor.css('display', 'none');
					blockWeights.css('display', 'none');
					blockTransition.css('display', 'none');
					blockMatcap.css('display', 'none');
					blockGps.css('display', 'none');

					if (selectedValue === 'composite') {
						blockWeights.css('display', 'block');
						blockElevation.css('display', 'block');
						blockRGB.css('display', 'block');
						blockIntensity.css('display', 'block');
					} else if (selectedValue === 'elevation') {
						blockElevation.css('display', 'block');
					} else if (selectedValue === 'RGB and Elevation') {
						blockRGB.css('display', 'block');
						blockElevation.css('display', 'block');
					} else if (selectedValue === 'rgba') {
						blockRGB.css('display', 'block');
					} else if (selectedValue === 'color') {
						blockColor.css('display', 'block');
					} else if (selectedValue === 'intensity') {
						blockIntensity.css('display', 'block');
					} else if (selectedValue === 'intensity gradient') {
						blockIntensity.css('display', 'block');
					} else if (selectedValue === "indices" ){
						blockIndex.css('display', 'block');
					} else if (selectedValue === "matcap" ){
						blockMatcap.css('display', 'block');
					} else if (selectedValue === "classification" ){
						// add classification color selctor?
					} else if (selectedValue === "gps-time" ){
						blockGps.css('display', 'block');
					} else if(selectedValue === "number of returns"){
						
					} else if(selectedValue === "return number"){
						
					} else if(["source id", "point source id"].includes(selectedValue)){
						
					} else {
						blockExtra.css('display', 'block');
					}
				};

				attributeSelection.selectmenu({change: updateMaterialPanel});

				let update = () => {
					attributeSelection.val(material.activeAttributeName).selectmenu('refresh');
				};
				this.addVolatileListener(material, "point_color_type_changed", update);
				this.addVolatileListener(material, "active_attribute_changed", update);

				update();
				updateMaterialPanel();
			}

			{
				const schemes = Object.keys(Potree.Gradients).map(name => ({name: name, values: Gradients[name]}));

				let elSchemeContainer = panel.find("#elevation_gradient_scheme_selection");

				for(let scheme of schemes){
					let elScheme = $(`
					<span style="flex-grow: 1;">
					</span>
				`);

					const svg = Potree.Utils.createSvgGradient(scheme.values);
					svg.setAttributeNS(null, "class", `button-icon`);

					elScheme.append($(svg));

					elScheme.click( () => {
						material.gradient = Gradients[scheme.name];
					});

					elSchemeContainer.append(elScheme);
				}
			}

			{
				let matcaps = [
					{name: "Normals", icon: `${Potree.resourcePath}/icons/matcap/check_normal+y.jpg`}, 
					{name: "Basic 1", icon: `${Potree.resourcePath}/icons/matcap/basic_1.jpg`}, 
					{name: "Basic 2", icon: `${Potree.resourcePath}/icons/matcap/basic_2.jpg`}, 
					{name: "Basic Dark", icon: `${Potree.resourcePath}/icons/matcap/basic_dark.jpg`}, 
					{name: "Basic Side", icon: `${Potree.resourcePath}/icons/matcap/basic_side.jpg`}, 
					{name: "Ceramic Dark", icon: `${Potree.resourcePath}/icons/matcap/ceramic_dark.jpg`}, 
					{name: "Ceramic Lightbulb", icon: `${Potree.resourcePath}/icons/matcap/ceramic_lightbulb.jpg`}, 
					{name: "Clay Brown", icon: `${Potree.resourcePath}/icons/matcap/clay_brown.jpg`}, 
					{name: "Clay Muddy", icon: `${Potree.resourcePath}/icons/matcap/clay_muddy.jpg`}, 
					{name: "Clay Studio", icon: `${Potree.resourcePath}/icons/matcap/clay_studio.jpg`}, 
					{name: "Resin", icon: `${Potree.resourcePath}/icons/matcap/resin.jpg`}, 
					{name: "Skin", icon: `${Potree.resourcePath}/icons/matcap/skin.jpg`}, 
					{name: "Jade", icon: `${Potree.resourcePath}/icons/matcap/jade.jpg`}, 
					{name: "Metal_ Anisotropic", icon: `${Potree.resourcePath}/icons/matcap/metal_anisotropic.jpg`}, 
					{name: "Metal Carpaint", icon: `${Potree.resourcePath}/icons/matcap/metal_carpaint.jpg`}, 
					{name: "Metal Lead", icon: `${Potree.resourcePath}/icons/matcap/metal_lead.jpg`}, 
					{name: "Metal Shiny", icon: `${Potree.resourcePath}/icons/matcap/metal_shiny.jpg`}, 
					{name: "Pearl", icon: `${Potree.resourcePath}/icons/matcap/pearl.jpg`}, 
					{name: "Toon", icon: `${Potree.resourcePath}/icons/matcap/toon.jpg`},
					{name: "Check Rim Light", icon: `${Potree.resourcePath}/icons/matcap/check_rim_light.jpg`}, 
					{name: "Check Rim Dark", icon: `${Potree.resourcePath}/icons/matcap/check_rim_dark.jpg`}, 
					{name: "Contours 1", icon: `${Potree.resourcePath}/icons/matcap/contours_1.jpg`}, 
					{name: "Contours 2", icon: `${Potree.resourcePath}/icons/matcap/contours_2.jpg`}, 
					{name: "Contours 3", icon: `${Potree.resourcePath}/icons/matcap/contours_3.jpg`}, 
					{name: "Reflection Check Horizontal", icon: `${Potree.resourcePath}/icons/matcap/reflection_check_horizontal.jpg`}, 
					{name: "Reflection Check Vertical", icon: `${Potree.resourcePath}/icons/matcap/reflection_check_vertical.jpg`}, 
				];

				let elMatcapContainer = panel.find("#matcap_scheme_selection");

				for(let matcap of matcaps){
					let elMatcap = $(`
						<img src="${matcap.icon}" class="button-icon" style="width: 25%;" />
				`);

					elMatcap.click( () => {
						material.matcap = matcap.icon.substring(matcap.icon.lastIndexOf('/'));
					});

					elMatcapContainer.append(elMatcap);
				}
			}

			{
				panel.find('#sldRGBGamma').slider({
					value: material.rgbGamma,
					min: 0, max: 4, step: 0.01,
					slide: (event, ui) => {material.rgbGamma = ui.value;}
				});

				panel.find('#sldRGBContrast').slider({
					value: material.rgbContrast,
					min: -1, max: 1, step: 0.01,
					slide: (event, ui) => {material.rgbContrast = ui.value;}
				});

				panel.find('#sldRGBBrightness').slider({
					value: material.rgbBrightness,
					min: -1, max: 1, step: 0.01,
					slide: (event, ui) => {material.rgbBrightness = ui.value;}
				});

				panel.find('#sldExtraGamma').slider({
					value: material.extraGamma,
					min: 0, max: 4, step: 0.01,
					slide: (event, ui) => {material.extraGamma = ui.value;}
				});

				panel.find('#sldExtraBrightness').slider({
					value: material.extraBrightness,
					min: -1, max: 1, step: 0.01,
					slide: (event, ui) => {material.extraBrightness = ui.value;}
				});

				panel.find('#sldExtraContrast').slider({
					value: material.extraContrast,
					min: -1, max: 1, step: 0.01,
					slide: (event, ui) => {material.extraContrast = ui.value;}
				});

				panel.find('#sldHeightRange').slider({
					range: true,
					min: 0, max: 1000, step: 0.01,
					values: [0, 1000],
					slide: (event, ui) => {
						material.heightMin = ui.values[0];
						material.heightMax = ui.values[1];
					}
				});

				panel.find('#sldIntensityGamma').slider({
					value: material.intensityGamma,
					min: 0, max: 4, step: 0.01,
					slide: (event, ui) => {material.intensityGamma = ui.value;}
				});

				panel.find('#sldIntensityContrast').slider({
					value: material.intensityContrast,
					min: -1, max: 1, step: 0.01,
					slide: (event, ui) => {material.intensityContrast = ui.value;}
				});

				panel.find('#sldIntensityBrightness').slider({
					value: material.intensityBrightness,
					min: -1, max: 1, step: 0.01,
					slide: (event, ui) => {material.intensityBrightness = ui.value;}
				});

				panel.find('#sldWeightRGB').slider({
					value: material.weightRGB,
					min: 0, max: 1, step: 0.01,
					slide: (event, ui) => {material.weightRGB = ui.value;}
				});

				panel.find('#sldWeightIntensity').slider({
					value: material.weightIntensity,
					min: 0, max: 1, step: 0.01,
					slide: (event, ui) => {material.weightIntensity = ui.value;}
				});

				panel.find('#sldWeightElevation').slider({
					value: material.weightElevation,
					min: 0, max: 1, step: 0.01,
					slide: (event, ui) => {material.weightElevation = ui.value;}
				});

				panel.find('#sldWeightClassification').slider({
					value: material.weightClassification,
					min: 0, max: 1, step: 0.01,
					slide: (event, ui) => {material.weightClassification = ui.value;}
				});

				panel.find('#sldWeightReturnNumber').slider({
					value: material.weightReturnNumber,
					min: 0, max: 1, step: 0.01,
					slide: (event, ui) => {material.weightReturnNumber = ui.value;}
				});

				panel.find('#sldWeightSourceID').slider({
					value: material.weightSourceID,
					min: 0, max: 1, step: 0.01,
					slide: (event, ui) => {material.weightSourceID = ui.value;}
				});

				panel.find(`#materials\\.color\\.picker`).spectrum({
					flat: true,
					showInput: true,
					preferredFormat: 'rgb',
					cancelText: '',
					chooseText: 'Apply',
					color: `#${material.color.getHexString()}`,
					move: color => {
						let cRGB = color.toRgb();
						let tc = new Color().setRGB(cRGB.r / 255, cRGB.g / 255, cRGB.b / 255);
						material.color = tc;
					},
					change: color => {
						let cRGB = color.toRgb();
						let tc = new Color().setRGB(cRGB.r / 255, cRGB.g / 255, cRGB.b / 255);
						material.color = tc;
					}
				});

				this.addVolatileListener(material, "color_changed", () => {
					panel.find(`#materials\\.color\\.picker`)
						.spectrum('set', `#${material.color.getHexString()}`);
				});

				let updateHeightRange = function () {
					

					let aPosition = pointcloud.getAttribute("position");

					let bMin, bMax;

					if(aPosition){
						// for new format 2.0 and loader that contain precomputed min/max of attributes
						let min = aPosition.range[0][2];
						let max = aPosition.range[1][2];
						let width = max - min;

						bMin = min - 0.2 * width;
						bMax = max + 0.2 * width;
					}else {
						// for format up until exlusive 2.0
						let box = [pointcloud.pcoGeometry.tightBoundingBox, pointcloud.getBoundingBoxWorld()]
							.find(v => v !== undefined);

						pointcloud.updateMatrixWorld(true);
						box = Utils.computeTransformedBoundingBox(box, pointcloud.matrixWorld);

						let bWidth = box.max.z - box.min.z;
						bMin = box.min.z - 0.2 * bWidth;
						bMax = box.max.z + 0.2 * bWidth;
					}

					let range = material.elevationRange;

					panel.find('#lblHeightRange').html(`${range[0].toFixed(2)} to ${range[1].toFixed(2)}`);
					panel.find('#sldHeightRange').slider({min: bMin, max: bMax, values: range});
				};

				let updateExtraRange = function () {

					let attributeName = material.activeAttributeName;
					let attribute = pointcloud.getAttribute(attributeName);

					if(attribute == null){
						return;
					}
					
					let range = material.getRange(attributeName);

					if(range == null){
						range = attribute.range;
					}

					// currently only supporting scalar ranges.
					// rgba, normals, positions, etc have vector ranges, however
					let isValidRange = (typeof range[0] === "number") && (typeof range[1] === "number");
					if(!isValidRange){
						return;
					}

					if(range){
						let msg = `${range[0].toFixed(2)} to ${range[1].toFixed(2)}`;
						panel.find('#lblExtraRange').html(msg);
					}else {
						panel.find("could not deduce range");
					}
				};

				let updateIntensityRange = function () {
					let range = material.intensityRange;

					panel.find('#lblIntensityRange').html(`${parseInt(range[0])} to ${parseInt(range[1])}`);
				};

				{
					updateHeightRange();
					panel.find(`#sldHeightRange`).slider('option', 'min');
					panel.find(`#sldHeightRange`).slider('option', 'max');
				}

				{
					let elGradientRepeat = panel.find("#gradient_repeat_option");
					elGradientRepeat.selectgroup({title: "Gradient"});

					elGradientRepeat.find("input").click( (e) => {
						this.viewer.setElevationGradientRepeat(ElevationGradientRepeat[e.target.value]);
					});

					let current = Object.keys(ElevationGradientRepeat)
						.filter(key => ElevationGradientRepeat[key] === this.viewer.elevationGradientRepeat);
					elGradientRepeat.find(`input[value=${current}]`).trigger("click");
				}

				let onIntensityChange = () => {
					let gamma = material.intensityGamma;
					let contrast = material.intensityContrast;
					let brightness = material.intensityBrightness;

					updateIntensityRange();

					panel.find('#lblIntensityGamma').html(gamma.toFixed(2));
					panel.find('#lblIntensityContrast').html(contrast.toFixed(2));
					panel.find('#lblIntensityBrightness').html(brightness.toFixed(2));

					panel.find('#sldIntensityGamma').slider({value: gamma});
					panel.find('#sldIntensityContrast').slider({value: contrast});
					panel.find('#sldIntensityBrightness').slider({value: brightness});
				};

				let onRGBChange = () => {
					let gamma = material.rgbGamma;
					let contrast = material.rgbContrast;
					let brightness = material.rgbBrightness;

					panel.find('#lblRGBGamma').html(gamma.toFixed(2));
					panel.find('#lblRGBContrast').html(contrast.toFixed(2));
					panel.find('#lblRGBBrightness').html(brightness.toFixed(2));

					panel.find('#sldRGBGamma').slider({value: gamma});
					panel.find('#sldRGBContrast').slider({value: contrast});
					panel.find('#sldRGBBrightness').slider({value: brightness});
				};

				this.addVolatileListener(material, "material_property_changed", updateExtraRange);
				this.addVolatileListener(material, "material_property_changed", updateHeightRange);
				this.addVolatileListener(material, "material_property_changed", onIntensityChange);
				this.addVolatileListener(material, "material_property_changed", onRGBChange);

				updateExtraRange();
				updateHeightRange();
				onIntensityChange();
				onRGBChange();
			}

		}

		

		setMeasurement(object){

			let TYPE = {
				DISTANCE: {panel: DistancePanel},
				AREA: {panel: AreaPanel},
				POINT: {panel: PointPanel},
				ANGLE: {panel: AnglePanel},
				HEIGHT: {panel: HeightPanel},
				PROFILE: {panel: ProfilePanel},
				VOLUME: {panel: VolumePanel},
				CIRCLE: {panel: CirclePanel},
				OTHER: {panel: PointPanel},
			};

			let getType = (measurement) => {
				if (measurement instanceof Measure) {
					if (measurement.showDistances && !measurement.showArea && !measurement.showAngles) {
						return TYPE.DISTANCE;
					} else if (measurement.showDistances && measurement.showArea && !measurement.showAngles) {
						return TYPE.AREA;
					} else if (measurement.maxMarkers === 1) {
						return TYPE.POINT;
					} else if (!measurement.showDistances && !measurement.showArea && measurement.showAngles) {
						return TYPE.ANGLE;
					} else if (measurement.showHeight) {
						return TYPE.HEIGHT;
					} else if (measurement.showCircle) {
						return TYPE.CIRCLE;
					} else {
						return TYPE.OTHER;
					}
				} else if (measurement instanceof Profile) {
					return TYPE.PROFILE;
				} else if (measurement instanceof Volume) {
					return TYPE.VOLUME;
				}
			};

			//this.container.html("measurement");

			let type = getType(object);
			let Panel = type.panel;

			let panel = new Panel(this.viewer, object, this);
			this.container.append(panel.elContent);
		}

		setCamera(camera){
			let panel = new CameraPanel(this.viewer, this);
			this.container.append(panel.elContent);
		}

		setAnnotation(annotation){
			let panel = new AnnotationPanel(this.viewer, this, annotation);
			this.container.append(panel.elContent);
		}

		setCameraAnimation(animation){
			let panel = new CameraAnimationPanel(this.viewer, this, animation);
			this.container.append(panel.elContent);
		}

	}

	function addCommas(nStr){
		nStr += '';
		let x = nStr.split('.');
		let x1 = x[0];
		let x2 = x.length > 1 ? '.' + x[1] : '';
		let rgx = /(\d+)(\d{3})/;
		while (rgx.test(x1)) {
			x1 = x1.replace(rgx, '$1' + ',' + '$2');
		}
		return x1 + x2;
	};

	function format(value){
		return addCommas(value.toFixed(3));
	};

	class HierarchicalSlider{

		constructor(params = {}){
			
			this.element = document.createElement("div");

			this.labels = [];
			this.sliders = [];
			this.range = params.range != null ? params.range : [0, 1];
			this.slide = params.slide != null ? params.slide : null;
			this.step = params.step != null ? params.step : 0.0001;

			let levels = params.levels != null ? params.levels : 1;

			for(let level = 0; level < levels; level++){
				this.addLevel();
			}

		}

		setRange(range){
			this.range = [...range];

			{ // root slider
				let slider = this.sliders[0];

				$(slider).slider({
					min: range[0],
					max: range[1],
				});
			}

			for(let i = 1; i < this.sliders.length; i++){
				let parentSlider = this.sliders[i - 1];
				let slider = this.sliders[i];

				let parentValues = $(parentSlider).slider("option", "values");
				let childRange = [...parentValues];

				$(slider).slider({
					min: childRange[0],
					max: childRange[1],
				});
			}
			
			this.updateLabels();
		}

		setValues(values){
			for(let slider of this.sliders){
				$(slider).slider({
					values: [...values],
				});
			}

			this.updateLabels();
		}

		addLevel(){
			const elLevel = document.createElement("li");
			const elRange = document.createTextNode("Range: ");
			const label = document.createElement("span");
			const slider = document.createElement("div");

			let level = this.sliders.length;
			let [min, max] = [0, 0];

			if(this.sliders.length === 0){
				[min, max] = this.range;
			}else {
				let parentSlider = this.sliders[this.sliders.length - 1];
				[min, max] = $(parentSlider).slider("option", "values");
			}
			
			$(slider).slider({
				range: true, 
				min: min, 
				max: max,
				step: this.step,
				values: [min, max],
				slide: (event, ui) => {
					
					// set all descendants to same range
					let levels = this.sliders.length;
					for(let i = level + 1; i < levels; i++){
						let descendant = this.sliders[i];

						$(descendant).slider({
							range: true,
							min: ui.values[0],
							max: ui.values[1],
							values: [...ui.values],
						});
					}

					if(this.slide){
						let values = [...ui.values];

						this.slide({
							target: this, 
							range: this.range,
							values: values,
						});
					}

					this.updateLabels();
				},
			});

			elLevel.append(elRange, label, slider);

			this.sliders.push(slider);
			this.labels.push(label);
			this.element.append(elLevel);

			this.updateLabels();
		}

		removeLevel(){

		}

		updateSliders(){

		}

		updateLabels(){

			let levels = this.sliders.length;

			for(let i = 0; i < levels; i++){

				let slider = this.sliders[i];
				let label = this.labels[i];

				let [min, max] = $(slider).slider("option", "values");
				let strMin = format(min);
				let strMax = format(max);
				let strLabel = `${strMin} to ${strMax}`;

				label.innerHTML = strLabel;
			}

		}


	}

	class OrientedImageControls extends EventDispatcher{
		
		constructor(viewer){
			super();
			
			this.viewer = viewer;
			this.renderer = viewer.renderer;

			this.originalCam = viewer.scene.getActiveCamera();
			this.shearCam = viewer.scene.getActiveCamera().clone();
			this.shearCam.rotation.set(this.originalCam.rotation.toArray());
			this.shearCam.updateProjectionMatrix();
			this.shearCam.updateProjectionMatrix = () => {
				return this.shearCam.projectionMatrix;
			};

			this.image = null;

			this.fadeFactor = 20;
			this.fovDelta = 0;

			this.fovMin = 0.1;
			this.fovMax = 120;

			this.shear = [0, 0];

			// const style = ``;
			this.elUp =    $(`<input type="button" value="🡅" style="position: absolute; top: 10px; left: calc(50%); z-index: 1000" />`);
			this.elRight = $(`<input type="button" value="🡆" style="position: absolute; top: calc(50%); right: 10px; z-index: 1000" />`);
			this.elDown =  $(`<input type="button" value="🡇" style="position: absolute; bottom: 10px; left: calc(50%); z-index: 1000" />`);
			this.elLeft =  $(`<input type="button" value="🡄" style="position: absolute; top: calc(50%); left: 10px; z-index: 1000" />`);
			this.elExit = $(`<input type="button" value="Back to 3D view" style="position: absolute; bottom: 10px; right: 10px; z-index: 1000" />`);

			this.elExit.click( () => {
				this.release();
			});

			this.elUp.click(() => {
				const fovY = viewer.getFOV();
				const top = Math.tan(MathUtils.degToRad(fovY / 2));
				this.shear[1] += 0.1 * top;
			});

			this.elRight.click(() => {
				const fovY = viewer.getFOV();
				const top = Math.tan(MathUtils.degToRad(fovY / 2));
				this.shear[0] += 0.1 * top;
			});

			this.elDown.click(() => {
				const fovY = viewer.getFOV();
				const top = Math.tan(MathUtils.degToRad(fovY / 2));
				this.shear[1] -= 0.1 * top;
			});

			this.elLeft.click(() => {
				const fovY = viewer.getFOV();
				const top = Math.tan(MathUtils.degToRad(fovY / 2));
				this.shear[0] -= 0.1 * top;
			});

			this.scene = null;
			this.sceneControls = new Scene();

			let scroll = (e) => {
				this.fovDelta += -e.delta * 1.0;
			};

			this.addEventListener('mousewheel', scroll);
			//this.addEventListener("mousemove", onMove);
		}

		hasSomethingCaptured(){
			return this.image !== null;
		}

		capture(image){
			if(this.hasSomethingCaptured()){
				return;
			}

			this.image = image;

			this.originalFOV = this.viewer.getFOV();
			this.originalControls = this.viewer.getControls();

			this.viewer.setControls(this);
			this.viewer.scene.overrideCamera = this.shearCam;

			const elCanvas = this.viewer.renderer.domElement;
			const elRoot = $(elCanvas.parentElement);

			this.shear = [0, 0];


			elRoot.append(this.elUp);
			elRoot.append(this.elRight);
			elRoot.append(this.elDown);
			elRoot.append(this.elLeft);
			elRoot.append(this.elExit);
		}

		release(){
			this.image = null;

			this.viewer.scene.overrideCamera = null;

			this.elUp.detach();
			this.elRight.detach();
			this.elDown.detach();
			this.elLeft.detach();
			this.elExit.detach();

			this.viewer.setFOV(this.originalFOV);
			this.viewer.setControls(this.originalControls);
		}

		setScene (scene) {
			this.scene = scene;
		}

		update (delta) {
			// const view = this.scene.view;

			// let prevTotal = this.shearCam.projectionMatrix.elements.reduce( (a, i) => a + i, 0);

			//const progression = Math.min(1, this.fadeFactor * delta);
			//const attenuation = Math.max(0, 1 - this.fadeFactor * delta);
			const progression = 1;
			const attenuation = 0;

			const oldFov = this.viewer.getFOV();
			let fovProgression =  progression * this.fovDelta;
			let newFov = oldFov * ((1 + fovProgression / 10));

			newFov = Math.max(this.fovMin, newFov);
			newFov = Math.min(this.fovMax, newFov);

			let diff = newFov / oldFov;

			const mouse = this.viewer.inputHandler.mouse;
			const canvasSize = this.viewer.renderer.getSize(new Vector2());
			const uv = [
				(mouse.x / canvasSize.x),
				((canvasSize.y - mouse.y) / canvasSize.y)
			];

			const fovY = newFov;
			const aspect = canvasSize.x / canvasSize.y;
			const top = Math.tan(MathUtils.degToRad(fovY / 2));
			const height = 2 * top;
			const width = aspect * height;

			const shearRangeX = [
				this.shear[0] - 0.5 * width,
				this.shear[0] + 0.5 * width,
			];

			const shearRangeY = [
				this.shear[1] - 0.5 * height,
				this.shear[1] + 0.5 * height,
			];

			const shx = (1 - uv[0]) * shearRangeX[0] + uv[0] * shearRangeX[1];
			const shy = (1 - uv[1]) * shearRangeY[0] + uv[1] * shearRangeY[1];

			const shu = (1 - diff);

			const newShear =  [
				(1 - shu) * this.shear[0] + shu * shx,
				(1 - shu) * this.shear[1] + shu * shy,
			];
			
			this.shear = newShear;
			this.viewer.setFOV(newFov);
			
			const {originalCam, shearCam} = this;

			originalCam.fov = newFov;
			originalCam.updateMatrixWorld();
			originalCam.updateProjectionMatrix();
			shearCam.copy(originalCam);
			shearCam.rotation.set(...originalCam.rotation.toArray());

			shearCam.updateMatrixWorld();
			shearCam.projectionMatrix.copy(originalCam.projectionMatrix);

			const [sx, sy] = this.shear;
			const mShear = new Matrix4().set(
				1, 0, sx, 0,
				0, 1, sy, 0,
				0, 0, 1, 0,
				0, 0, 0, 1,
			);

			const proj = shearCam.projectionMatrix;
			proj.multiply(mShear);
			shearCam.projectionMatrixInverse.copy(proj).invert();

			let total = shearCam.projectionMatrix.elements.reduce( (a, i) => a + i, 0);

			this.fovDelta *= attenuation;
		}
	};

	// https://support.pix4d.com/hc/en-us/articles/205675256-How-are-yaw-pitch-roll-defined
	// https://support.pix4d.com/hc/en-us/articles/202558969-How-are-omega-phi-kappa-defined

	function createMaterial(){

		let vertexShader = `
	uniform float uNear;
	varying vec2 vUV;
	varying vec4 vDebug;
	
	void main(){
		vDebug = vec4(0.0, 1.0, 0.0, 1.0);
		vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
		// make sure that this mesh is at least in front of the near plane
		modelViewPosition.xyz += normalize(modelViewPosition.xyz) * uNear;
		gl_Position = projectionMatrix * modelViewPosition;
		vUV = uv;
	}
	`;

		let fragmentShader = `
	uniform sampler2D tColor;
	uniform float uOpacity;
	varying vec2 vUV;
	varying vec4 vDebug;
	void main(){
		vec4 color = texture2D(tColor, vUV);
		gl_FragColor = color;
		gl_FragColor.a = uOpacity;
	}
	`;
		const material = new ShaderMaterial( {
			uniforms: {
				// time: { value: 1.0 },
				// resolution: { value: new THREE.Vector2() }
				tColor: {value: new Texture() },
				uNear: {value: 0.0},
				uOpacity: {value: 1.0},
			},
			vertexShader: vertexShader,
			fragmentShader: fragmentShader,
			side: DoubleSide,
		} );

		material.side = DoubleSide;

		return material;
	}

	const planeGeometry = new PlaneGeometry(1, 1);
	const lineGeometry = new Geometry();

	lineGeometry.vertices.push(
		new Vector3(-0.5, -0.5, 0),
		new Vector3( 0.5, -0.5, 0),
		new Vector3( 0.5,  0.5, 0),
		new Vector3(-0.5,  0.5, 0),
		new Vector3(-0.5, -0.5, 0),
	);

	class OrientedImage{

		constructor(id){

			this.id = id;
			this.fov = 1.0;
			this.position = new Vector3();
			this.rotation = new Vector3();
			this.width = 0;
			this.height = 0;
			this.fov = 1.0;

			const material = createMaterial();
			const lineMaterial = new LineBasicMaterial( { color: 0x00ff00 } );
			this.mesh = new Mesh(planeGeometry, material);
			this.line = new Line(lineGeometry, lineMaterial);
			this.texture = null;

			this.mesh.orientedImage = this;
		}

		set(position, rotation, dimension, fov){

			let radians = rotation.map(MathUtils.degToRad);

			this.position.set(...position);
			this.mesh.position.set(...position);

			this.rotation.set(...radians);
			this.mesh.rotation.set(...radians);

			[this.width, this.height] = dimension;
			this.mesh.scale.set(this.width / this.height, 1, 1);

			this.fov = fov;

			this.updateTransform();
		}

		updateTransform(){
			let {mesh, line, fov} = this;

			mesh.updateMatrixWorld();
			const dir = mesh.getWorldDirection();
			const alpha = MathUtils.degToRad(fov / 2);
			const d = -0.5 / Math.tan(alpha);
			const move = dir.clone().multiplyScalar(d);
			mesh.position.add(move);

			line.position.copy(mesh.position);
			line.scale.copy(mesh.scale);
			line.rotation.copy(mesh.rotation);
		}

	};

	class OrientedImages extends EventDispatcher{

		constructor(){
			super();

			this.node = null;
			this.cameraParams = null;
			this.imageParams = null;
			this.images = null;
			this._visible = true;
		}

		set visible(visible){
			if(this._visible === visible){
				return;
			}

			for(const image of this.images){
				image.mesh.visible = visible;
				image.line.visible = visible;
			}

			this._visible = visible;
			this.dispatchEvent({
				type: "visibility_changed",
				images: this,
			});
		}

		get visible(){
			return this._visible;
		}


	};

	class OrientedImageLoader{

		static async loadCameraParams(path){
			const res = await fetch(path);
			const text = await res.text();

			const parser = new DOMParser();
			const doc = parser.parseFromString(text, "application/xml");

			const width = parseInt(doc.getElementsByTagName("width")[0].textContent);
			const height = parseInt(doc.getElementsByTagName("height")[0].textContent);
			const f = parseFloat(doc.getElementsByTagName("f")[0].textContent);

			let a = (height / 2)  / f;
			let fov = 2 * MathUtils.radToDeg(Math.atan(a));

			const params = {
				path: path,
				width: width,
				height: height,
				f: f,
				fov: fov,
			};

			return params;
		}

		static async loadImageParams(path){

			const response = await fetch(path);
			if(!response.ok){
				console.error(`failed to load ${path}`);
				return;
			}

			const content = await response.text();
			const lines = content.split(/\r?\n/);
			const imageParams = [];

			for(let i = 1; i < lines.length; i++){
				const line = lines[i];

				if(line.startsWith("#")){
					continue;
				}

				const tokens = line.split(/\s+/);

				if(tokens.length < 6){
					continue;
				}

				const params = {
					id: tokens[0],
					x: Number.parseFloat(tokens[1]),
					y: Number.parseFloat(tokens[2]),
					z: Number.parseFloat(tokens[3]),
					omega: Number.parseFloat(tokens[4]),
					phi: Number.parseFloat(tokens[5]),
					kappa: Number.parseFloat(tokens[6]),
				};

				// const whitelist = ["47518.jpg"];
				// if(whitelist.includes(params.id)){
				// 	imageParams.push(params);
				// }
				imageParams.push(params);
			}

			// debug
			//return [imageParams[50]];

			return imageParams;
		}

		static async load(cameraParamsPath, imageParamsPath, viewer){

			const tStart = performance.now();

			const [cameraParams, imageParams] = await Promise.all([
				OrientedImageLoader.loadCameraParams(cameraParamsPath),
				OrientedImageLoader.loadImageParams(imageParamsPath),
			]);

			const orientedImageControls = new OrientedImageControls(viewer);
			const raycaster = new Raycaster();

			const tEnd = performance.now();
			console.log(tEnd - tStart);

			// const sp = new THREE.PlaneGeometry(1, 1);
			// const lg = new THREE.Geometry();

			// lg.vertices.push(
			// 	new THREE.Vector3(-0.5, -0.5, 0),
			// 	new THREE.Vector3( 0.5, -0.5, 0),
			// 	new THREE.Vector3( 0.5,  0.5, 0),
			// 	new THREE.Vector3(-0.5,  0.5, 0),
			// 	new THREE.Vector3(-0.5, -0.5, 0),
			// );

			const {width, height} = cameraParams;
			const orientedImages = [];
			const sceneNode = new Object3D();
			sceneNode.name = "oriented_images";

			for(const params of imageParams){

				// const material = createMaterial();
				// const lm = new THREE.LineBasicMaterial( { color: 0x00ff00 } );
				// const mesh = new THREE.Mesh(sp, material);

				const {x, y, z, omega, phi, kappa} = params;
				// const [rx, ry, rz] = [omega, phi, kappa]
				// 	.map(THREE.Math.degToRad);
				
				// mesh.position.set(x, y, z);
				// mesh.scale.set(width / height, 1, 1);
				// mesh.rotation.set(rx, ry, rz);
				// {
				// 	mesh.updateMatrixWorld();
				// 	const dir = mesh.getWorldDirection();
				// 	const alpha = THREE.Math.degToRad(cameraParams.fov / 2);
				// 	const d = -0.5 / Math.tan(alpha);
				// 	const move = dir.clone().multiplyScalar(d);
				// 	mesh.position.add(move);
				// }
				// sceneNode.add(mesh);

				// const line = new THREE.Line(lg, lm);
				// line.position.copy(mesh.position);
				// line.scale.copy(mesh.scale);
				// line.rotation.copy(mesh.rotation);
				// sceneNode.add(line);

				let orientedImage = new OrientedImage(params.id);
				// orientedImage.setPosition(x, y, z);
				// orientedImage.setRotation(omega, phi, kappa);
				// orientedImage.setDimension(width, height);
				let position = [x, y, z];
				let rotation = [omega, phi, kappa];
				let dimension = [width, height];
				orientedImage.set(position, rotation, dimension, cameraParams.fov);

				sceneNode.add(orientedImage.mesh);
				sceneNode.add(orientedImage.line);
				
				orientedImages.push(orientedImage);
			}

			let hoveredElement = null;
			let clipVolume = null;

			const onMouseMove = (evt) => {
				const tStart = performance.now();
				if(hoveredElement){
					hoveredElement.line.material.color.setRGB(0, 1, 0);
				}
				evt.preventDefault();

				//var array = getMousePosition( container, evt.clientX, evt.clientY );
				const rect = viewer.renderer.domElement.getBoundingClientRect();
				const [x, y] = [evt.clientX, evt.clientY];
				const array = [ 
					( x - rect.left ) / rect.width, 
					( y - rect.top ) / rect.height 
				];
				const onClickPosition = new Vector2(...array);
				//const intersects = getIntersects(onClickPosition, scene.children);
				const camera = viewer.scene.getActiveCamera();
				const mouse = new Vector3(
					+ ( onClickPosition.x * 2 ) - 1, 
					- ( onClickPosition.y * 2 ) + 1 );
				const objects = orientedImages.map(i => i.mesh);
				raycaster.setFromCamera( mouse, camera );
				const intersects = raycaster.intersectObjects( objects );
				let selectionChanged = false;

				if ( intersects.length > 0){
					//console.log(intersects);
					const intersection = intersects[0];
					const orientedImage = intersection.object.orientedImage;
					orientedImage.line.material.color.setRGB(1, 0, 0);
					selectionChanged = hoveredElement !== orientedImage;
					hoveredElement = orientedImage;
				}else {
					hoveredElement = null;
				}

				let shouldRemoveClipVolume = clipVolume !== null && hoveredElement === null;
				let shouldAddClipVolume = clipVolume === null && hoveredElement !== null;

				if(clipVolume !== null && (hoveredElement === null || selectionChanged)){
					// remove existing
					viewer.scene.removePolygonClipVolume(clipVolume);
					clipVolume = null;
				}
				
				if(shouldAddClipVolume || selectionChanged){
					const img = hoveredElement;
					const fov = cameraParams.fov;
					const aspect  = cameraParams.width / cameraParams.height;
					const near = 1.0;
					const far = 1000 * 1000;
					const camera = new PerspectiveCamera(fov, aspect, near, far);
					camera.rotation.order = viewer.scene.getActiveCamera().rotation.order;
					camera.rotation.copy(img.mesh.rotation);
					{
						const mesh = img.mesh;
						const dir = mesh.getWorldDirection();
						const pos = mesh.position;
						const alpha = MathUtils.degToRad(fov / 2);
						const d = 0.5 / Math.tan(alpha);
						const newCamPos = pos.clone().add(dir.clone().multiplyScalar(d));
						const newCamDir = pos.clone().sub(newCamPos);
						const newCamTarget = new Vector3().addVectors(
							newCamPos,
							newCamDir.clone().multiplyScalar(viewer.getMoveSpeed()));
						camera.position.copy(newCamPos);
					}
					let volume = new Potree.PolygonClipVolume(camera);
					let m0 = new Mesh();
					let m1 = new Mesh();
					let m2 = new Mesh();
					let m3 = new Mesh();
					m0.position.set(-1, -1, 0);
					m1.position.set( 1, -1, 0);
					m2.position.set( 1,  1, 0);
					m3.position.set(-1,  1, 0);
					volume.markers.push(m0, m1, m2, m3);
					volume.initialized = true;
					
					viewer.scene.addPolygonClipVolume(volume);
					clipVolume = volume;
				}
				const tEnd = performance.now();
				//console.log(tEnd - tStart);
			};

			const moveToImage = (image) => {
				console.log("move to image " + image.id);

				const mesh = image.mesh;
				const newCamPos = image.position.clone();
				const newCamTarget = mesh.position.clone();

				viewer.scene.view.setView(newCamPos, newCamTarget, 500, () => {
					orientedImageControls.capture(image);
				});

				if(image.texture === null){

					const target = image;

					const tmpImagePath = `${Potree.resourcePath}/images/loading.jpg`;
					new TextureLoader().load(tmpImagePath,
						(texture) => {
							if(target.texture === null){
								target.texture = texture;
								target.mesh.material.uniforms.tColor.value = texture;
								mesh.material.needsUpdate = true;
							}
						}
					);

					const imagePath = `${imageParamsPath}/../${target.id}`;
					new TextureLoader().load(imagePath,
						(texture) => {
							target.texture = texture;
							target.mesh.material.uniforms.tColor.value = texture;
							mesh.material.needsUpdate = true;
						}
					);
					

				}
			};

			const onMouseClick = (evt) => {

				if(orientedImageControls.hasSomethingCaptured()){
					return;
				}

				if(hoveredElement){
					moveToImage(hoveredElement);
				}
			};
			viewer.renderer.domElement.addEventListener( 'mousemove', onMouseMove, false );
			viewer.renderer.domElement.addEventListener( 'mousedown', onMouseClick, false );

			viewer.addEventListener("update", () => {

				for(const image of orientedImages){
					const world = image.mesh.matrixWorld;
					const {width, height} = image;
					const aspect = width / height;

					const camera = viewer.scene.getActiveCamera();

					const imgPos = image.mesh.getWorldPosition(new Vector3());
					const camPos = camera.position;
					const d = camPos.distanceTo(imgPos);

					const minSize = 1; // in degrees of fov
					const a = MathUtils.degToRad(minSize);
					let r = d * Math.tan(a);
					r = Math.max(r, 1);


					image.mesh.scale.set(r * aspect, r, 1);
					image.line.scale.set(r * aspect, r, 1);

					image.mesh.material.uniforms.uNear.value = camera.near;

				}

			});

			const images = new OrientedImages();
			images.node = sceneNode;
			images.cameraParamsPath = cameraParamsPath;
			images.imageParamsPath = imageParamsPath;
			images.cameraParams = cameraParams;
			images.imageParams = imageParams;
			images.images = orientedImages;

			Potree.debug.moveToImage = moveToImage;

			return images;
		}
	}

	let sg = new SphereGeometry(1, 8, 8);
	let sgHigh = new SphereGeometry(1, 128, 128);

	let sm = new MeshBasicMaterial({side: BackSide});
	let smHovered = new MeshBasicMaterial({side: BackSide, color: 0xff0000});

	let raycaster = new Raycaster();
	let currentlyHovered = null;

	let previousView = {
		controls: null,
		position: null,
		target: null,
	};

	class Image360{

		constructor(file, time, longitude, latitude, altitude, course, pitch, roll){
			this.file = file;
			this.time = time;
			this.longitude = longitude;
			this.latitude = latitude;
			this.altitude = altitude;
			this.course = course;
			this.pitch = pitch;
			this.roll = roll;
			this.mesh = null;
		}
	};

	class Images360 extends EventDispatcher{

		constructor(viewer){
			super();

			this.viewer = viewer;

			this.selectingEnabled = true;

			this.images = [];
			this.node = new Object3D();

			this.sphere = new Mesh(sgHigh, sm);
			this.sphere.visible = false;
			this.sphere.scale.set(1000, 1000, 1000);
			this.node.add(this.sphere);
			this._visible = true;
			// this.node.add(label);

			this.focusedImage = null;

			let elUnfocus = document.createElement("input");
			elUnfocus.type = "button";
			elUnfocus.value = "unfocus";
			elUnfocus.style.position = "absolute";
			elUnfocus.style.right = "10px";
			elUnfocus.style.bottom = "10px";
			elUnfocus.style.zIndex = "10000";
			elUnfocus.style.fontSize = "2em";
			elUnfocus.addEventListener("click", () => this.unfocus());
			this.elUnfocus = elUnfocus;

			this.domRoot = viewer.renderer.domElement.parentElement;
			this.domRoot.appendChild(elUnfocus);
			this.elUnfocus.style.display = "none";

			viewer.addEventListener("update", () => {
				this.update(viewer);
			});
			viewer.inputHandler.addInputListener(this);

			this.addEventListener("mousedown", () => {
				if(currentlyHovered){
					this.focus(currentlyHovered.image360);
				}
			});
			
		};

		set visible(visible){
			if(this._visible === visible){
				return;
			}


			for(const image of this.images){
				image.mesh.visible = visible && (this.focusedImage == null);
			}

			this.sphere.visible = visible && (this.focusedImage != null);
			this._visible = visible;
			this.dispatchEvent({
				type: "visibility_changed",
				images: this,
			});
		}

		get visible(){
			return this._visible;
		}

		focus(image360){
			if(this.focusedImage !== null){
				this.unfocus();
			}

			previousView = {
				controls: this.viewer.controls,
				position: this.viewer.scene.view.position.clone(),
				target: viewer.scene.view.getPivot(),
			};

			this.viewer.setControls(this.viewer.orbitControls);
			this.viewer.orbitControls.doubleClockZoomEnabled = false;

			for(let image of this.images){
				image.mesh.visible = false;
			}

			this.selectingEnabled = false;

			this.sphere.visible = false;

			this.load(image360).then( () => {
				this.sphere.visible = true;
				this.sphere.material.map = image360.texture;
				this.sphere.material.needsUpdate = true;
			});

			{ // orientation
				let {course, pitch, roll} = image360;
				this.sphere.rotation.set(
					MathUtils.degToRad(+roll + 90),
					MathUtils.degToRad(-pitch),
					MathUtils.degToRad(-course + 90),
					"ZYX"
				);
			}

			this.sphere.position.set(...image360.position);

			let target = new Vector3(...image360.position);
			let dir = target.clone().sub(viewer.scene.view.position).normalize();
			let move = dir.multiplyScalar(0.000001);
			let newCamPos = target.clone().sub(move);

			viewer.scene.view.setView(
				newCamPos, 
				target,
				500
			);

			this.focusedImage = image360;

			this.elUnfocus.style.display = "";
		}

		unfocus(){
			this.selectingEnabled = true;

			for(let image of this.images){
				image.mesh.visible = true;
			}

			let image = this.focusedImage;

			if(image === null){
				return;
			}


			this.sphere.material.map = null;
			this.sphere.material.needsUpdate = true;
			this.sphere.visible = false;

			let pos = viewer.scene.view.position;
			let target = viewer.scene.view.getPivot();
			let dir = target.clone().sub(pos).normalize();
			let move = dir.multiplyScalar(10);
			let newCamPos = target.clone().sub(move);

			viewer.orbitControls.doubleClockZoomEnabled = true;
			viewer.setControls(previousView.controls);

			viewer.scene.view.setView(
				previousView.position, 
				previousView.target,
				500
			);


			this.focusedImage = null;

			this.elUnfocus.style.display = "none";
		}

		load(image360){

			return new Promise(resolve => {
				let texture = new TextureLoader().load(image360.file, resolve);
				texture.wrapS = RepeatWrapping;
				texture.repeat.x = -1;

				image360.texture = texture;
			});

		}

		handleHovering(){
			let mouse = viewer.inputHandler.mouse;
			let camera = viewer.scene.getActiveCamera();
			let domElement = viewer.renderer.domElement;

			let ray = Potree.Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);

			// let tStart = performance.now();
			raycaster.ray.copy(ray);
			let intersections = raycaster.intersectObjects(this.node.children);

			if(intersections.length === 0){
				// label.visible = false;

				return;
			}

			let intersection = intersections[0];
			currentlyHovered = intersection.object;
			currentlyHovered.material = smHovered;

			//label.visible = true;
			//label.setText(currentlyHovered.image360.file);
			//currentlyHovered.getWorldPosition(label.position);
		}

		update(){

			let {viewer} = this;

			if(currentlyHovered){
				currentlyHovered.material = sm;
				currentlyHovered = null;
			}

			if(this.selectingEnabled){
				this.handleHovering();
			}

		}

	};


	class Images360Loader{

		static async load(url, viewer, params = {}){

			if(!params.transform){
				params.transform = {
					forward: a => a,
				};
			}
			
			let response = await fetch(`${url}/coordinates.txt`);
			let text = await response.text();

			let lines = text.split(/\r?\n/);
			let coordinateLines = lines.slice(1);

			let images360 = new Images360(viewer);

			for(let line of coordinateLines){

				if(line.trim().length === 0){
					continue;
				}

				let tokens = line.split(/\t/);

				let [filename, time, long, lat, alt, course, pitch, roll] = tokens;
				time = parseFloat(time);
				long = parseFloat(long);
				lat = parseFloat(lat);
				alt = parseFloat(alt);
				course = parseFloat(course);
				pitch = parseFloat(pitch);
				roll = parseFloat(roll);

				filename = filename.replace(/"/g, "");
				let file = `${url}/${filename}`;

				let image360 = new Image360(file, time, long, lat, alt, course, pitch, roll);

				let xy = params.transform.forward([long, lat]);
				let position = [...xy, alt];
				image360.position = position;

				images360.images.push(image360);
			}

			Images360Loader.createSceneNodes(images360, params.transform);

			return images360;

		}

		static createSceneNodes(images360, transform){

			for(let image360 of images360.images){
				let {longitude, latitude, altitude} = image360;
				let xy = transform.forward([longitude, latitude]);

				let mesh = new Mesh(sg, sm);
				mesh.position.set(...xy, altitude);
				mesh.scale.set(1, 1, 1);
				mesh.material.transparent = true;
				mesh.material.opacity = 0.75;
				mesh.image360 = image360;

				{ // orientation
					var {course, pitch, roll} = image360;
					mesh.rotation.set(
						MathUtils.degToRad(+roll + 90),
						MathUtils.degToRad(-pitch),
						MathUtils.degToRad(-course + 90),
						"ZYX"
					);
				}

				images360.node.add(mesh);

				image360.mesh = mesh;
			}
		}

		

	};

	// This is a generated file. Do not edit.
	var Space_Separator = /[\u1680\u2000-\u200A\u202F\u205F\u3000]/;
	var ID_Start = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]/;
	var ID_Continue = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/;

	var unicode = {
		Space_Separator: Space_Separator,
		ID_Start: ID_Start,
		ID_Continue: ID_Continue
	};

	var util = {
	    isSpaceSeparator (c) {
	        return typeof c === 'string' && unicode.Space_Separator.test(c)
	    },

	    isIdStartChar (c) {
	        return typeof c === 'string' && (
	            (c >= 'a' && c <= 'z') ||
	        (c >= 'A' && c <= 'Z') ||
	        (c === '$') || (c === '_') ||
	        unicode.ID_Start.test(c)
	        )
	    },

	    isIdContinueChar (c) {
	        return typeof c === 'string' && (
	            (c >= 'a' && c <= 'z') ||
	        (c >= 'A' && c <= 'Z') ||
	        (c >= '0' && c <= '9') ||
	        (c === '$') || (c === '_') ||
	        (c === '\u200C') || (c === '\u200D') ||
	        unicode.ID_Continue.test(c)
	        )
	    },

	    isDigit (c) {
	        return typeof c === 'string' && /[0-9]/.test(c)
	    },

	    isHexDigit (c) {
	        return typeof c === 'string' && /[0-9A-Fa-f]/.test(c)
	    },
	};

	let source;
	let parseState;
	let stack;
	let pos;
	let line;
	let column;
	let token;
	let key$1;
	let root;

	var parse = function parse (text, reviver) {
	    source = String(text);
	    parseState = 'start';
	    stack = [];
	    pos = 0;
	    line = 1;
	    column = 0;
	    token = undefined;
	    key$1 = undefined;
	    root = undefined;

	    do {
	        token = lex();

	        // This code is unreachable.
	        // if (!parseStates[parseState]) {
	        //     throw invalidParseState()
	        // }

	        parseStates[parseState]();
	    } while (token.type !== 'eof')

	    if (typeof reviver === 'function') {
	        return internalize({'': root}, '', reviver)
	    }

	    return root
	};

	function internalize (holder, name, reviver) {
	    const value = holder[name];
	    if (value != null && typeof value === 'object') {
	        for (const key in value) {
	            const replacement = internalize(value, key, reviver);
	            if (replacement === undefined) {
	                delete value[key];
	            } else {
	                value[key] = replacement;
	            }
	        }
	    }

	    return reviver.call(holder, name, value)
	}

	let lexState;
	let buffer;
	let doubleQuote;
	let sign$1;
	let c;

	function lex () {
	    lexState = 'default';
	    buffer = '';
	    doubleQuote = false;
	    sign$1 = 1;

	    for (;;) {
	        c = peek();

	        // This code is unreachable.
	        // if (!lexStates[lexState]) {
	        //     throw invalidLexState(lexState)
	        // }

	        const token = lexStates[lexState]();
	        if (token) {
	            return token
	        }
	    }
	}

	function peek () {
	    if (source[pos]) {
	        return String.fromCodePoint(source.codePointAt(pos))
	    }
	}

	function read () {
	    const c = peek();

	    if (c === '\n') {
	        line++;
	        column = 0;
	    } else if (c) {
	        column += c.length;
	    } else {
	        column++;
	    }

	    if (c) {
	        pos += c.length;
	    }

	    return c
	}

	const lexStates = {
	    default () {
	        switch (c) {
	        case '\t':
	        case '\v':
	        case '\f':
	        case ' ':
	        case '\u00A0':
	        case '\uFEFF':
	        case '\n':
	        case '\r':
	        case '\u2028':
	        case '\u2029':
	            read();
	            return

	        case '/':
	            read();
	            lexState = 'comment';
	            return

	        case undefined:
	            read();
	            return newToken('eof')
	        }

	        if (util.isSpaceSeparator(c)) {
	            read();
	            return
	        }

	        // This code is unreachable.
	        // if (!lexStates[parseState]) {
	        //     throw invalidLexState(parseState)
	        // }

	        return lexStates[parseState]()
	    },

	    comment () {
	        switch (c) {
	        case '*':
	            read();
	            lexState = 'multiLineComment';
	            return

	        case '/':
	            read();
	            lexState = 'singleLineComment';
	            return
	        }

	        throw invalidChar(read())
	    },

	    multiLineComment () {
	        switch (c) {
	        case '*':
	            read();
	            lexState = 'multiLineCommentAsterisk';
	            return

	        case undefined:
	            throw invalidChar(read())
	        }

	        read();
	    },

	    multiLineCommentAsterisk () {
	        switch (c) {
	        case '*':
	            read();
	            return

	        case '/':
	            read();
	            lexState = 'default';
	            return

	        case undefined:
	            throw invalidChar(read())
	        }

	        read();
	        lexState = 'multiLineComment';
	    },

	    singleLineComment () {
	        switch (c) {
	        case '\n':
	        case '\r':
	        case '\u2028':
	        case '\u2029':
	            read();
	            lexState = 'default';
	            return

	        case undefined:
	            read();
	            return newToken('eof')
	        }

	        read();
	    },

	    value () {
	        switch (c) {
	        case '{':
	        case '[':
	            return newToken('punctuator', read())

	        case 'n':
	            read();
	            literal('ull');
	            return newToken('null', null)

	        case 't':
	            read();
	            literal('rue');
	            return newToken('boolean', true)

	        case 'f':
	            read();
	            literal('alse');
	            return newToken('boolean', false)

	        case '-':
	        case '+':
	            if (read() === '-') {
	                sign$1 = -1;
	            }

	            lexState = 'sign';
	            return

	        case '.':
	            buffer = read();
	            lexState = 'decimalPointLeading';
	            return

	        case '0':
	            buffer = read();
	            lexState = 'zero';
	            return

	        case '1':
	        case '2':
	        case '3':
	        case '4':
	        case '5':
	        case '6':
	        case '7':
	        case '8':
	        case '9':
	            buffer = read();
	            lexState = 'decimalInteger';
	            return

	        case 'I':
	            read();
	            literal('nfinity');
	            return newToken('numeric', Infinity)

	        case 'N':
	            read();
	            literal('aN');
	            return newToken('numeric', NaN)

	        case '"':
	        case "'":
	            doubleQuote = (read() === '"');
	            buffer = '';
	            lexState = 'string';
	            return
	        }

	        throw invalidChar(read())
	    },

	    identifierNameStartEscape () {
	        if (c !== 'u') {
	            throw invalidChar(read())
	        }

	        read();
	        const u = unicodeEscape();
	        switch (u) {
	        case '$':
	        case '_':
	            break

	        default:
	            if (!util.isIdStartChar(u)) {
	                throw invalidIdentifier()
	            }

	            break
	        }

	        buffer += u;
	        lexState = 'identifierName';
	    },

	    identifierName () {
	        switch (c) {
	        case '$':
	        case '_':
	        case '\u200C':
	        case '\u200D':
	            buffer += read();
	            return

	        case '\\':
	            read();
	            lexState = 'identifierNameEscape';
	            return
	        }

	        if (util.isIdContinueChar(c)) {
	            buffer += read();
	            return
	        }

	        return newToken('identifier', buffer)
	    },

	    identifierNameEscape () {
	        if (c !== 'u') {
	            throw invalidChar(read())
	        }

	        read();
	        const u = unicodeEscape();
	        switch (u) {
	        case '$':
	        case '_':
	        case '\u200C':
	        case '\u200D':
	            break

	        default:
	            if (!util.isIdContinueChar(u)) {
	                throw invalidIdentifier()
	            }

	            break
	        }

	        buffer += u;
	        lexState = 'identifierName';
	    },

	    sign () {
	        switch (c) {
	        case '.':
	            buffer = read();
	            lexState = 'decimalPointLeading';
	            return

	        case '0':
	            buffer = read();
	            lexState = 'zero';
	            return

	        case '1':
	        case '2':
	        case '3':
	        case '4':
	        case '5':
	        case '6':
	        case '7':
	        case '8':
	        case '9':
	            buffer = read();
	            lexState = 'decimalInteger';
	            return

	        case 'I':
	            read();
	            literal('nfinity');
	            return newToken('numeric', sign$1 * Infinity)

	        case 'N':
	            read();
	            literal('aN');
	            return newToken('numeric', NaN)
	        }

	        throw invalidChar(read())
	    },

	    zero () {
	        switch (c) {
	        case '.':
	            buffer += read();
	            lexState = 'decimalPoint';
	            return

	        case 'e':
	        case 'E':
	            buffer += read();
	            lexState = 'decimalExponent';
	            return

	        case 'x':
	        case 'X':
	            buffer += read();
	            lexState = 'hexadecimal';
	            return
	        }

	        return newToken('numeric', sign$1 * 0)
	    },

	    decimalInteger () {
	        switch (c) {
	        case '.':
	            buffer += read();
	            lexState = 'decimalPoint';
	            return

	        case 'e':
	        case 'E':
	            buffer += read();
	            lexState = 'decimalExponent';
	            return
	        }

	        if (util.isDigit(c)) {
	            buffer += read();
	            return
	        }

	        return newToken('numeric', sign$1 * Number(buffer))
	    },

	    decimalPointLeading () {
	        if (util.isDigit(c)) {
	            buffer += read();
	            lexState = 'decimalFraction';
	            return
	        }

	        throw invalidChar(read())
	    },

	    decimalPoint () {
	        switch (c) {
	        case 'e':
	        case 'E':
	            buffer += read();
	            lexState = 'decimalExponent';
	            return
	        }

	        if (util.isDigit(c)) {
	            buffer += read();
	            lexState = 'decimalFraction';
	            return
	        }

	        return newToken('numeric', sign$1 * Number(buffer))
	    },

	    decimalFraction () {
	        switch (c) {
	        case 'e':
	        case 'E':
	            buffer += read();
	            lexState = 'decimalExponent';
	            return
	        }

	        if (util.isDigit(c)) {
	            buffer += read();
	            return
	        }

	        return newToken('numeric', sign$1 * Number(buffer))
	    },

	    decimalExponent () {
	        switch (c) {
	        case '+':
	        case '-':
	            buffer += read();
	            lexState = 'decimalExponentSign';
	            return
	        }

	        if (util.isDigit(c)) {
	            buffer += read();
	            lexState = 'decimalExponentInteger';
	            return
	        }

	        throw invalidChar(read())
	    },

	    decimalExponentSign () {
	        if (util.isDigit(c)) {
	            buffer += read();
	            lexState = 'decimalExponentInteger';
	            return
	        }

	        throw invalidChar(read())
	    },

	    decimalExponentInteger () {
	        if (util.isDigit(c)) {
	            buffer += read();
	            return
	        }

	        return newToken('numeric', sign$1 * Number(buffer))
	    },

	    hexadecimal () {
	        if (util.isHexDigit(c)) {
	            buffer += read();
	            lexState = 'hexadecimalInteger';
	            return
	        }

	        throw invalidChar(read())
	    },

	    hexadecimalInteger () {
	        if (util.isHexDigit(c)) {
	            buffer += read();
	            return
	        }

	        return newToken('numeric', sign$1 * Number(buffer))
	    },

	    string () {
	        switch (c) {
	        case '\\':
	            read();
	            buffer += escape$1();
	            return

	        case '"':
	            if (doubleQuote) {
	                read();
	                return newToken('string', buffer)
	            }

	            buffer += read();
	            return

	        case "'":
	            if (!doubleQuote) {
	                read();
	                return newToken('string', buffer)
	            }

	            buffer += read();
	            return

	        case '\n':
	        case '\r':
	            throw invalidChar(read())

	        case '\u2028':
	        case '\u2029':
	            separatorChar(c);
	            break

	        case undefined:
	            throw invalidChar(read())
	        }

	        buffer += read();
	    },

	    start () {
	        switch (c) {
	        case '{':
	        case '[':
	            return newToken('punctuator', read())

	        // This code is unreachable since the default lexState handles eof.
	        // case undefined:
	        //     return newToken('eof')
	        }

	        lexState = 'value';
	    },

	    beforePropertyName () {
	        switch (c) {
	        case '$':
	        case '_':
	            buffer = read();
	            lexState = 'identifierName';
	            return

	        case '\\':
	            read();
	            lexState = 'identifierNameStartEscape';
	            return

	        case '}':
	            return newToken('punctuator', read())

	        case '"':
	        case "'":
	            doubleQuote = (read() === '"');
	            lexState = 'string';
	            return
	        }

	        if (util.isIdStartChar(c)) {
	            buffer += read();
	            lexState = 'identifierName';
	            return
	        }

	        throw invalidChar(read())
	    },

	    afterPropertyName () {
	        if (c === ':') {
	            return newToken('punctuator', read())
	        }

	        throw invalidChar(read())
	    },

	    beforePropertyValue () {
	        lexState = 'value';
	    },

	    afterPropertyValue () {
	        switch (c) {
	        case ',':
	        case '}':
	            return newToken('punctuator', read())
	        }

	        throw invalidChar(read())
	    },

	    beforeArrayValue () {
	        if (c === ']') {
	            return newToken('punctuator', read())
	        }

	        lexState = 'value';
	    },

	    afterArrayValue () {
	        switch (c) {
	        case ',':
	        case ']':
	            return newToken('punctuator', read())
	        }

	        throw invalidChar(read())
	    },

	    end () {
	        // This code is unreachable since it's handled by the default lexState.
	        // if (c === undefined) {
	        //     read()
	        //     return newToken('eof')
	        // }

	        throw invalidChar(read())
	    },
	};

	function newToken (type, value) {
	    return {
	        type,
	        value,
	        line,
	        column,
	    }
	}

	function literal (s) {
	    for (const c of s) {
	        const p = peek();

	        if (p !== c) {
	            throw invalidChar(read())
	        }

	        read();
	    }
	}

	function escape$1 () {
	    const c = peek();
	    switch (c) {
	    case 'b':
	        read();
	        return '\b'

	    case 'f':
	        read();
	        return '\f'

	    case 'n':
	        read();
	        return '\n'

	    case 'r':
	        read();
	        return '\r'

	    case 't':
	        read();
	        return '\t'

	    case 'v':
	        read();
	        return '\v'

	    case '0':
	        read();
	        if (util.isDigit(peek())) {
	            throw invalidChar(read())
	        }

	        return '\0'

	    case 'x':
	        read();
	        return hexEscape()

	    case 'u':
	        read();
	        return unicodeEscape()

	    case '\n':
	    case '\u2028':
	    case '\u2029':
	        read();
	        return ''

	    case '\r':
	        read();
	        if (peek() === '\n') {
	            read();
	        }

	        return ''

	    case '1':
	    case '2':
	    case '3':
	    case '4':
	    case '5':
	    case '6':
	    case '7':
	    case '8':
	    case '9':
	        throw invalidChar(read())

	    case undefined:
	        throw invalidChar(read())
	    }

	    return read()
	}

	function hexEscape () {
	    let buffer = '';
	    let c = peek();

	    if (!util.isHexDigit(c)) {
	        throw invalidChar(read())
	    }

	    buffer += read();

	    c = peek();
	    if (!util.isHexDigit(c)) {
	        throw invalidChar(read())
	    }

	    buffer += read();

	    return String.fromCodePoint(parseInt(buffer, 16))
	}

	function unicodeEscape () {
	    let buffer = '';
	    let count = 4;

	    while (count-- > 0) {
	        const c = peek();
	        if (!util.isHexDigit(c)) {
	            throw invalidChar(read())
	        }

	        buffer += read();
	    }

	    return String.fromCodePoint(parseInt(buffer, 16))
	}

	const parseStates = {
	    start () {
	        if (token.type === 'eof') {
	            throw invalidEOF()
	        }

	        push();
	    },

	    beforePropertyName () {
	        switch (token.type) {
	        case 'identifier':
	        case 'string':
	            key$1 = token.value;
	            parseState = 'afterPropertyName';
	            return

	        case 'punctuator':
	            // This code is unreachable since it's handled by the lexState.
	            // if (token.value !== '}') {
	            //     throw invalidToken()
	            // }

	            pop();
	            return

	        case 'eof':
	            throw invalidEOF()
	        }

	        // This code is unreachable since it's handled by the lexState.
	        // throw invalidToken()
	    },

	    afterPropertyName () {
	        // This code is unreachable since it's handled by the lexState.
	        // if (token.type !== 'punctuator' || token.value !== ':') {
	        //     throw invalidToken()
	        // }

	        if (token.type === 'eof') {
	            throw invalidEOF()
	        }

	        parseState = 'beforePropertyValue';
	    },

	    beforePropertyValue () {
	        if (token.type === 'eof') {
	            throw invalidEOF()
	        }

	        push();
	    },

	    beforeArrayValue () {
	        if (token.type === 'eof') {
	            throw invalidEOF()
	        }

	        if (token.type === 'punctuator' && token.value === ']') {
	            pop();
	            return
	        }

	        push();
	    },

	    afterPropertyValue () {
	        // This code is unreachable since it's handled by the lexState.
	        // if (token.type !== 'punctuator') {
	        //     throw invalidToken()
	        // }

	        if (token.type === 'eof') {
	            throw invalidEOF()
	        }

	        switch (token.value) {
	        case ',':
	            parseState = 'beforePropertyName';
	            return

	        case '}':
	            pop();
	        }

	        // This code is unreachable since it's handled by the lexState.
	        // throw invalidToken()
	    },

	    afterArrayValue () {
	        // This code is unreachable since it's handled by the lexState.
	        // if (token.type !== 'punctuator') {
	        //     throw invalidToken()
	        // }

	        if (token.type === 'eof') {
	            throw invalidEOF()
	        }

	        switch (token.value) {
	        case ',':
	            parseState = 'beforeArrayValue';
	            return

	        case ']':
	            pop();
	        }

	        // This code is unreachable since it's handled by the lexState.
	        // throw invalidToken()
	    },

	    end () {
	        // This code is unreachable since it's handled by the lexState.
	        // if (token.type !== 'eof') {
	        //     throw invalidToken()
	        // }
	    },
	};

	function push () {
	    let value;

	    switch (token.type) {
	    case 'punctuator':
	        switch (token.value) {
	        case '{':
	            value = {};
	            break

	        case '[':
	            value = [];
	            break
	        }

	        break

	    case 'null':
	    case 'boolean':
	    case 'numeric':
	    case 'string':
	        value = token.value;
	        break

	    // This code is unreachable.
	    // default:
	    //     throw invalidToken()
	    }

	    if (root === undefined) {
	        root = value;
	    } else {
	        const parent = stack[stack.length - 1];
	        if (Array.isArray(parent)) {
	            parent.push(value);
	        } else {
	            parent[key$1] = value;
	        }
	    }

	    if (value !== null && typeof value === 'object') {
	        stack.push(value);

	        if (Array.isArray(value)) {
	            parseState = 'beforeArrayValue';
	        } else {
	            parseState = 'beforePropertyName';
	        }
	    } else {
	        const current = stack[stack.length - 1];
	        if (current == null) {
	            parseState = 'end';
	        } else if (Array.isArray(current)) {
	            parseState = 'afterArrayValue';
	        } else {
	            parseState = 'afterPropertyValue';
	        }
	    }
	}

	function pop () {
	    stack.pop();

	    const current = stack[stack.length - 1];
	    if (current == null) {
	        parseState = 'end';
	    } else if (Array.isArray(current)) {
	        parseState = 'afterArrayValue';
	    } else {
	        parseState = 'afterPropertyValue';
	    }
	}

	// This code is unreachable.
	// function invalidParseState () {
	//     return new Error(`JSON5: invalid parse state '${parseState}'`)
	// }

	// This code is unreachable.
	// function invalidLexState (state) {
	//     return new Error(`JSON5: invalid lex state '${state}'`)
	// }

	function invalidChar (c) {
	    if (c === undefined) {
	        return syntaxError(`JSON5: invalid end of input at ${line}:${column}`)
	    }

	    return syntaxError(`JSON5: invalid character '${formatChar(c)}' at ${line}:${column}`)
	}

	function invalidEOF () {
	    return syntaxError(`JSON5: invalid end of input at ${line}:${column}`)
	}

	// This code is unreachable.
	// function invalidToken () {
	//     if (token.type === 'eof') {
	//         return syntaxError(`JSON5: invalid end of input at ${line}:${column}`)
	//     }

	//     const c = String.fromCodePoint(token.value.codePointAt(0))
	//     return syntaxError(`JSON5: invalid character '${formatChar(c)}' at ${line}:${column}`)
	// }

	function invalidIdentifier () {
	    column -= 5;
	    return syntaxError(`JSON5: invalid identifier character at ${line}:${column}`)
	}

	function separatorChar (c) {
	    console.warn(`JSON5: '${formatChar(c)}' in strings is not valid ECMAScript; consider escaping`);
	}

	function formatChar (c) {
	    const replacements = {
	        "'": "\\'",
	        '"': '\\"',
	        '\\': '\\\\',
	        '\b': '\\b',
	        '\f': '\\f',
	        '\n': '\\n',
	        '\r': '\\r',
	        '\t': '\\t',
	        '\v': '\\v',
	        '\0': '\\0',
	        '\u2028': '\\u2028',
	        '\u2029': '\\u2029',
	    };

	    if (replacements[c]) {
	        return replacements[c]
	    }

	    if (c < ' ') {
	        const hexString = c.charCodeAt(0).toString(16);
	        return '\\x' + ('00' + hexString).substring(hexString.length)
	    }

	    return c
	}

	function syntaxError (message) {
	    const err = new SyntaxError(message);
	    err.lineNumber = line;
	    err.columnNumber = column;
	    return err
	}

	var stringify = function stringify (value, replacer, space) {
	    const stack = [];
	    let indent = '';
	    let propertyList;
	    let replacerFunc;
	    let gap = '';
	    let quote;

	    if (
	        replacer != null &&
	        typeof replacer === 'object' &&
	        !Array.isArray(replacer)
	    ) {
	        space = replacer.space;
	        quote = replacer.quote;
	        replacer = replacer.replacer;
	    }

	    if (typeof replacer === 'function') {
	        replacerFunc = replacer;
	    } else if (Array.isArray(replacer)) {
	        propertyList = [];
	        for (const v of replacer) {
	            let item;

	            if (typeof v === 'string') {
	                item = v;
	            } else if (
	                typeof v === 'number' ||
	                v instanceof String ||
	                v instanceof Number
	            ) {
	                item = String(v);
	            }

	            if (item !== undefined && propertyList.indexOf(item) < 0) {
	                propertyList.push(item);
	            }
	        }
	    }

	    if (space instanceof Number) {
	        space = Number(space);
	    } else if (space instanceof String) {
	        space = String(space);
	    }

	    if (typeof space === 'number') {
	        if (space > 0) {
	            space = Math.min(10, Math.floor(space));
	            gap = '          '.substr(0, space);
	        }
	    } else if (typeof space === 'string') {
	        gap = space.substr(0, 10);
	    }

	    return serializeProperty('', {'': value})

	    function serializeProperty (key, holder) {
	        let value = holder[key];
	        if (value != null) {
	            if (typeof value.toJSON5 === 'function') {
	                value = value.toJSON5(key);
	            } else if (typeof value.toJSON === 'function') {
	                value = value.toJSON(key);
	            }
	        }

	        if (replacerFunc) {
	            value = replacerFunc.call(holder, key, value);
	        }

	        if (value instanceof Number) {
	            value = Number(value);
	        } else if (value instanceof String) {
	            value = String(value);
	        } else if (value instanceof Boolean) {
	            value = value.valueOf();
	        }

	        switch (value) {
	        case null: return 'null'
	        case true: return 'true'
	        case false: return 'false'
	        }

	        if (typeof value === 'string') {
	            return quoteString(value, false)
	        }

	        if (typeof value === 'number') {
	            return String(value)
	        }

	        if (typeof value === 'object') {
	            return Array.isArray(value) ? serializeArray(value) : serializeObject(value)
	        }

	        return undefined
	    }

	    function quoteString (value) {
	        const quotes = {
	            "'": 0.1,
	            '"': 0.2,
	        };

	        const replacements = {
	            "'": "\\'",
	            '"': '\\"',
	            '\\': '\\\\',
	            '\b': '\\b',
	            '\f': '\\f',
	            '\n': '\\n',
	            '\r': '\\r',
	            '\t': '\\t',
	            '\v': '\\v',
	            '\0': '\\0',
	            '\u2028': '\\u2028',
	            '\u2029': '\\u2029',
	        };

	        let product = '';

	        for (let i = 0; i < value.length; i++) {
	            const c = value[i];
	            switch (c) {
	            case "'":
	            case '"':
	                quotes[c]++;
	                product += c;
	                continue

	            case '\0':
	                if (util.isDigit(value[i + 1])) {
	                    product += '\\x00';
	                    continue
	                }
	            }

	            if (replacements[c]) {
	                product += replacements[c];
	                continue
	            }

	            if (c < ' ') {
	                let hexString = c.charCodeAt(0).toString(16);
	                product += '\\x' + ('00' + hexString).substring(hexString.length);
	                continue
	            }

	            product += c;
	        }

	        const quoteChar = quote || Object.keys(quotes).reduce((a, b) => (quotes[a] < quotes[b]) ? a : b);

	        product = product.replace(new RegExp(quoteChar, 'g'), replacements[quoteChar]);

	        return quoteChar + product + quoteChar
	    }

	    function serializeObject (value) {
	        if (stack.indexOf(value) >= 0) {
	            throw TypeError('Converting circular structure to JSON5')
	        }

	        stack.push(value);

	        let stepback = indent;
	        indent = indent + gap;

	        let keys = propertyList || Object.keys(value);
	        let partial = [];
	        for (const key of keys) {
	            const propertyString = serializeProperty(key, value);
	            if (propertyString !== undefined) {
	                let member = serializeKey(key) + ':';
	                if (gap !== '') {
	                    member += ' ';
	                }
	                member += propertyString;
	                partial.push(member);
	            }
	        }

	        let final;
	        if (partial.length === 0) {
	            final = '{}';
	        } else {
	            let properties;
	            if (gap === '') {
	                properties = partial.join(',');
	                final = '{' + properties + '}';
	            } else {
	                let separator = ',\n' + indent;
	                properties = partial.join(separator);
	                final = '{\n' + indent + properties + ',\n' + stepback + '}';
	            }
	        }

	        stack.pop();
	        indent = stepback;
	        return final
	    }

	    function serializeKey (key) {
	        if (key.length === 0) {
	            return quoteString(key, true)
	        }

	        const firstChar = String.fromCodePoint(key.codePointAt(0));
	        if (!util.isIdStartChar(firstChar)) {
	            return quoteString(key, true)
	        }

	        for (let i = firstChar.length; i < key.length; i++) {
	            if (!util.isIdContinueChar(String.fromCodePoint(key.codePointAt(i)))) {
	                return quoteString(key, true)
	            }
	        }

	        return key
	    }

	    function serializeArray (value) {
	        if (stack.indexOf(value) >= 0) {
	            throw TypeError('Converting circular structure to JSON5')
	        }

	        stack.push(value);

	        let stepback = indent;
	        indent = indent + gap;

	        let partial = [];
	        for (let i = 0; i < value.length; i++) {
	            const propertyString = serializeProperty(String(i), value);
	            partial.push((propertyString !== undefined) ? propertyString : 'null');
	        }

	        let final;
	        if (partial.length === 0) {
	            final = '[]';
	        } else {
	            if (gap === '') {
	                let properties = partial.join(',');
	                final = '[' + properties + ']';
	            } else {
	                let separator = ',\n' + indent;
	                let properties = partial.join(separator);
	                final = '[\n' + indent + properties + ',\n' + stepback + ']';
	            }
	        }

	        stack.pop();
	        indent = stepback;
	        return final
	    }
	};

	const JSON5 = {
	    parse,
	    stringify,
	};

	var lib = JSON5;

	class Sidebar{

		constructor(viewer){
			this.viewer = viewer;

			this.measuringTool = viewer.measuringTool;
			this.profileTool = viewer.profileTool;
			this.volumeTool = viewer.volumeTool;

			this.dom = $("#sidebar_root");
		}

		createToolIcon(icon, title, callback){
			let element = $(`
			<img src="${icon}"
				style="width: 32px; height: 32px"
				class="button-icon"
				data-i18n="${title}" />
		`);

			element.click(callback);

			return element;
		}

		init(){

			this.initAccordion();
			this.initAppearance();
			this.initToolbar();
			this.initScene();
			this.initNavigation();
			this.initFilters();
			this.initClippingTool();
			this.initSettings();
			
			$('#potree_version_number').html(Potree.version.major + "." + Potree.version.minor + Potree.version.suffix);
		}

			

		initToolbar(){

			// ANGLE
			let elToolbar = $('#tools');
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/angle.png',
				'[title]tt.angle_measurement',
				() => {
					$('#menu_measurements').next().slideDown();
					let measurement = this.measuringTool.startInsertion({
						showDistances: false,
						showAngles: true,
						showArea: false,
						closed: true,
						maxMarkers: 3,
						name: 'Angle'});

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === measurement.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// POINT
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/point.svg',
				'[title]tt.point_measurement',
				() => {
					$('#menu_measurements').next().slideDown();
					let measurement = this.measuringTool.startInsertion({
						showDistances: false,
						showAngles: false,
						showCoordinates: true,
						showArea: false,
						closed: true,
						maxMarkers: 1,
						name: 'Point'});

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === measurement.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// DISTANCE
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/distance.svg',
				'[title]tt.distance_measurement',
				() => {
					$('#menu_measurements').next().slideDown();
					let measurement = this.measuringTool.startInsertion({
						showDistances: true,
						showArea: false,
						closed: false,
						name: 'Distance'});

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === measurement.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// HEIGHT
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/height.svg',
				'[title]tt.height_measurement',
				() => {
					$('#menu_measurements').next().slideDown();
					let measurement = this.measuringTool.startInsertion({
						showDistances: false,
						showHeight: true,
						showArea: false,
						closed: false,
						maxMarkers: 2,
						name: 'Height'});

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === measurement.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// CIRCLE
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/circle.svg',
				'[title]tt.circle_measurement',
				() => {
					$('#menu_measurements').next().slideDown();
					let measurement = this.measuringTool.startInsertion({
						showDistances: false,
						showHeight: false,
						showArea: false,
						showCircle: true,
						showEdges: false,
						closed: false,
						maxMarkers: 3,
						name: 'Circle'});

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === measurement.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// AZIMUTH
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/azimuth.svg',
				'Azimuth',
				() => {
					$('#menu_measurements').next().slideDown();
					let measurement = this.measuringTool.startInsertion({
						showDistances: false,
						showHeight: false,
						showArea: false,
						showCircle: false,
						showEdges: false,
						showAzimuth: true,
						closed: false,
						maxMarkers: 2,
						name: 'Azimuth'});

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === measurement.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// AREA
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/area.svg',
				'[title]tt.area_measurement',
				() => {
					$('#menu_measurements').next().slideDown();
					let measurement = this.measuringTool.startInsertion({
						showDistances: true,
						showArea: true,
						closed: true,
						name: 'Area'});

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === measurement.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// VOLUME
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/volume.svg',
				'[title]tt.volume_measurement',
				() => {
					let volume = this.volumeTool.startInsertion(); 

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === volume.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// SPHERE VOLUME
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/sphere_distances.svg',
				'[title]tt.volume_measurement',
				() => { 
					let volume = this.volumeTool.startInsertion({type: SphereVolume}); 

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === volume.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// PROFILE
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/profile.svg',
				'[title]tt.height_profile',
				() => {
					$('#menu_measurements').next().slideDown(); ;
					let profile = this.profileTool.startInsertion();

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === profile.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// ANNOTATION
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/annotation.svg',
				'[title]tt.annotation',
				() => {
					$('#menu_measurements').next().slideDown(); ;
					let annotation = this.viewer.annotationTool.startInsertion();

					let annotationsRoot = $("#jstree_scene").jstree().get_json("annotations");
					let jsonNode = annotationsRoot.children.find(child => child.data.uuid === annotation.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// REMOVE ALL
			elToolbar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/reset_tools.svg',
				'[title]tt.remove_all_measurement',
				() => {
					this.viewer.scene.removeAllMeasurements();
				}
			));


			{ // SHOW / HIDE Measurements
				let elShow = $("#measurement_options_show");
				elShow.selectgroup({title: "Show/Hide labels"});

				elShow.find("input").click( (e) => {
					const show = e.target.value === "SHOW";
					this.measuringTool.showLabels = show;
				});

				let currentShow = this.measuringTool.showLabels ? "SHOW" : "HIDE";
				elShow.find(`input[value=${currentShow}]`).trigger("click");
			}
		}

		initScene(){

			let elScene = $("#menu_scene");
			let elObjects = elScene.next().find("#scene_objects");
			let elProperties = elScene.next().find("#scene_object_properties");
			

			{
				let elExport = elScene.next().find("#scene_export");

				let geoJSONIcon = `${Potree.resourcePath}/icons/file_geojson.svg`;
				let dxfIcon = `${Potree.resourcePath}/icons/file_dxf.svg`;
				let potreeIcon = `${Potree.resourcePath}/icons/file_potree.svg`;

				elExport.append(`
				Export: <br>
				<a href="#" download="measure.json"><img name="geojson_export_button" src="${geoJSONIcon}" class="button-icon" style="height: 24px" /></a>
				<a href="#" download="measure.dxf"><img name="dxf_export_button" src="${dxfIcon}" class="button-icon" style="height: 24px" /></a>
				<a href="#" download="potree.json5"><img name="potree_export_button" src="${potreeIcon}" class="button-icon" style="height: 24px" /></a>
			`);

				let elDownloadJSON = elExport.find("img[name=geojson_export_button]").parent();
				elDownloadJSON.click( (event) => {
					let scene = this.viewer.scene;
					let measurements = [...scene.measurements, ...scene.profiles, ...scene.volumes];

					if(measurements.length > 0){
						let geoJson = GeoJSONExporter.toString(measurements);

						let url = window.URL.createObjectURL(new Blob([geoJson], {type: 'data:application/octet-stream'}));
						elDownloadJSON.attr('href', url);
					}else {
						this.viewer.postError("no measurements to export");
						event.preventDefault();
					}
				});

				let elDownloadDXF = elExport.find("img[name=dxf_export_button]").parent();
				elDownloadDXF.click( (event) => {
					let scene = this.viewer.scene;
					let measurements = [...scene.measurements, ...scene.profiles, ...scene.volumes];

					if(measurements.length > 0){
						let dxf = DXFExporter.toString(measurements);

						let url = window.URL.createObjectURL(new Blob([dxf], {type: 'data:application/octet-stream'}));
						elDownloadDXF.attr('href', url);
					}else {
						this.viewer.postError("no measurements to export");
						event.preventDefault();
					}
				});

				let elDownloadPotree = elExport.find("img[name=potree_export_button]").parent();
				elDownloadPotree.click( (event) => {

					let data = Potree.saveProject(this.viewer);
					let dataString = lib.stringify(data, null, "\t");

					let url = window.URL.createObjectURL(new Blob([dataString], {type: 'data:application/octet-stream'}));
					elDownloadPotree.attr('href', url);
				});
			}

			let propertiesPanel = new PropertiesPanel(elProperties, this.viewer);
			propertiesPanel.setScene(this.viewer.scene);
			
			localStorage.removeItem('jstree');

			let tree = $(`<div id="jstree_scene"></div>`);
			elObjects.append(tree);

			tree.jstree({
				'plugins': ["checkbox", "state"],
				'core': {
					"dblclick_toggle": false,
					"state": {
						"checked" : true
					},
					'check_callback': true,
					"expand_selected_onload": true
				},
				"checkbox" : {
					"keep_selected_style": true,
					"three_state": false,
					"whole_node": false,
					"tie_selection": false,
				},
			});

			let createNode = (parent, text, icon, object) => {
				let nodeID = tree.jstree('create_node', parent, { 
						"text": text, 
						"icon": icon,
						"data": object
					}, 
					"last", false, false);
				
				if(object.visible){
					tree.jstree('check_node', nodeID);
				}else {
					tree.jstree('uncheck_node', nodeID);
				}

				return nodeID;
			};

			let pcID = tree.jstree('create_node', "#", { "text": "<b>Point Clouds</b>", "id": "pointclouds"}, "last", false, false);
			let measurementID = tree.jstree('create_node', "#", { "text": "<b>Measurements</b>", "id": "measurements" }, "last", false, false);
			let annotationsID = tree.jstree('create_node', "#", { "text": "<b>Annotations</b>", "id": "annotations" }, "last", false, false);
			let otherID = tree.jstree('create_node', "#", { "text": "<b>Other</b>", "id": "other" }, "last", false, false);
			//let vectorsID = tree.jstree('create_node', "#", { "text": "<b>Vectors</b>", "id": "vectors" }, "last", false, false);
			//let imagesID = tree.jstree('create_node', "#", { "text": "<b> Images</b>", "id": "images" }, "last", false, false);

			tree.jstree("check_node", pcID);
			tree.jstree("check_node", measurementID);
			tree.jstree("check_node", annotationsID);
			tree.jstree("check_node", otherID);
			//tree.jstree("check_node", vectorsID);
			//tree.jstree("check_node", imagesID);

			tree.on('create_node.jstree', (e, data) => {
				tree.jstree("open_all");
			});

			tree.on("select_node.jstree", (e, data) => {
				let object = data.node.data;
				propertiesPanel.set(object);

				this.viewer.inputHandler.deselectAll();

				if(object instanceof Volume){
					this.viewer.inputHandler.toggleSelection(object);
				}

				$(this.viewer.renderer.domElement).focus();
			});

			tree.on("deselect_node.jstree", (e, data) => {
				propertiesPanel.set(null);
			});

			tree.on("delete_node.jstree", (e, data) => {
				propertiesPanel.set(null);
			});

			tree.on('dblclick','.jstree-anchor', (e) => {

				let instance = $.jstree.reference(e.target);
				let node = instance.get_node(e.target);
				let object = node.data;

				// ignore double click on checkbox
				if(e.target.classList.contains("jstree-checkbox")){
					return;
				}

				if(object instanceof PointCloudTree){
					let box = this.viewer.getBoundingBox([object]);
					let node = new Object3D();
					node.boundingBox = box;
					this.viewer.zoomTo(node, 1, 500);
				}else if(object instanceof Measure){
					let points = object.points.map(p => p.position);
					let box = new Box3().setFromPoints(points);
					if(box.getSize(new Vector3()).length() > 0){
						let node = new Object3D();
						node.boundingBox = box;
						this.viewer.zoomTo(node, 2, 500);
					}
				}else if(object instanceof Profile){
					let points = object.points;
					let box = new Box3().setFromPoints(points);
					if(box.getSize(new Vector3()).length() > 0){
						let node = new Object3D();
						node.boundingBox = box;
						this.viewer.zoomTo(node, 1, 500);
					}
				}else if(object instanceof Volume){
					
					let box = object.boundingBox.clone().applyMatrix4(object.matrixWorld);

					if(box.getSize(new Vector3()).length() > 0){
						let node = new Object3D();
						node.boundingBox = box;
						this.viewer.zoomTo(node, 1, 500);
					}
				}else if(object instanceof Annotation){
					object.moveHere(this.viewer.scene.getActiveCamera());
				}else if(object instanceof PolygonClipVolume){
					let dir = object.camera.getWorldDirection(new Vector3());
					let target;

					if(object.camera instanceof OrthographicCamera){
						dir.multiplyScalar(object.camera.right);
						target = new Vector3().addVectors(object.camera.position, dir);
						this.viewer.setCameraMode(CameraMode.ORTHOGRAPHIC);
					}else if(object.camera instanceof PerspectiveCamera){
						dir.multiplyScalar(this.viewer.scene.view.radius);
						target = new Vector3().addVectors(object.camera.position, dir);
						this.viewer.setCameraMode(CameraMode.PERSPECTIVE);
					}
					
					this.viewer.scene.view.position.copy(object.camera.position);
					this.viewer.scene.view.lookAt(target);
				}else if(object.type === "SpotLight"){
					let distance = (object.distance > 0) ? object.distance / 4 : 5 * 1000;
					let position = object.position;
					let target = new Vector3().addVectors(
						position, 
						object.getWorldDirection(new Vector3()).multiplyScalar(distance));

					this.viewer.scene.view.position.copy(object.position);
					this.viewer.scene.view.lookAt(target);
				}else if(object instanceof Object3D){
					let box = new Box3().setFromObject(object);

					if(box.getSize(new Vector3()).length() > 0){
						let node = new Object3D();
						node.boundingBox = box;
						this.viewer.zoomTo(node, 1, 500);
					}
				}else if(object instanceof OrientedImage){
					// TODO zoom to images

					// let box = new THREE.Box3().setFromObject(object);

					// if(box.getSize(new THREE.Vector3()).length() > 0){
					// 	let node = new THREE.Object3D();
					// 	node.boundingBox = box;
					// 	this.viewer.zoomTo(node, 1, 500);
					// }
				}else if(object instanceof Images360){
					// TODO
				}else if(object instanceof Geopackage){
					// TODO
				}
			});

			tree.on("uncheck_node.jstree", (e, data) => {
				let object = data.node.data;

				if(object){
					object.visible = false;
				}
			});

			tree.on("check_node.jstree", (e, data) => {
				let object = data.node.data;

				if(object){
					object.visible = true;
				}
			});


			let onPointCloudAdded = (e) => {
				let pointcloud = e.pointcloud;
				let cloudIcon = `${Potree.resourcePath}/icons/cloud.svg`;
				let node = createNode(pcID, pointcloud.name, cloudIcon, pointcloud);

				pointcloud.addEventListener("visibility_changed", () => {
					if(pointcloud.visible){
						tree.jstree('check_node', node);
					}else {
						tree.jstree('uncheck_node', node);
					}
				});
			};

			let onMeasurementAdded = (e) => {
				let measurement = e.measurement;
				let icon = Utils.getMeasurementIcon(measurement);
				createNode(measurementID, measurement.name, icon, measurement);
			};

			let onVolumeAdded = (e) => {
				let volume = e.volume;
				let icon = Utils.getMeasurementIcon(volume);
				let node = createNode(measurementID, volume.name, icon, volume);

				volume.addEventListener("visibility_changed", () => {
					if(volume.visible){
						tree.jstree('check_node', node);
					}else {
						tree.jstree('uncheck_node', node);
					}
				});
			};

			let onProfileAdded = (e) => {
				let profile = e.profile;
				let icon = Utils.getMeasurementIcon(profile);
				createNode(measurementID, profile.name, icon, profile);
			};

			let onAnnotationAdded = (e) => {
				let annotation = e.annotation;

				let annotationIcon = `${Potree.resourcePath}/icons/annotation.svg`;
				let parentID = this.annotationMapping.get(annotation.parent);
				let annotationID = createNode(parentID, annotation.title, annotationIcon, annotation);
				this.annotationMapping.set(annotation, annotationID);

				annotation.addEventListener("annotation_changed", (e) => {
					let annotationsRoot = $("#jstree_scene").jstree().get_json("annotations");
					let jsonNode = annotationsRoot.children.find(child => child.data.uuid === annotation.uuid);
					
					$.jstree.reference(jsonNode.id).rename_node(jsonNode.id, annotation.title);
				});
			};

			let onCameraAnimationAdded = (e) => {
				const animation = e.animation;

				const animationIcon = `${Potree.resourcePath}/icons/camera_animation.svg`;
				createNode(otherID, "animation", animationIcon, animation);
			};

			let onOrientedImagesAdded = (e) => {
				const images = e.images;

				const imagesIcon = `${Potree.resourcePath}/icons/picture.svg`;
				const node = createNode(imagesID, "images", imagesIcon, images);

				images.addEventListener("visibility_changed", () => {
					if(images.visible){
						tree.jstree('check_node', node);
					}else {
						tree.jstree('uncheck_node', node);
					}
				});
			};

			let onImages360Added = (e) => {
				const images = e.images;

				const imagesIcon = `${Potree.resourcePath}/icons/picture.svg`;
				const node = createNode(imagesID, "360° images", imagesIcon, images);

				images.addEventListener("visibility_changed", () => {
					if(images.visible){
						tree.jstree('check_node', node);
					}else {
						tree.jstree('uncheck_node', node);
					}
				});
			};

			const onGeopackageAdded = (e) => {
				const geopackage = e.geopackage;

				const geopackageIcon = `${Potree.resourcePath}/icons/triangle.svg`;
				const tree = $(`#jstree_scene`);
				const parentNode = "vectors";

				for(const layer of geopackage.node.children){
					const name = layer.name;

					let shpPointsID = tree.jstree('create_node', parentNode, { 
							"text": name, 
							"icon": geopackageIcon,
							"object": layer,
							"data": layer,
						}, 
						"last", false, false);
					tree.jstree(layer.visible ? "check_node" : "uncheck_node", shpPointsID);
				}

			};

			this.viewer.scene.addEventListener("pointcloud_added", onPointCloudAdded);
			this.viewer.scene.addEventListener("measurement_added", onMeasurementAdded);
			this.viewer.scene.addEventListener("profile_added", onProfileAdded);
			this.viewer.scene.addEventListener("volume_added", onVolumeAdded);
			this.viewer.scene.addEventListener("camera_animation_added", onCameraAnimationAdded);
			this.viewer.scene.addEventListener("oriented_images_added", onOrientedImagesAdded);
			this.viewer.scene.addEventListener("360_images_added", onImages360Added);
			this.viewer.scene.addEventListener("geopackage_added", onGeopackageAdded);
			this.viewer.scene.addEventListener("polygon_clip_volume_added", onVolumeAdded);
			this.viewer.scene.annotations.addEventListener("annotation_added", onAnnotationAdded);

			let onMeasurementRemoved = (e) => {
				let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
				let jsonNode = measurementsRoot.children.find(child => child.data.uuid === e.measurement.uuid);
				
				tree.jstree("delete_node", jsonNode.id);
			};

			let onVolumeRemoved = (e) => {
				let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
				let jsonNode = measurementsRoot.children.find(child => child.data.uuid === e.volume.uuid);
				
				tree.jstree("delete_node", jsonNode.id);
			};

			let onPolygonClipVolumeRemoved = (e) => {
				let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
				let jsonNode = measurementsRoot.children.find(child => child.data.uuid === e.volume.uuid);
				
				tree.jstree("delete_node", jsonNode.id);
			};

			let onProfileRemoved = (e) => {
				let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
				let jsonNode = measurementsRoot.children.find(child => child.data.uuid === e.profile.uuid);
				
				tree.jstree("delete_node", jsonNode.id);
			};

			this.viewer.scene.addEventListener("measurement_removed", onMeasurementRemoved);
			this.viewer.scene.addEventListener("volume_removed", onVolumeRemoved);
			this.viewer.scene.addEventListener("polygon_clip_volume_removed", onPolygonClipVolumeRemoved);
			this.viewer.scene.addEventListener("profile_removed", onProfileRemoved);

			{
				let annotationIcon = `${Potree.resourcePath}/icons/annotation.svg`;
				this.annotationMapping = new Map(); 
				this.annotationMapping.set(this.viewer.scene.annotations, annotationsID);
				this.viewer.scene.annotations.traverseDescendants(annotation => {
					let parentID = this.annotationMapping.get(annotation.parent);
					let annotationID = createNode(parentID, annotation.title, annotationIcon, annotation);
					this.annotationMapping.set(annotation, annotationID);
				});
			}

			const scene = this.viewer.scene;
			for(let pointcloud of scene.pointclouds){
				onPointCloudAdded({pointcloud: pointcloud});
			}

			for(let measurement of scene.measurements){
				onMeasurementAdded({measurement: measurement});
			}

			for(let volume of [...scene.volumes, ...scene.polygonClipVolumes]){
				onVolumeAdded({volume: volume});
			}

			for(let animation of scene.cameraAnimations){
				onCameraAnimationAdded({animation: animation});
			}

			for(let images of scene.orientedImages){
				onOrientedImagesAdded({images: images});
			}

			for(let images of scene.images360){
				onImages360Added({images: images});
			}

			for(const geopackage of scene.geopackages){
				onGeopackageAdded({geopackage: geopackage});
			}

			for(let profile of scene.profiles){
				onProfileAdded({profile: profile});
			}

			{
				createNode(otherID, "Camera", null, new Camera());
			}

			this.viewer.addEventListener("scene_changed", (e) => {
				propertiesPanel.setScene(e.scene);

				e.oldScene.removeEventListener("pointcloud_added", onPointCloudAdded);
				e.oldScene.removeEventListener("measurement_added", onMeasurementAdded);
				e.oldScene.removeEventListener("profile_added", onProfileAdded);
				e.oldScene.removeEventListener("volume_added", onVolumeAdded);
				e.oldScene.removeEventListener("polygon_clip_volume_added", onVolumeAdded);
				e.oldScene.removeEventListener("measurement_removed", onMeasurementRemoved);

				e.scene.addEventListener("pointcloud_added", onPointCloudAdded);
				e.scene.addEventListener("measurement_added", onMeasurementAdded);
				e.scene.addEventListener("profile_added", onProfileAdded);
				e.scene.addEventListener("volume_added", onVolumeAdded);
				e.scene.addEventListener("polygon_clip_volume_added", onVolumeAdded);
				e.scene.addEventListener("measurement_removed", onMeasurementRemoved);
			});

		}

		initClippingTool(){


			this.viewer.addEventListener("cliptask_changed", (event) => {
				console.log("TODO");
			});

			this.viewer.addEventListener("clipmethod_changed", (event) => {
				console.log("TODO");
			});

			{
				let elClipTask = $("#cliptask_options");
				elClipTask.selectgroup({title: "Clip Task"});

				elClipTask.find("input").click( (e) => {
					this.viewer.setClipTask(ClipTask[e.target.value]);
				});

				let currentClipTask = Object.keys(ClipTask)
					.filter(key => ClipTask[key] === this.viewer.clipTask);
				elClipTask.find(`input[value=${currentClipTask}]`).trigger("click");
			}

			{
				let elClipMethod = $("#clipmethod_options");
				elClipMethod.selectgroup({title: "Clip Method"});

				elClipMethod.find("input").click( (e) => {
					this.viewer.setClipMethod(ClipMethod[e.target.value]);
				});

				let currentClipMethod = Object.keys(ClipMethod)
					.filter(key => ClipMethod[key] === this.viewer.clipMethod);
				elClipMethod.find(`input[value=${currentClipMethod}]`).trigger("click");
			}

			let clippingToolBar = $("#clipping_tools");

			// CLIP VOLUME
			clippingToolBar.append(this.createToolIcon(
				Potree.resourcePath + '/icons/clip_volume.svg',
				'[title]tt.clip_volume',
				() => {
					let item = this.volumeTool.startInsertion({clip: true}); 

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === item.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			// CLIP POLYGON
			clippingToolBar.append(this.createToolIcon(
				Potree.resourcePath + "/icons/clip-polygon.svg",
				"[title]tt.clip_polygon",
				() => {
					let item = this.viewer.clippingTool.startInsertion({type: "polygon"});

					let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
					let jsonNode = measurementsRoot.children.find(child => child.data.uuid === item.uuid);
					$.jstree.reference(jsonNode.id).deselect_all();
					$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
				}
			));

			{// SCREEN BOX SELECT
				let boxSelectTool = new ScreenBoxSelectTool(this.viewer);

				clippingToolBar.append(this.createToolIcon(
					Potree.resourcePath + "/icons/clip-screen.svg",
					"[title]tt.screen_clip_box",
					() => {
						if(!(this.viewer.scene.getActiveCamera() instanceof OrthographicCamera)){
							this.viewer.postMessage(`Switch to Orthographic Camera Mode before using the Screen-Box-Select tool.`, 
								{duration: 2000});
							return;
						}
						
						let item = boxSelectTool.startInsertion();

						let measurementsRoot = $("#jstree_scene").jstree().get_json("measurements");
						let jsonNode = measurementsRoot.children.find(child => child.data.uuid === item.uuid);
						$.jstree.reference(jsonNode.id).deselect_all();
						$.jstree.reference(jsonNode.id).select_node(jsonNode.id);
					}
				));
			}

			{ // REMOVE CLIPPING TOOLS
				clippingToolBar.append(this.createToolIcon(
					Potree.resourcePath + "/icons/remove.svg",
					"[title]tt.remove_all_measurement",
					() => {

						this.viewer.scene.removeAllClipVolumes();
					}
				));
			}

		}

		initFilters(){
			this.initClassificationList();
			//this.initReturnFilters();
			//this.initGPSTimeFilters();
			//this.initPointSourceIDFilters();

		}

		initReturnFilters(){
			let elReturnFilterPanel = $('#return_filter_panel');

			{ // RETURN NUMBER
				let sldReturnNumber = elReturnFilterPanel.find('#sldReturnNumber');
				let lblReturnNumber = elReturnFilterPanel.find('#lblReturnNumber');

				sldReturnNumber.slider({
					range: true,
					min: 0, max: 7, step: 1,
					values: [0, 7],
					slide: (event, ui) => {
						this.viewer.setFilterReturnNumberRange(ui.values[0], ui.values[1]);
					}
				});

				let onReturnNumberChanged = (event) => {
					let [from, to] = this.viewer.filterReturnNumberRange;

					lblReturnNumber[0].innerHTML = `${from} to ${to}`;
					sldReturnNumber.slider({values: [from, to]});
				};

				this.viewer.addEventListener('filter_return_number_range_changed', onReturnNumberChanged);

				onReturnNumberChanged();
			}

			{ // NUMBER OF RETURNS
				let sldNumberOfReturns = elReturnFilterPanel.find('#sldNumberOfReturns');
				let lblNumberOfReturns = elReturnFilterPanel.find('#lblNumberOfReturns');

				sldNumberOfReturns.slider({
					range: true,
					min: 0, max: 7, step: 1,
					values: [0, 7],
					slide: (event, ui) => {
						this.viewer.setFilterNumberOfReturnsRange(ui.values[0], ui.values[1]);
					}
				});

				let onNumberOfReturnsChanged = (event) => {
					let [from, to] = this.viewer.filterNumberOfReturnsRange;

					lblNumberOfReturns[0].innerHTML = `${from} to ${to}`;
					sldNumberOfReturns.slider({values: [from, to]});
				};

				this.viewer.addEventListener('filter_number_of_returns_range_changed', onNumberOfReturnsChanged);

				onNumberOfReturnsChanged();
			}
		}

		initGPSTimeFilters(){

			let elGPSTimeFilterPanel = $('#gpstime_filter_panel');

			{
				let slider = new HierarchicalSlider({
					levels: 4,
					slide: (event) => {
						this.viewer.setFilterGPSTimeRange(...event.values);
					},
				});

				let initialized = false;

				let initialize = () => {
					
					let elRangeContainer = $("#gpstime_multilevel_range_container");
					elRangeContainer[0].prepend(slider.element);

					let extent = this.viewer.getGpsTimeExtent();

					slider.setRange(extent);
					slider.setValues(extent);


					initialized = true;
				};

				this.viewer.addEventListener("update", (e) => {
					let extent = this.viewer.getGpsTimeExtent();
					let gpsTimeAvailable = extent[0] !== Infinity;

					if(!initialized && gpsTimeAvailable){
						initialize();
					}

					slider.setRange(extent);
				});
			}


			{
				
				const txtGpsTime = elGPSTimeFilterPanel.find("#txtGpsTime");
				const btnFindGpsTime = elGPSTimeFilterPanel.find("#btnFindGpsTime");

				let targetTime = null;

				txtGpsTime.on("input", (e) => {
					const str = txtGpsTime.val();

					if(!isNaN(str)){
						const value = parseFloat(str);
						targetTime = value;

						txtGpsTime.css("background-color", "");
					}else {
						targetTime = null;

						txtGpsTime.css("background-color", "#ff9999");
					}

				});

				btnFindGpsTime.click( () => {
					
					if(targetTime !== null){
						viewer.moveToGpsTimeVicinity(targetTime);
					}
				});
			}

		}

		initPointSourceIDFilters() {
			let elPointSourceIDFilterPanel = $('#pointsourceid_filter_panel');

			{
				let slider = new HierarchicalSlider({
					levels: 4,
					range: [0, 65535],
					precision: 1,
					slide: (event) => {
						let values = event.values;
						this.viewer.setFilterPointSourceIDRange(values[0], values[1]);
					}
				});

				let initialized = false;

				let initialize = () => {
					elPointSourceIDFilterPanel[0].prepend(slider.element);

					initialized = true;
				};

				this.viewer.addEventListener("update", (e) => {
					let extent = this.viewer.filterPointSourceIDRange;

					if(!initialized){
						initialize();

						slider.setValues(extent);
					}
					
				});
			}

			// let lblPointSourceID = elPointSourceIDFilterPanel.find("#lblPointSourceID");
			// let elPointSourceID = elPointSourceIDFilterPanel.find("#spnPointSourceID");

			// let slider = new ZoomableSlider();
			// elPointSourceID[0].appendChild(slider.element);
			// slider.update();

			// slider.change( () => {
			// 	let range = slider.chosenRange;
			// 	this.viewer.setFilterPointSourceIDRange(range[0], range[1]);
			// });

			// let onPointSourceIDExtentChanged = (event) => {
			// 	let range = this.viewer.filterPointSourceIDExtent;
			// 	slider.setVisibleRange(range);
			// };

			// let onPointSourceIDChanged = (event) => {
			// 	let range = this.viewer.filterPointSourceIDRange;

			// 	let precision = 1;
			// 	let from = `${Utils.addCommas(range[0].toFixed(precision))}`;
			// 	let to = `${Utils.addCommas(range[1].toFixed(precision))}`;
			// 	lblPointSourceID[0].innerHTML = `${from} to ${to}`;

			// 	slider.setRange(range);
			// };

			// this.viewer.addEventListener('filter_point_source_id_range_changed', onPointSourceIDChanged);
			// this.viewer.addEventListener('filter_point_source_id_extent_changed', onPointSourceIDExtentChanged);

		}

		initClassificationList(){
			let elClassificationList = $('#classificationList');

			let addClassificationItem = (code, name) => {
				const classification = this.viewer.classifications[code];
				const inputID = 'chkClassification_' + code;
				const colorPickerID = 'colorPickerClassification_' + code;

				const checked = classification.visible ? "checked" : "";

				let element = $(`
				<li>
					<label style="whitespace: nowrap; display: flex">
						<input id="${inputID}" type="checkbox" ${checked}/>
						<span style="flex-grow: 1">${name}</span>
						<input id="${colorPickerID}" style="zoom: 0.5" />
					</label>
				</li>
			`);

				const elInput = element.find('input');
				const elColorPicker = element.find(`#${colorPickerID}`);

				elInput.click(event => {
					this.viewer.setClassificationVisibility(code, event.target.checked);
				});

				let defaultColor = classification.color.map(c => c *  255).join(", ");
				defaultColor = `rgb(${defaultColor})`;


				elColorPicker.spectrum({
					// flat: true,
					color: defaultColor,
					showInput: true,
					preferredFormat: 'rgb',
					cancelText: '',
					chooseText: 'Apply',
					move: color => {
						let rgb = color.toRgb();
						const c = [rgb.r / 255, rgb.g / 255, rgb.b / 255, 1];
						classification.color = c;
					},
					change: color => {
						let rgb = color.toRgb();
						const c = [rgb.r / 255, rgb.g / 255, rgb.b / 255, 1];
						classification.color = c;
					}
				});

				elClassificationList.append(element);
			};

			const addToggleAllButton = () => { // toggle all button
				const element = $(`
				<li>
					<label style="whitespace: nowrap">
						<input id="toggleClassificationFilters" type="checkbox" checked/>
						<span>show/hide all</span>
					</label>
				</li>
			`);

				let elInput = element.find('input');

				elInput.click(event => {
					this.viewer.toggleAllClassificationsVisibility();
				});

				elClassificationList.append(element);
			};

			const addInvertButton = () => { 
				const element = $(`
				<li>
					<input type="button" value="invert" />
				</li>
			`);

				let elInput = element.find('input');

				elInput.click( () => {
					const classifications = this.viewer.classifications;
		
					for(let key of Object.keys(classifications)){
						let value = classifications[key];
						this.viewer.setClassificationVisibility(key, !value.visible);
					}
				});

				elClassificationList.append(element);
			};

			const populate = () => {
				addToggleAllButton();
				for (let classID in this.viewer.classifications) {
					addClassificationItem(classID, this.viewer.classifications[classID].name);
				}
				addInvertButton();
			};

			populate();

			this.viewer.addEventListener("classifications_changed", () => {
				elClassificationList.empty();
				populate();
			});

			this.viewer.addEventListener("classification_visibility_changed", () => {

				{ // set checked state of classification buttons
					for(const classID of Object.keys(this.viewer.classifications)){
						const classValue = this.viewer.classifications[classID];

						let elItem = elClassificationList.find(`#chkClassification_${classID}`);
						elItem.prop("checked", classValue.visible);
					}
				}

				{ // set checked state of toggle button based on state of all other buttons
					let numVisible = 0;
					let numItems = 0;
					for(const key of Object.keys(this.viewer.classifications)){
						if(this.viewer.classifications[key].visible){
							numVisible++;
						}
						numItems++;
					}
					const allVisible = numVisible === numItems;

					let elToggle = elClassificationList.find("#toggleClassificationFilters");
					elToggle.prop("checked", allVisible);
				}
			});
		}

		initAccordion(){
			$('.accordion > h3').each(function(){
				let header = $(this);
				let content = $(this).next();

				//header.addClass('accordion-header ui-widget');
				//content.addClass('accordion-content ui-widget');

				content.hide();

				header.click(() => {
					content.slideToggle();
				});
			});

			let languages = [
				["EN", "en"],
				["FR", "fr"],
				["DE", "de"],
				["JP", "jp"],
				["ES", "es"],
				["SE", "se"]
			];

			let elLanguages = $('#potree_languages');
			for(let i = 0; i < languages.length; i++){
				let [key, value] = languages[i];
				let element = $(`<a>${key}</a>`);
				element.click(() => this.viewer.setLanguage(value));

				if(i === 0){
					element.css("margin-left", "30px");
				}
				
				elLanguages.append(element);

				if(i < languages.length - 1){
					elLanguages.append($(document.createTextNode(' - ')));	
				}
			}


			// to close all, call
			// $(".accordion > div").hide()

			// to open the, for example, tool menu, call:
			// $("#menu_tools").next().show()
		}

		initAppearance(){

			/*const sldPointBudget = this.dom.find('#sldPointBudget');

			sldPointBudget.slider({
				value: this.viewer.getPointBudget(),
				min: 100 * 1000,
				max: 10 * 1000 * 1000,
				step: 1000,
				slide: (event, ui) => { this.viewer.setPointBudget(ui.value); }
			});*/

			this.dom.find('#sldFOV').slider({
				value: this.viewer.getFOV(),
				min: 20,
				max: 100,
				step: 1,
				slide: (event, ui) => { this.viewer.setFOV(ui.value); }
			});

			$('#sldEDLRadius').slider({
				value: this.viewer.getEDLRadius(),
				min: 1,
				max: 4,
				step: 0.01,
				slide: (event, ui) => { this.viewer.setEDLRadius(ui.value); }
			});

			$('#sldEDLStrength').slider({
				value: this.viewer.getEDLStrength(),
				min: 0,
				max: 5,
				step: 0.01,
				slide: (event, ui) => { this.viewer.setEDLStrength(ui.value); }
			});

			$('#sldEDLOpacity').slider({
				value: this.viewer.getEDLOpacity(),
				min: 0,
				max: 1,
				step: 0.01,
				slide: (event, ui) => { this.viewer.setEDLOpacity(ui.value); }
			});

			/*this.viewer.addEventListener('point_budget_changed', (event) => {
				$('#lblPointBudget')[0].innerHTML = Utils.addCommas(this.viewer.getPointBudget());
				sldPointBudget.slider({value: this.viewer.getPointBudget()});
			});*/

			this.viewer.addEventListener('fov_changed', (event) => {
				$('#lblFOV')[0].innerHTML = parseInt(this.viewer.getFOV());
				$('#sldFOV').slider({value: this.viewer.getFOV()});
			});

			this.viewer.addEventListener('use_edl_changed', (event) => {
				$('#chkEDLEnabled')[0].checked = this.viewer.getEDLEnabled();
			});

			this.viewer.addEventListener('edl_radius_changed', (event) => {
				$('#lblEDLRadius')[0].innerHTML = this.viewer.getEDLRadius().toFixed(1);
				$('#sldEDLRadius').slider({value: this.viewer.getEDLRadius()});
			});

			this.viewer.addEventListener('edl_strength_changed', (event) => {
				$('#lblEDLStrength')[0].innerHTML = this.viewer.getEDLStrength().toFixed(1);
				$('#sldEDLStrength').slider({value: this.viewer.getEDLStrength()});
			});

			this.viewer.addEventListener('background_changed', (event) => {
				$("input[name=background][value='" + this.viewer.getBackground() + "']").prop('checked', true);
			});

			/*$('#lblPointBudget')[0].innerHTML = Utils.addCommas(this.viewer.getPointBudget());*/
			$('#lblFOV')[0].innerHTML = parseInt(this.viewer.getFOV());
			$('#lblEDLRadius')[0].innerHTML = this.viewer.getEDLRadius().toFixed(1);
			$('#lblEDLStrength')[0].innerHTML = this.viewer.getEDLStrength().toFixed(1);
			$('#chkEDLEnabled')[0].checked = this.viewer.getEDLEnabled();
			
			{
				let elBackground = $(`#background_options`);
				elBackground.selectgroup();

				elBackground.find("input").click( (e) => {
					this.viewer.setBackground(e.target.value);
				});

				let currentBackground = this.viewer.getBackground();
				$(`input[name=background_options][value=${currentBackground}]`).trigger("click");
			}

			$('#chkEDLEnabled').click( () => {
				this.viewer.setEDLEnabled($('#chkEDLEnabled').prop("checked"));
			});
		}

		initNavigation(){
			let elNavigation = $('#navigation');
			let sldMoveSpeed = $('#sldMoveSpeed');
			let lblMoveSpeed = $('#lblMoveSpeed');

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + '/icons/earth_controls_1.png',
				'[title]tt.earth_control',
				() => { this.viewer.setControls(this.viewer.earthControls); }
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + '/icons/fps_controls.svg',
				'[title]tt.flight_control',
				() => {
					this.viewer.setControls(this.viewer.fpControls);
					this.viewer.fpControls.lockElevation = false;
				}
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + '/icons/helicopter_controls.svg',
				'[title]tt.heli_control',
				() => { 
					this.viewer.setControls(this.viewer.fpControls);
					this.viewer.fpControls.lockElevation = true;
				}
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + '/icons/orbit_controls.svg',
				'[title]tt.orbit_control',
				() => { this.viewer.setControls(this.viewer.orbitControls); }
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + '/icons/focus.svg',
				'[title]tt.focus_control',
				() => { this.viewer.fitToScreen(); }
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + "/icons/navigation_cube.svg",
				"[title]tt.navigation_cube_control",
				() => {this.viewer.toggleNavigationCube();}
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + "/images/compas.svg",
				"[title]tt.compass",
				() => {
					const visible = !this.viewer.compass.isVisible();
					this.viewer.compass.setVisible(visible);
				}
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + "/icons/camera_animation.svg",
				"[title]tt.camera_animation",
				() => {
					const animation = CameraAnimation.defaultFromView(this.viewer);

					viewer.scene.addCameraAnimation(animation);
				}
			));


			elNavigation.append("<br>");


			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + "/icons/left.svg",
				"[title]tt.left_view_control",
				() => {this.viewer.setLeftView();}
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + "/icons/right.svg",
				"[title]tt.right_view_control",
				() => {this.viewer.setRightView();}
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + "/icons/front.svg",
				"[title]tt.front_view_control",
				() => {this.viewer.setFrontView();}
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + "/icons/back.svg",
				"[title]tt.back_view_control",
				() => {this.viewer.setBackView();}
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + "/icons/top.svg",
				"[title]tt.top_view_control",
				() => {this.viewer.setTopView();}
			));

			elNavigation.append(this.createToolIcon(
				Potree.resourcePath + "/icons/bottom.svg",
				"[title]tt.bottom_view_control",
				() => {this.viewer.setBottomView();}
			));





			let elCameraProjection = $(`
			<selectgroup id="camera_projection_options">
				<option id="camera_projection_options_perspective" value="PERSPECTIVE">Perspective</option>
				<option id="camera_projection_options_orthigraphic" value="ORTHOGRAPHIC">Orthographic</option>
			</selectgroup>
		`);
			elNavigation.append(elCameraProjection);
			elCameraProjection.selectgroup({title: "Camera Projection"});
			elCameraProjection.find("input").click( (e) => {
				this.viewer.setCameraMode(CameraMode[e.target.value]);
			});
			let cameraMode = Object.keys(CameraMode)
				.filter(key => CameraMode[key] === this.viewer.scene.cameraMode);
			elCameraProjection.find(`input[value=${cameraMode}]`).trigger("click");

			let speedRange = new Vector2(1, 10 * 1000);

			let toLinearSpeed = (value) => {
				return Math.pow(value, 4) * speedRange.y + speedRange.x;
			};

			let toExpSpeed = (value) => {
				return Math.pow((value - speedRange.x) / speedRange.y, 1 / 4);
			};

			sldMoveSpeed.slider({
				value: toExpSpeed(this.viewer.getMoveSpeed()),
				min: 0,
				max: 1,
				step: 0.01,
				slide: (event, ui) => { this.viewer.setMoveSpeed(toLinearSpeed(ui.value)); }
			});

			this.viewer.addEventListener('move_speed_changed', (event) => {
				lblMoveSpeed.html(this.viewer.getMoveSpeed().toFixed(1));
				sldMoveSpeed.slider({value: toExpSpeed(this.viewer.getMoveSpeed())});
			});

			lblMoveSpeed.html(this.viewer.getMoveSpeed().toFixed(1));
		}


		initSettings(){

			{
				$('#sldMinNodeSize').slider({
					value: this.viewer.getMinNodeSize(),
					min: 0,
					max: 1000,
					step: 0.01,
					slide: (event, ui) => { this.viewer.setMinNodeSize(ui.value); }
				});

				this.viewer.addEventListener('minnodesize_changed', (event) => {
					$('#lblMinNodeSize').html(parseInt(this.viewer.getMinNodeSize()));
					$('#sldMinNodeSize').slider({value: this.viewer.getMinNodeSize()});
				});
				$('#lblMinNodeSize').html(parseInt(this.viewer.getMinNodeSize()));
			}

			{
				let elSplatQuality = $("#splat_quality_options");
				elSplatQuality.selectgroup({title: "Splat Quality"});

				elSplatQuality.find("input").click( (e) => {
					if(e.target.value === "standard"){
						this.viewer.useHQ = false;
					}else if(e.target.value === "hq"){
						this.viewer.useHQ = true;
					}
				});

				let currentQuality = this.viewer.useHQ ? "hq" : "standard";
				elSplatQuality.find(`input[value=${currentQuality}]`).trigger("click");
			}

			$('#show_bounding_box').click(() => {
				this.viewer.setShowBoundingBox($('#show_bounding_box').prop("checked"));
			});

			$('#set_freeze').click(() => {
				this.viewer.setFreeze($('#set_freeze').prop("checked"));
			});
		}

	}

	class AnnotationTool extends EventDispatcher{
		constructor (viewer) {
			super();

			this.viewer = viewer;
			this.renderer = viewer.renderer;

			this.sg = new SphereGeometry(0.1);
			this.sm = new MeshNormalMaterial();
			this.s = new Mesh(this.sg, this.sm);
		}

		startInsertion (args = {}) {
			let domElement = this.viewer.renderer.domElement;

			let annotation = new Annotation({
				position: [589748.270, 231444.540, 753.675],
				title: "Annotation Title",
				description: `Annotation Description`
			});
			this.dispatchEvent({type: 'start_inserting_annotation', annotation: annotation});

			const annotations = this.viewer.scene.annotations;
			annotations.add(annotation);

			let callbacks = {
				cancel: null,
				finish: null,
			};

			let insertionCallback = (e) => {
				if (e.button === MOUSE.LEFT) {
					callbacks.finish();
				} else if (e.button === MOUSE.RIGHT) {
					callbacks.cancel();
				}
			};

			callbacks.cancel = e => {
				annotations.remove(annotation);

				domElement.removeEventListener('mouseup', insertionCallback, true);
			};

			callbacks.finish = e => {
				domElement.removeEventListener('mouseup', insertionCallback, true);
			};

			domElement.addEventListener('mouseup', insertionCallback, true);

			let drag = (e) => {
				let I = Utils.getMousePointCloudIntersection(
					e.drag.end, 
					e.viewer.scene.getActiveCamera(), 
					e.viewer, 
					e.viewer.scene.pointclouds,
					{pickClipped: true});

				if (I) {
					this.s.position.copy(I.location);

					annotation.position.copy(I.location);
				}
			};

			let drop = (e) => {
				viewer.scene.scene.remove(this.s);
				this.s.removeEventListener("drag", drag);
				this.s.removeEventListener("drop", drop);
			};

			this.s.addEventListener('drag', drag);
			this.s.addEventListener('drop', drop);

			this.viewer.scene.scene.add(this.s);
			this.viewer.inputHandler.startDragging(this.s);

			return annotation;
		}
		
		update(){
			// let camera = this.viewer.scene.getActiveCamera();
			// let domElement = this.renderer.domElement;
			// let measurements = this.viewer.scene.measurements;

			// const renderAreaSize = this.renderer.getSize(new THREE.Vector2());
			// let clientWidth = renderAreaSize.width;
			// let clientHeight = renderAreaSize.height;

		}

		render(){
			//this.viewer.renderer.render(this.scene, this.viewer.scene.getActiveCamera());
		}
	};

	/**
	 * @author mschuetz / http://mschuetz.at
	 *
	 *
	 */

	class InputHandler extends EventDispatcher {
		constructor (viewer) {
			super();

			this.viewer = viewer;
			this.renderer = viewer.renderer;
			this.domElement = this.renderer.domElement;
			this.enabled = true;
			
			this.scene = null;
			this.interactiveScenes = [];
			this.interactiveObjects = new Set();
			this.inputListeners = [];
			this.blacklist = new Set();

			this.drag = null;
			this.mouse = new Vector2(0, 0);

			this.selection = [];

			this.hoveredElements = [];
			this.pressedKeys = {};

			this.wheelDelta = 0;

			this.speed = 1;

			this.logMessages = false;

			if (this.domElement.tabIndex === -1) {
				this.domElement.tabIndex = 2222;
			}

			this.domElement.addEventListener('contextmenu', (event) => { event.preventDefault(); }, false);
			this.domElement.addEventListener('click', this.onMouseClick.bind(this), false);
			this.domElement.addEventListener('mousedown', this.onMouseDown.bind(this), false);
			this.domElement.addEventListener('mouseup', this.onMouseUp.bind(this), false);
			this.domElement.addEventListener('mousemove', this.onMouseMove.bind(this), false);
			this.domElement.addEventListener('mousewheel', this.onMouseWheel.bind(this), false);
			this.domElement.addEventListener('DOMMouseScroll', this.onMouseWheel.bind(this), false); // Firefox
			this.domElement.addEventListener('dblclick', this.onDoubleClick.bind(this));
			this.domElement.addEventListener('keydown', this.onKeyDown.bind(this));
			this.domElement.addEventListener('keyup', this.onKeyUp.bind(this));
			this.domElement.addEventListener('touchstart', this.onTouchStart.bind(this));
			this.domElement.addEventListener('touchend', this.onTouchEnd.bind(this));
			this.domElement.addEventListener('touchmove', this.onTouchMove.bind(this));
		}

		addInputListener (listener) {
			this.inputListeners.push(listener);
		}

		removeInputListener (listener) {
			this.inputListeners = this.inputListeners.filter(e => e !== listener);
		}

		getSortedListeners(){
			return this.inputListeners.sort( (a, b) => {
				let ia = (a.importance !== undefined) ? a.importance : 0;
				let ib = (b.importance !== undefined) ? b.importance : 0;

				return ib - ia;
			});
		}

		onTouchStart (e) {
			if (this.logMessages) console.log(this.constructor.name + ': onTouchStart');

			e.preventDefault();

			if (e.touches.length === 1) {
				let rect = this.domElement.getBoundingClientRect();
				let x = e.touches[0].pageX - rect.left;
				let y = e.touches[0].pageY - rect.top;
				this.mouse.set(x, y);

				this.startDragging(null);
			}

			
			for (let inputListener of this.getSortedListeners()) {
				inputListener.dispatchEvent({
					type: e.type,
					touches: e.touches,
					changedTouches: e.changedTouches
				});
			}
		}

		onTouchEnd (e) {
			if (this.logMessages) console.log(this.constructor.name + ': onTouchEnd');

			e.preventDefault();

			for (let inputListener of this.getSortedListeners()) {
				inputListener.dispatchEvent({
					type: 'drop',
					drag: this.drag,
					viewer: this.viewer
				});
			}

			this.drag = null;

			for (let inputListener of this.getSortedListeners()) {
				inputListener.dispatchEvent({
					type: e.type,
					touches: e.touches,
					changedTouches: e.changedTouches
				});
			}
		}

		onTouchMove (e) {
			if (this.logMessages) console.log(this.constructor.name + ': onTouchMove');

			e.preventDefault();

			if (e.touches.length === 1) {
				let rect = this.domElement.getBoundingClientRect();
				let x = e.touches[0].pageX - rect.left;
				let y = e.touches[0].pageY - rect.top;
				this.mouse.set(x, y);

				if (this.drag) {
					this.drag.mouse = 1;

					this.drag.lastDrag.x = x - this.drag.end.x;
					this.drag.lastDrag.y = y - this.drag.end.y;

					this.drag.end.set(x, y);

					if (this.logMessages) console.log(this.constructor.name + ': drag: ');
					for (let inputListener of this.getSortedListeners()) {
						inputListener.dispatchEvent({
							type: 'drag',
							drag: this.drag,
							viewer: this.viewer
						});
					}
				}
			}

			for (let inputListener of this.getSortedListeners()) {
				inputListener.dispatchEvent({
					type: e.type,
					touches: e.touches,
					changedTouches: e.changedTouches
				});
			}

			// DEBUG CODE
			// let debugTouches = [...e.touches, {
			//	pageX: this.domElement.clientWidth / 2,
			//	pageY: this.domElement.clientHeight / 2}];
			// for(let inputListener of this.getSortedListeners()){
			//	inputListener.dispatchEvent({
			//		type: e.type,
			//		touches: debugTouches,
			//		changedTouches: e.changedTouches
			//	});
			// }
		}

		onKeyDown (e) {
			if (this.logMessages) console.log(this.constructor.name + ': onKeyDown');

			// DELETE
			if (e.keyCode === KeyCodes.DELETE && this.selection.length > 0) {
				this.dispatchEvent({
					type: 'delete',
					selection: this.selection
				});

				this.deselectAll();
			}

			this.dispatchEvent({
				type: 'keydown',
				keyCode: e.keyCode,
				event: e
			});

			// for(let l of this.getSortedListeners()){
			//	l.dispatchEvent({
			//		type: "keydown",
			//		keyCode: e.keyCode,
			//		event: e
			//	});
			// }

			this.pressedKeys[e.keyCode] = true;

			// e.preventDefault();
		}

		onKeyUp (e) {
			if (this.logMessages) console.log(this.constructor.name + ': onKeyUp');

			delete this.pressedKeys[e.keyCode];

			e.preventDefault();
		}

		onDoubleClick (e) {
			if (this.logMessages) console.log(this.constructor.name + ': onDoubleClick');

			let consumed = false;
			for (let hovered of this.hoveredElements) {
				if (hovered._listeners && hovered._listeners['dblclick']) {
					hovered.object.dispatchEvent({
						type: 'dblclick',
						mouse: this.mouse,
						object: hovered.object
					});
					consumed = true;
					break;
				}
			}

			if (!consumed) {
				for (let inputListener of this.getSortedListeners()) {
					inputListener.dispatchEvent({
						type: 'dblclick',
						mouse: this.mouse,
						object: null
					});
				}
			}

			e.preventDefault();
		}

		onMouseClick (e) {
			if (this.logMessages) console.log(this.constructor.name + ': onMouseClick');

			e.preventDefault();
		}

		onMouseDown (e) {
			if (this.logMessages) console.log(this.constructor.name + ': onMouseDown');

			e.preventDefault();

			let consumed = false;
			let consume = () => { return consumed = true; };
			if (this.hoveredElements.length === 0) {
				for (let inputListener of this.getSortedListeners()) {
					inputListener.dispatchEvent({
						type: 'mousedown',
						viewer: this.viewer,
						mouse: this.mouse
					});
				}
			}else {
				for(let hovered of this.hoveredElements){
					let object = hovered.object;
					object.dispatchEvent({
						type: 'mousedown',
						viewer: this.viewer,
						consume: consume
					});

					if(consumed){
						break;
					}
				}
			}

			if (!this.drag) {
				let target = this.hoveredElements
					.find(el => (
						el.object._listeners &&
						el.object._listeners['drag'] &&
						el.object._listeners['drag'].length > 0));

				if (target) {
					this.startDragging(target.object, {location: target.point});
				} else {
					this.startDragging(null);
				}
			}

			if (this.scene) {
				this.viewStart = this.scene.view.clone();
			}
		}

		onMouseUp (e) {
			if (this.logMessages) console.log(this.constructor.name + ': onMouseUp');

			e.preventDefault();

			let noMovement = this.getNormalizedDrag().length() === 0;

			
			let consumed = false;
			let consume = () => { return consumed = true; };
			if (this.hoveredElements.length === 0) {
				for (let inputListener of this.getSortedListeners()) {
					inputListener.dispatchEvent({
						type: 'mouseup',
						viewer: this.viewer,
						mouse: this.mouse,
						consume: consume
					});

					if(consumed){
						break;
					}
				}
			}else {
				let hovered = this.hoveredElements
					.map(e => e.object)
					.find(e => (e._listeners && e._listeners['mouseup']));
				if(hovered){
					hovered.dispatchEvent({
						type: 'mouseup',
						viewer: this.viewer,
						consume: consume
					});
				}
			}

			if (this.drag) {
				if (this.drag.object) {
					if (this.logMessages) console.log(`${this.constructor.name}: drop ${this.drag.object.name}`);
					this.drag.object.dispatchEvent({
						type: 'drop',
						drag: this.drag,
						viewer: this.viewer

					});
				} else {
					for (let inputListener of this.getSortedListeners()) {
						inputListener.dispatchEvent({
							type: 'drop',
							drag: this.drag,
							viewer: this.viewer
						});
					}
				}

				// check for a click
				let clicked = this.hoveredElements.map(h => h.object).find(v => v === this.drag.object) !== undefined;
				if(clicked){
					if (this.logMessages) console.log(`${this.constructor.name}: click ${this.drag.object.name}`);
					this.drag.object.dispatchEvent({
						type: 'click',
						viewer: this.viewer,
						consume: consume,
					});
				}

				this.drag = null;
			}

			if(!consumed){
				if (e.button === MOUSE.LEFT) {
					if (noMovement) {
						let selectable = this.hoveredElements
							.find(el => el.object._listeners && el.object._listeners['select']);

						if (selectable) {
							selectable = selectable.object;

							if (this.isSelected(selectable)) {
								this.selection
									.filter(e => e !== selectable)
									.forEach(e => this.toggleSelection(e));
							} else {
								this.deselectAll();
								this.toggleSelection(selectable);
							}
						} else {
							this.deselectAll();
						}
					}
				} else if ((e.button === MOUSE.RIGHT) && noMovement) {
					this.deselectAll();
				}
			}
		}

		onMouseMove (e) {
			e.preventDefault();

			let rect = this.domElement.getBoundingClientRect();
			let x = e.clientX - rect.left;
			let y = e.clientY - rect.top;
			this.mouse.set(x, y);

			let hoveredElements = this.getHoveredElements();
			if(hoveredElements.length > 0){
				let names = hoveredElements.map(h => h.object.name).join(", ");
				if (this.logMessages) console.log(`${this.constructor.name}: onMouseMove; hovered: '${names}'`);
			}

			if (this.drag) {
				this.drag.mouse = e.buttons;

				this.drag.lastDrag.x = x - this.drag.end.x;
				this.drag.lastDrag.y = y - this.drag.end.y;

				this.drag.end.set(x, y);

				if (this.drag.object) {
					if (this.logMessages) console.log(this.constructor.name + ': drag: ' + this.drag.object.name);
					this.drag.object.dispatchEvent({
						type: 'drag',
						drag: this.drag,
						viewer: this.viewer
					});
				} else {
					if (this.logMessages) console.log(this.constructor.name + ': drag: ');

					let dragConsumed = false;
					for (let inputListener of this.getSortedListeners()) {
						inputListener.dispatchEvent({
							type: 'drag',
							drag: this.drag,
							viewer: this.viewer,
							consume: () => {dragConsumed = true;}
						});

						if(dragConsumed){
							break;
						}
					}
				}
			}else {
				let curr = hoveredElements.map(a => a.object).find(a => true);
				let prev = this.hoveredElements.map(a => a.object).find(a => true);

				if(curr !== prev){
					if(curr){
						if (this.logMessages) console.log(`${this.constructor.name}: mouseover: ${curr.name}`);
						curr.dispatchEvent({
							type: 'mouseover',
							object: curr,
						});
					}
					if(prev){
						if (this.logMessages) console.log(`${this.constructor.name}: mouseleave: ${prev.name}`);
						prev.dispatchEvent({
							type: 'mouseleave',
							object: prev,
						});
					}
				}

				if(hoveredElements.length > 0){
					let object = hoveredElements
						.map(e => e.object)
						.find(e => (e._listeners && e._listeners['mousemove']));
					
					if(object){
						object.dispatchEvent({
							type: 'mousemove',
							object: object
						});
					}
				}

			}
			
			// for (let inputListener of this.getSortedListeners()) {
			// 	inputListener.dispatchEvent({
			// 		type: 'mousemove',
			// 		object: null
			// 	});
			// }
			

			this.hoveredElements = hoveredElements;
		}
		
		onMouseWheel(e){
			if(!this.enabled) return;

			if(this.logMessages) console.log(this.constructor.name + ": onMouseWheel");
			
			e.preventDefault();

			let delta = 0;
			if (e.wheelDelta !== undefined) { // WebKit / Opera / Explorer 9
				delta = e.wheelDelta;
			} else if (e.detail !== undefined) { // Firefox
				delta = -e.detail;
			}

			let ndelta = Math.sign(delta);

			// this.wheelDelta += Math.sign(delta);

			if (this.hoveredElement) {
				this.hoveredElement.object.dispatchEvent({
					type: 'mousewheel',
					delta: ndelta,
					object: this.hoveredElement.object
				});
			} else {
				for (let inputListener of this.getSortedListeners()) {
					inputListener.dispatchEvent({
						type: 'mousewheel',
						delta: ndelta,
						object: null
					});
				}
			}
		}

		startDragging (object, args = null) {

			let name = object ? object.name : "no name";
			if (this.logMessages) console.log(`${this.constructor.name}: startDragging: '${name}'`);

			this.drag = {
				start: this.mouse.clone(),
				end: this.mouse.clone(),
				lastDrag: new Vector2(0, 0),
				startView: this.scene.view.clone(),
				object: object
			};

			if (args) {
				for (let key of Object.keys(args)) {
					this.drag[key] = args[key];
				}
			}
		}

		getMousePointCloudIntersection (mouse) {
			return Utils.getMousePointCloudIntersection(
				this.mouse, 
				this.scene.getActiveCamera(), 
				this.viewer, 
				this.scene.pointclouds);
		}

		toggleSelection (object) {
			let oldSelection = this.selection;

			let index = this.selection.indexOf(object);

			if (index === -1) {
				this.selection.push(object);
				object.dispatchEvent({
					type: 'select'
				});
			} else {
				this.selection.splice(index, 1);
				object.dispatchEvent({
					type: 'deselect'
				});
			}

			this.dispatchEvent({
				type: 'selection_changed',
				oldSelection: oldSelection,
				selection: this.selection
			});
		}

		deselect(object){

			let oldSelection = this.selection;

			let index = this.selection.indexOf(object);

			if(index >= 0){
				this.selection.splice(index, 1);
				object.dispatchEvent({
					type: 'deselect'
				});

				this.dispatchEvent({
					type: 'selection_changed',
					oldSelection: oldSelection,
					selection: this.selection
				});
			}
		}

		deselectAll () {
			for (let object of this.selection) {
				object.dispatchEvent({
					type: 'deselect'
				});
			}

			let oldSelection = this.selection;

			if (this.selection.length > 0) {
				this.selection = [];
				this.dispatchEvent({
					type: 'selection_changed',
					oldSelection: oldSelection,
					selection: this.selection
				});
			}
		}

		isSelected (object) {
			let index = this.selection.indexOf(object);

			return index !== -1;
		}

		registerInteractiveObject(object){
			this.interactiveObjects.add(object);
		}

		removeInteractiveObject(object){
			this.interactiveObjects.delete(object);
		}

		registerInteractiveScene (scene) {
			let index = this.interactiveScenes.indexOf(scene);
			if (index === -1) {
				this.interactiveScenes.push(scene);
			}
		}

		unregisterInteractiveScene (scene) {
			let index = this.interactiveScenes.indexOf(scene);
			if (index > -1) {
				this.interactiveScenes.splice(index, 1);
			}
		}

		getHoveredElement () {
			let hoveredElements = this.getHoveredElements();
			if (hoveredElements.length > 0) {
				return hoveredElements[0];
			} else {
				return null;
			}
		}

		getHoveredElements () {
			let scenes = this.interactiveScenes.concat(this.scene.scene);

			let interactableListeners = ['mouseup', 'mousemove', 'mouseover', 'mouseleave', 'drag', 'drop', 'click', 'select', 'deselect'];
			let interactables = [];
			for (let scene of scenes) {
				scene.traverseVisible(node => {
					if (node._listeners && node.visible && !this.blacklist.has(node)) {
						let hasInteractableListener = interactableListeners.filter((e) => {
							return node._listeners[e] !== undefined;
						}).length > 0;

						if (hasInteractableListener) {
							interactables.push(node);
						}
					}
				});
			}
			
			let camera = this.scene.getActiveCamera();
			let ray = Utils.mouseToRay(this.mouse, camera, this.domElement.clientWidth, this.domElement.clientHeight);
			
			let raycaster = new Raycaster();
			raycaster.ray.set(ray.origin, ray.direction);
			raycaster.params.Line.threshold = 0.2;

			let intersections = raycaster.intersectObjects(interactables.filter(o => o.visible), false);

			return intersections;
		}

		setScene (scene) {
			this.deselectAll();

			this.scene = scene;
		}

		update (delta) {

		}

		getNormalizedDrag () {
			if (!this.drag) {
				return new Vector2(0, 0);
			}

			let diff = new Vector2().subVectors(this.drag.end, this.drag.start);

			diff.x = diff.x / this.domElement.clientWidth;
			diff.y = diff.y / this.domElement.clientHeight;

			return diff;
		}

		getNormalizedLastDrag () {
			if (!this.drag) {
				return new Vector2(0, 0);
			}

			let lastDrag = this.drag.lastDrag.clone();

			lastDrag.x = lastDrag.x / this.domElement.clientWidth;
			lastDrag.y = lastDrag.y / this.domElement.clientHeight;

			return lastDrag;
		}
	}

	class NavigationCube extends Object3D {

		constructor(viewer){
			super();

			this.viewer = viewer;

			let createPlaneMaterial = (img) => {
				let material = new MeshBasicMaterial( {
					depthTest: true, 
					depthWrite: true,
					side: DoubleSide
				});
				new TextureLoader().load(
					exports.resourcePath + '/textures/navigation/' + img,
					function(texture) {
						texture.anisotropy = viewer.renderer.capabilities.getMaxAnisotropy();
						material.map = texture;
						material.needsUpdate = true;
					});
				return material;
			};

			let planeGeometry = new PlaneGeometry(1, 1);

			this.front = new Mesh(planeGeometry, createPlaneMaterial('F.png'));
			this.front.position.y = -0.5;
			this.front.rotation.x = Math.PI / 2.0;
			this.front.updateMatrixWorld();
			this.front.name = "F";
			this.add(this.front);

			this.back = new Mesh(planeGeometry, createPlaneMaterial('B.png'));
			this.back.position.y = 0.5;
			this.back.rotation.x = Math.PI / 2.0;
			this.back.updateMatrixWorld();
			this.back.name = "B";
			this.add(this.back);

			this.left = new Mesh(planeGeometry, createPlaneMaterial('L.png'));
			this.left.position.x = -0.5;
			this.left.rotation.y = Math.PI / 2.0;
			this.left.updateMatrixWorld();
			this.left.name = "L";
			this.add(this.left);

			this.right = new Mesh(planeGeometry, createPlaneMaterial('R.png'));
			this.right.position.x = 0.5;
			this.right.rotation.y = Math.PI / 2.0;
			this.right.updateMatrixWorld();
			this.right.name = "R";
			this.add(this.right);

			this.bottom = new Mesh(planeGeometry, createPlaneMaterial('D.png'));
			this.bottom.position.z = -0.5;
			this.bottom.updateMatrixWorld();
			this.bottom.name = "D";
			this.add(this.bottom);

			this.top = new Mesh(planeGeometry, createPlaneMaterial('U.png'));
			this.top.position.z = 0.5;
			this.top.updateMatrixWorld();
			this.top.name = "U";
			this.add(this.top);

			this.width = 150; // in px

			this.camera = new OrthographicCamera(-1, 1, 1, -1, -1, 1);
			this.camera.position.copy(new Vector3(0, 0, 0));
			this.camera.lookAt(new Vector3(0, 1, 0));
			this.camera.updateMatrixWorld();
			this.camera.rotation.order = "ZXY";

			let onMouseDown = (event) => {
				if (!this.visible) {
					return;
				}
				
				this.pickedFace = null;
				let mouse = new Vector2();
				mouse.x = event.clientX - (window.innerWidth - this.width);
				mouse.y = event.clientY;

				if(mouse.x < 0 || mouse.y > this.width) return;

				mouse.x = (mouse.x / this.width) * 2 - 1;
				mouse.y = -(mouse.y / this.width) * 2 + 1;

				let raycaster = new Raycaster();
				raycaster.setFromCamera(mouse, this.camera);
				raycaster.ray.origin.sub(this.camera.getWorldDirection(new Vector3()));

				let intersects = raycaster.intersectObjects(this.children);

				let minDistance = 1000;
				for (let i = 0; i < intersects.length; i++) {
					if(intersects[i].distance < minDistance) {
						this.pickedFace = intersects[i].object.name;
						minDistance = intersects[i].distance;
					}
				}
				
				if(this.pickedFace) {
					this.viewer.setView(this.pickedFace);
				}
			};

			this.viewer.renderer.domElement.addEventListener('mousedown', onMouseDown, false);
		}

		update(rotation) {
			this.camera.rotation.copy(rotation);
			this.camera.updateMatrixWorld();
		}

	}

	/**
	 * @author mschuetz / http://mschuetz.at
	 *
	 * adapted from THREE.OrbitControls by
	 *
	 * @author qiao / https://github.com/qiao
	 * @author mrdoob / http://mrdoob.com
	 * @author alteredq / http://alteredqualia.com/
	 * @author WestLangley / http://github.com/WestLangley
	 * @author erich666 / http://erichaines.com
	 *
	 *
	 *
	 */

	 
	class OrbitControls extends EventDispatcher{
		
		constructor(viewer){
			super();
			
			this.viewer = viewer;
			this.renderer = viewer.renderer;

			this.scene = null;
			this.sceneControls = new Scene();

			this.rotationSpeed = 5;

			this.fadeFactor = 20;
			this.yawDelta = 0;
			this.pitchDelta = 0;
			this.panDelta = new Vector2(0, 0);
			this.radiusDelta = 0;

			this.doubleClockZoomEnabled = true;

			this.tweens = [];

			let drag = (e) => {
				if (e.drag.object !== null) {
					return;
				}

				if (e.drag.startHandled === undefined) {
					e.drag.startHandled = true;

					this.dispatchEvent({type: 'start'});
				}

				let ndrag = {
					x: e.drag.lastDrag.x / this.renderer.domElement.clientWidth,
					y: e.drag.lastDrag.y / this.renderer.domElement.clientHeight
				};

				if (e.drag.mouse === MOUSE$1.LEFT) {
					this.yawDelta += ndrag.x * this.rotationSpeed;
					this.pitchDelta += ndrag.y * this.rotationSpeed;

					this.stopTweens();
				} else if (e.drag.mouse === MOUSE$1.RIGHT) {
					this.panDelta.x += ndrag.x;
					this.panDelta.y += ndrag.y;

					this.stopTweens();
				}
			};

			let drop = e => {
				this.dispatchEvent({type: 'end'});
			};

			let scroll = (e) => {
				let resolvedRadius = this.scene.view.radius + this.radiusDelta;

				this.radiusDelta += -e.delta * resolvedRadius * 0.1;

				this.stopTweens();
			};

			let dblclick = (e) => {
				if(this.doubleClockZoomEnabled){
					this.zoomToLocation(e.mouse);
				}
			};

			let previousTouch = null;
			let touchStart = e => {
				previousTouch = e;
			};

			let touchEnd = e => {
				previousTouch = e;
			};

			let touchMove = e => {
				if (e.touches.length === 2 && previousTouch.touches.length === 2){
					let prev = previousTouch;
					let curr = e;

					let prevDX = prev.touches[0].pageX - prev.touches[1].pageX;
					let prevDY = prev.touches[0].pageY - prev.touches[1].pageY;
					let prevDist = Math.sqrt(prevDX * prevDX + prevDY * prevDY);

					let currDX = curr.touches[0].pageX - curr.touches[1].pageX;
					let currDY = curr.touches[0].pageY - curr.touches[1].pageY;
					let currDist = Math.sqrt(currDX * currDX + currDY * currDY);

					let delta = currDist / prevDist;
					let resolvedRadius = this.scene.view.radius + this.radiusDelta;
					let newRadius = resolvedRadius / delta;
					this.radiusDelta = newRadius - resolvedRadius;

					this.stopTweens();
				}else if(e.touches.length === 3 && previousTouch.touches.length === 3){
					let prev = previousTouch;
					let curr = e;

					let prevMeanX = (prev.touches[0].pageX + prev.touches[1].pageX + prev.touches[2].pageX) / 3;
					let prevMeanY = (prev.touches[0].pageY + prev.touches[1].pageY + prev.touches[2].pageY) / 3;

					let currMeanX = (curr.touches[0].pageX + curr.touches[1].pageX + curr.touches[2].pageX) / 3;
					let currMeanY = (curr.touches[0].pageY + curr.touches[1].pageY + curr.touches[2].pageY) / 3;

					let delta = {
						x: (currMeanX - prevMeanX) / this.renderer.domElement.clientWidth,
						y: (currMeanY - prevMeanY) / this.renderer.domElement.clientHeight
					};

					this.panDelta.x += delta.x;
					this.panDelta.y += delta.y;

					this.stopTweens();
				}

				previousTouch = e;
			};

			this.addEventListener('touchstart', touchStart);
			this.addEventListener('touchend', touchEnd);
			this.addEventListener('touchmove', touchMove);
			this.addEventListener('drag', drag);
			this.addEventListener('drop', drop);
			this.addEventListener('mousewheel', scroll);
			this.addEventListener('dblclick', dblclick);
		}

		setScene (scene) {
			this.scene = scene;
		}

		stop(){
			this.yawDelta = 0;
			this.pitchDelta = 0;
			this.radiusDelta = 0;
			this.panDelta.set(0, 0);
		}
		
		zoomToLocation(mouse){
			let camera = this.scene.getActiveCamera();
			
			let I = Utils.getMousePointCloudIntersection(
				mouse,
				camera,
				this.viewer,
				this.scene.pointclouds,
				{pickClipped: true});

			if (I === null) {
				return;
			}

			let targetRadius = 0;
			{
				let minimumJumpDistance = 0.2;

				let domElement = this.renderer.domElement;
				let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);

				let nodes = I.pointcloud.nodesOnRay(I.pointcloud.visibleNodes, ray);
				let lastNode = nodes[nodes.length - 1];
				let radius = lastNode.getBoundingSphere(new Sphere()).radius;
				targetRadius = Math.min(this.scene.view.radius, radius);
				targetRadius = Math.max(minimumJumpDistance, targetRadius);
			}

			let d = this.scene.view.direction.multiplyScalar(-1);
			let cameraTargetPosition = new Vector3().addVectors(I.location, d.multiplyScalar(targetRadius));
			// TODO Unused: let controlsTargetPosition = I.location;

			let animationDuration = 600;
			let easing = TWEEN.Easing.Quartic.Out;

			{ // animate
				let value = {x: 0};
				let tween = new TWEEN.Tween(value).to({x: 1}, animationDuration);
				tween.easing(easing);
				this.tweens.push(tween);

				let startPos = this.scene.view.position.clone();
				let targetPos = cameraTargetPosition.clone();
				let startRadius = this.scene.view.radius;
				let targetRadius = cameraTargetPosition.distanceTo(I.location);

				tween.onUpdate(() => {
					let t = value.x;
					this.scene.view.position.x = (1 - t) * startPos.x + t * targetPos.x;
					this.scene.view.position.y = (1 - t) * startPos.y + t * targetPos.y;
					this.scene.view.position.z = (1 - t) * startPos.z + t * targetPos.z;

					this.scene.view.radius = (1 - t) * startRadius + t * targetRadius;
					this.viewer.setMoveSpeed(this.scene.view.radius);
				});

				tween.onComplete(() => {
					this.tweens = this.tweens.filter(e => e !== tween);
				});

				tween.start();
			}
		}

		stopTweens () {
			this.tweens.forEach(e => e.stop());
			this.tweens = [];
		}

		update (delta) {
			let view = this.scene.view;

			{ // apply rotation
				let progression = Math.min(1, this.fadeFactor * delta);

				let yaw = view.yaw;
				let pitch = view.pitch;
				let pivot = view.getPivot();

				yaw -= progression * this.yawDelta;
				pitch -= progression * this.pitchDelta;

				view.yaw = yaw;
				view.pitch = pitch;

				let V = this.scene.view.direction.multiplyScalar(-view.radius);
				let position = new Vector3().addVectors(pivot, V);

				view.position.copy(position);
			}

			{ // apply pan
				let progression = Math.min(1, this.fadeFactor * delta);
				let panDistance = progression * view.radius * 3;

				let px = -this.panDelta.x * panDistance;
				let py = this.panDelta.y * panDistance;

				view.pan(px, py);
			}

			{ // apply zoom
				let progression = Math.min(1, this.fadeFactor * delta);

				// let radius = view.radius + progression * this.radiusDelta * view.radius * 0.1;
				let radius = view.radius + progression * this.radiusDelta;

				let V = view.direction.multiplyScalar(-radius);
				let position = new Vector3().addVectors(view.getPivot(), V);
				view.radius = radius;

				view.position.copy(position);
			}

			{
				let speed = view.radius;
				this.viewer.setMoveSpeed(speed);
			}

			{ // decelerate over time
				let progression = Math.min(1, this.fadeFactor * delta);
				let attenuation = Math.max(0, 1 - this.fadeFactor * delta);

				this.yawDelta *= attenuation;
				this.pitchDelta *= attenuation;
				this.panDelta.multiplyScalar(attenuation);
				// this.radiusDelta *= attenuation;
				this.radiusDelta -= progression * this.radiusDelta;
			}
		}
	};

	/**
	 * @author mschuetz / http://mschuetz.at
	 *
	 * adapted from THREE.OrbitControls by
	 *
	 * @author qiao / https://github.com/qiao
	 * @author mrdoob / http://mrdoob.com
	 * @author alteredq / http://alteredqualia.com/
	 * @author WestLangley / http://github.com/WestLangley
	 * @author erich666 / http://erichaines.com
	 *
	 *
	 *
	 */


	class FirstPersonControls extends EventDispatcher {
		constructor (viewer) {
			super();

			this.viewer = viewer;
			this.renderer = viewer.renderer;

			this.scene = null;
			this.sceneControls = new Scene();

			this.rotationSpeed = 200;
			this.moveSpeed = 10;
			this.lockElevation = false;

			this.keys = {
				FORWARD: ['W'.charCodeAt(0), 38],
				BACKWARD: ['S'.charCodeAt(0), 40],
				LEFT: ['A'.charCodeAt(0), 37],
				RIGHT: ['D'.charCodeAt(0), 39],
				UP: ['R'.charCodeAt(0), 33],
				DOWN: ['F'.charCodeAt(0), 34]
			};

			this.fadeFactor = 50;
			this.yawDelta = 0;
			this.pitchDelta = 0;
			this.translationDelta = new Vector3(0, 0, 0);
			this.translationWorldDelta = new Vector3(0, 0, 0);

			this.tweens = [];

			let drag = (e) => {
				if (e.drag.object !== null) {
					return;
				}

				if (e.drag.startHandled === undefined) {
					e.drag.startHandled = true;

					this.dispatchEvent({type: 'start'});
				}

				let moveSpeed = this.viewer.getMoveSpeed();

				let ndrag = {
					x: e.drag.lastDrag.x / this.renderer.domElement.clientWidth,
					y: e.drag.lastDrag.y / this.renderer.domElement.clientHeight
				};

				if (e.drag.mouse === MOUSE$1.LEFT) {
					this.yawDelta += ndrag.x * this.rotationSpeed;
					this.pitchDelta += ndrag.y * this.rotationSpeed;
				} else if (e.drag.mouse === MOUSE$1.RIGHT) {
					this.translationDelta.x -= ndrag.x * moveSpeed * 100;
					this.translationDelta.z += ndrag.y * moveSpeed * 100;
				}
			};

			let drop = e => {
				this.dispatchEvent({type: 'end'});
			};

			let scroll = (e) => {
				let speed = this.viewer.getMoveSpeed();

				if (e.delta < 0) {
					speed = speed * 0.9;
				} else if (e.delta > 0) {
					speed = speed / 0.9;
				}

				speed = Math.max(speed, 0.1);

				this.viewer.setMoveSpeed(speed);
			};

			let dblclick = (e) => {
				this.zoomToLocation(e.mouse);
			};

			this.addEventListener('drag', drag);
			this.addEventListener('drop', drop);
			this.addEventListener('mousewheel', scroll);
			this.addEventListener('dblclick', dblclick);
		}

		setScene (scene) {
			this.scene = scene;
		}

		stop(){
			this.yawDelta = 0;
			this.pitchDelta = 0;
			this.translationDelta.set(0, 0, 0);
		}
		
		zoomToLocation(mouse){
			let camera = this.scene.getActiveCamera();
			
			let I = Utils.getMousePointCloudIntersection(
				mouse,
				camera,
				this.viewer,
				this.scene.pointclouds);

			if (I === null) {
				return;
			}

			let targetRadius = 0;
			{
				let minimumJumpDistance = 0.2;

				let domElement = this.renderer.domElement;
				let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);

				let nodes = I.pointcloud.nodesOnRay(I.pointcloud.visibleNodes, ray);
				let lastNode = nodes[nodes.length - 1];
				let radius = lastNode.getBoundingSphere(new Sphere()).radius;
				targetRadius = Math.min(this.scene.view.radius, radius);
				targetRadius = Math.max(minimumJumpDistance, targetRadius);
			}

			let d = this.scene.view.direction.multiplyScalar(-1);
			let cameraTargetPosition = new Vector3().addVectors(I.location, d.multiplyScalar(targetRadius));
			// TODO Unused: let controlsTargetPosition = I.location;

			let animationDuration = 600;
			let easing = TWEEN.Easing.Quartic.Out;

			{ // animate
				let value = {x: 0};
				let tween = new TWEEN.Tween(value).to({x: 1}, animationDuration);
				tween.easing(easing);
				this.tweens.push(tween);

				let startPos = this.scene.view.position.clone();
				let targetPos = cameraTargetPosition.clone();
				let startRadius = this.scene.view.radius;
				let targetRadius = cameraTargetPosition.distanceTo(I.location);

				tween.onUpdate(() => {
					let t = value.x;
					this.scene.view.position.x = (1 - t) * startPos.x + t * targetPos.x;
					this.scene.view.position.y = (1 - t) * startPos.y + t * targetPos.y;
					this.scene.view.position.z = (1 - t) * startPos.z + t * targetPos.z;

					this.scene.view.radius = (1 - t) * startRadius + t * targetRadius;
					this.viewer.setMoveSpeed(this.scene.view.radius / 2.5);
				});

				tween.onComplete(() => {
					this.tweens = this.tweens.filter(e => e !== tween);
				});

				tween.start();
			}
		}

		update (delta) {
			let view = this.scene.view;

			{ // cancel move animations on user input
				let changes = [ this.yawDelta,
					this.pitchDelta,
					this.translationDelta.length(),
					this.translationWorldDelta.length() ];
				let changeHappens = changes.some(e => Math.abs(e) > 0.001);
				if (changeHappens && this.tweens.length > 0) {
					this.tweens.forEach(e => e.stop());
					this.tweens = [];
				}
			}

			{ // accelerate while input is given
				let ih = this.viewer.inputHandler;

				let moveForward = this.keys.FORWARD.some(e => ih.pressedKeys[e]);
				let moveBackward = this.keys.BACKWARD.some(e => ih.pressedKeys[e]);
				let moveLeft = this.keys.LEFT.some(e => ih.pressedKeys[e]);
				let moveRight = this.keys.RIGHT.some(e => ih.pressedKeys[e]);
				let moveUp = this.keys.UP.some(e => ih.pressedKeys[e]);
				let moveDown = this.keys.DOWN.some(e => ih.pressedKeys[e]);

				if(this.lockElevation){
					let dir = view.direction;
					dir.z = 0;
					dir.normalize();

					if (moveForward && moveBackward) {
						this.translationWorldDelta.set(0, 0, 0);
					} else if (moveForward) {
						this.translationWorldDelta.copy(dir.multiplyScalar(this.viewer.getMoveSpeed()));
					} else if (moveBackward) {
						this.translationWorldDelta.copy(dir.multiplyScalar(-this.viewer.getMoveSpeed()));
					}
				}else {
					if (moveForward && moveBackward) {
						this.translationDelta.y = 0;
					} else if (moveForward) {
						this.translationDelta.y = this.viewer.getMoveSpeed();
					} else if (moveBackward) {
						this.translationDelta.y = -this.viewer.getMoveSpeed();
					}
				}

				if (moveLeft && moveRight) {
					this.translationDelta.x = 0;
				} else if (moveLeft) {
					this.translationDelta.x = -this.viewer.getMoveSpeed();
				} else if (moveRight) {
					this.translationDelta.x = this.viewer.getMoveSpeed();
				}

				if (moveUp && moveDown) {
					this.translationWorldDelta.z = 0;
				} else if (moveUp) {
					this.translationWorldDelta.z = this.viewer.getMoveSpeed();
				} else if (moveDown) {
					this.translationWorldDelta.z = -this.viewer.getMoveSpeed();
				}
			}

			{ // apply rotation
				let yaw = view.yaw;
				let pitch = view.pitch;

				yaw -= this.yawDelta * delta;
				pitch -= this.pitchDelta * delta;

				view.yaw = yaw;
				view.pitch = pitch;
			}

			{ // apply translation
				view.translate(
					this.translationDelta.x * delta,
					this.translationDelta.y * delta,
					this.translationDelta.z * delta
				);

				view.translateWorld(
					this.translationWorldDelta.x * delta,
					this.translationWorldDelta.y * delta,
					this.translationWorldDelta.z * delta
				);
			}

			{ // set view target according to speed
				view.radius = 3 * this.viewer.getMoveSpeed();
			}

			{ // decelerate over time
				let attenuation = Math.max(0, 1 - this.fadeFactor * delta);
				this.yawDelta *= attenuation;
				this.pitchDelta *= attenuation;
				this.translationDelta.multiplyScalar(attenuation);
				this.translationWorldDelta.multiplyScalar(attenuation);
			}
		}
	};

	class EarthControls extends EventDispatcher {
		constructor (viewer) {
			super(viewer);

			this.viewer = viewer;
			this.renderer = viewer.renderer;

			this.scene = null;
			this.sceneControls = new Scene();

			this.rotationSpeed = 10;

			this.fadeFactor = 20;
			this.wheelDelta = 0;
			this.zoomDelta = new Vector3();
			this.camStart = null;

			this.tweens = [];

			{
				let sg = new SphereGeometry(1, 16, 16);
				let sm = new MeshNormalMaterial();
				this.pivotIndicator = new Mesh(sg, sm);
				this.pivotIndicator.visible = false;
				this.sceneControls.add(this.pivotIndicator);
			}

			let drag = (e) => {
				if (e.drag.object !== null) {
					return;
				}

				if (!this.pivot) {
					return;
				}

				if (e.drag.startHandled === undefined) {
					e.drag.startHandled = true;

					this.dispatchEvent({type: 'start'});
				}

				let camStart = this.camStart;
				let camera = this.scene.getActiveCamera();
				let view = this.viewer.scene.view;

				// let camera = this.viewer.scene.camera;
				let mouse = e.drag.end;
				let domElement = this.viewer.renderer.domElement;

				if (e.drag.mouse === MOUSE$1.LEFT) {

					let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);
					let plane = new Plane().setFromNormalAndCoplanarPoint(
						new Vector3(0, 0, 1),
						this.pivot);

					let distanceToPlane = ray.distanceToPlane(plane);

					if (distanceToPlane > 0) {
						let I = new Vector3().addVectors(
							camStart.position,
							ray.direction.clone().multiplyScalar(distanceToPlane));

						let movedBy = new Vector3().subVectors(
							I, this.pivot);

						let newCamPos = camStart.position.clone().sub(movedBy);

						view.position.copy(newCamPos);

						{
							let distance = newCamPos.distanceTo(this.pivot);
							view.radius = distance;
							let speed = view.radius / 2.5;
							this.viewer.setMoveSpeed(speed);
						}
					}
				} else if (e.drag.mouse === MOUSE$1.RIGHT) {
					let ndrag = {
						x: e.drag.lastDrag.x / this.renderer.domElement.clientWidth,
						y: e.drag.lastDrag.y / this.renderer.domElement.clientHeight
					};

					let yawDelta = -ndrag.x * this.rotationSpeed * 0.5;
					let pitchDelta = -ndrag.y * this.rotationSpeed * 0.2;

					let originalPitch = view.pitch;
					let tmpView = view.clone();
					tmpView.pitch = tmpView.pitch + pitchDelta;
					pitchDelta = tmpView.pitch - originalPitch;

					let pivotToCam = new Vector3().subVectors(view.position, this.pivot);
					let pivotToCamTarget = new Vector3().subVectors(view.getPivot(), this.pivot);
					let side = view.getSide();

					pivotToCam.applyAxisAngle(side, pitchDelta);
					pivotToCamTarget.applyAxisAngle(side, pitchDelta);

					pivotToCam.applyAxisAngle(new Vector3(0, 0, 1), yawDelta);
					pivotToCamTarget.applyAxisAngle(new Vector3(0, 0, 1), yawDelta);

					let newCam = new Vector3().addVectors(this.pivot, pivotToCam);
					// TODO: Unused: let newCamTarget = new THREE.Vector3().addVectors(this.pivot, pivotToCamTarget);

					view.position.copy(newCam);
					view.yaw += yawDelta;
					view.pitch += pitchDelta;
				}
			};

			let onMouseDown = e => {
				let I = Utils.getMousePointCloudIntersection(
					e.mouse, 
					this.scene.getActiveCamera(), 
					this.viewer, 
					this.scene.pointclouds, 
					{pickClipped: false});

				if (I) {
					this.pivot = I.location;
					this.camStart = this.scene.getActiveCamera().clone();
					this.pivotIndicator.visible = true;
					this.pivotIndicator.position.copy(I.location);
				}
			};

			let drop = e => {
				this.dispatchEvent({type: 'end'});
			};

			let onMouseUp = e => {
				this.camStart = null;
				this.pivot = null;
				this.pivotIndicator.visible = false;
			};

			let scroll = (e) => {
				this.wheelDelta += e.delta;
			};

			let dblclick = (e) => {
				this.zoomToLocation(e.mouse);
			};

			this.addEventListener('drag', drag);
			this.addEventListener('drop', drop);
			this.addEventListener('mousewheel', scroll);
			this.addEventListener('mousedown', onMouseDown);
			this.addEventListener('mouseup', onMouseUp);
			this.addEventListener('dblclick', dblclick);
		}

		setScene (scene) {
			this.scene = scene;
		}

		stop(){
			this.wheelDelta = 0;
			this.zoomDelta.set(0, 0, 0);
		}
		
		zoomToLocation(mouse){
			let camera = this.scene.getActiveCamera();
			
			let I = Utils.getMousePointCloudIntersection(
				mouse,
				camera,
				this.viewer,
				this.scene.pointclouds);

			if (I === null) {
				return;
			}

			let targetRadius = 0;
			{
				let minimumJumpDistance = 0.2;

				let domElement = this.renderer.domElement;
				let ray = Utils.mouseToRay(mouse, camera, domElement.clientWidth, domElement.clientHeight);

				let nodes = I.pointcloud.nodesOnRay(I.pointcloud.visibleNodes, ray);
				let lastNode = nodes[nodes.length - 1];
				let radius = lastNode.getBoundingSphere(new Sphere()).radius;
				targetRadius = Math.min(this.scene.view.radius, radius);
				targetRadius = Math.max(minimumJumpDistance, targetRadius);
			}

			let d = this.scene.view.direction.multiplyScalar(-1);
			let cameraTargetPosition = new Vector3().addVectors(I.location, d.multiplyScalar(targetRadius));
			// TODO Unused: let controlsTargetPosition = I.location;

			let animationDuration = 600;
			let easing = TWEEN.Easing.Quartic.Out;

			{ // animate
				let value = {x: 0};
				let tween = new TWEEN.Tween(value).to({x: 1}, animationDuration);
				tween.easing(easing);
				this.tweens.push(tween);

				let startPos = this.scene.view.position.clone();
				let targetPos = cameraTargetPosition.clone();
				let startRadius = this.scene.view.radius;
				let targetRadius = cameraTargetPosition.distanceTo(I.location);

				tween.onUpdate(() => {
					let t = value.x;
					this.scene.view.position.x = (1 - t) * startPos.x + t * targetPos.x;
					this.scene.view.position.y = (1 - t) * startPos.y + t * targetPos.y;
					this.scene.view.position.z = (1 - t) * startPos.z + t * targetPos.z;

					this.scene.view.radius = (1 - t) * startRadius + t * targetRadius;
					this.viewer.setMoveSpeed(this.scene.view.radius / 2.5);
				});

				tween.onComplete(() => {
					this.tweens = this.tweens.filter(e => e !== tween);
				});

				tween.start();
			}
		}

		update (delta) {
			let view = this.scene.view;
			let fade = Math.pow(0.5, this.fadeFactor * delta);
			let progression = 1 - fade;
			let camera = this.scene.getActiveCamera();
			
			// compute zoom
			if (this.wheelDelta !== 0) {
				let I = Utils.getMousePointCloudIntersection(
					this.viewer.inputHandler.mouse, 
					this.scene.getActiveCamera(), 
					this.viewer, 
					this.scene.pointclouds);

				if (I) {
					let resolvedPos = new Vector3().addVectors(view.position, this.zoomDelta);
					let distance = I.location.distanceTo(resolvedPos);
					let jumpDistance = distance * 0.2 * this.wheelDelta;
					let targetDir = new Vector3().subVectors(I.location, view.position);
					targetDir.normalize();

					resolvedPos.add(targetDir.multiplyScalar(jumpDistance));
					this.zoomDelta.subVectors(resolvedPos, view.position);

					{
						let distance = resolvedPos.distanceTo(I.location);
						view.radius = distance;
						let speed = view.radius / 2.5;
						this.viewer.setMoveSpeed(speed);
					}
				}
			}

			// apply zoom
			if (this.zoomDelta.length() !== 0) {
				let p = this.zoomDelta.clone().multiplyScalar(progression);

				let newPos = new Vector3().addVectors(view.position, p);
				view.position.copy(newPos);
			}

			if (this.pivotIndicator.visible) {
				let distance = this.pivotIndicator.position.distanceTo(view.position);
				let pixelwidth = this.renderer.domElement.clientwidth;
				let pixelHeight = this.renderer.domElement.clientHeight;
				let pr = Utils.projectedRadius(1, camera, distance, pixelwidth, pixelHeight);
				let scale = (10 / pr);
				this.pivotIndicator.scale.set(scale, scale, scale);
			}

			// decelerate over time
			{
				this.zoomDelta.multiplyScalar(fade);
				this.wheelDelta = 0;
			}
		}
	};

	/**
	 * @author chrisl / Geodan
	 *
	 * adapted from Potree.FirstPersonControls by
	 *
	 * @author mschuetz / http://mschuetz.at
	 *
	 * and THREE.DeviceOrientationControls  by
	 *
	 * @author richt / http://richt.me
	 * @author WestLangley / http://github.com/WestLangley
	 *
	 *
	 *
	 */

	class DeviceOrientationControls extends EventDispatcher{
		constructor(viewer){
			super();

			this.viewer = viewer;
			this.renderer = viewer.renderer;

			this.scene = null;
			this.sceneControls = new Scene();

			this.screenOrientation = window.orientation || 0;

			let deviceOrientationChange = e => {
				this.deviceOrientation = e;
			};

			let screenOrientationChange = e => {
				this.screenOrientation = window.orientation || 0;
			};

			if ('ondeviceorientationabsolute' in window) {
				window.addEventListener('deviceorientationabsolute', deviceOrientationChange);
			} else if ('ondeviceorientation' in window) {
				window.addEventListener('deviceorientation', deviceOrientationChange);
			} else {
				console.warn("No device orientation found.");
			}
			// window.addEventListener('deviceorientation', deviceOrientationChange);
			window.addEventListener('orientationchange', screenOrientationChange);
		}

		setScene (scene) {
			this.scene = scene;
		}

		update (delta) {
			let computeQuaternion = function (alpha, beta, gamma, orient) {
				let quaternion = new Quaternion();

				let zee = new Vector3(0, 0, 1);
				let euler = new Euler();
				let q0 = new Quaternion();

				euler.set(beta, gamma, alpha, 'ZXY');
				quaternion.setFromEuler(euler);
				quaternion.multiply(q0.setFromAxisAngle(zee, -orient));

				return quaternion;
			};

			if (typeof this.deviceOrientation !== 'undefined') {
				let alpha = this.deviceOrientation.alpha ? MathUtils.degToRad(this.deviceOrientation.alpha) : 0;
				let beta = this.deviceOrientation.beta ? MathUtils.degToRad(this.deviceOrientation.beta) : 0;
				let gamma = this.deviceOrientation.gamma ? MathUtils.degToRad(this.deviceOrientation.gamma) : 0;
				let orient = this.screenOrientation ? MathUtils.degToRad(this.screenOrientation) : 0;

				let quaternion = computeQuaternion(alpha, beta, gamma, orient);
				viewer.scene.cameraP.quaternion.set(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
			}
		}
	};

	var GLTFLoader = ( function () {

		function GLTFLoader( manager ) {

			Loader.call( this, manager );

			this.dracoLoader = null;
			this.ddsLoader = null;
			this.ktx2Loader = null;
			this.meshoptDecoder = null;

			this.pluginCallbacks = [];

			this.register( function ( parser ) {

				return new GLTFMaterialsClearcoatExtension( parser );

			} );

			this.register( function ( parser ) {

				return new GLTFTextureBasisUExtension( parser );

			} );

			this.register( function ( parser ) {

				return new GLTFTextureWebPExtension( parser );

			} );

			this.register( function ( parser ) {

				return new GLTFMaterialsTransmissionExtension( parser );

			} );

			this.register( function ( parser ) {

				return new GLTFLightsExtension( parser );

			} );

			this.register( function ( parser ) {

				return new GLTFMeshoptCompression( parser );

			} );

		}

		GLTFLoader.prototype = Object.assign( Object.create( Loader.prototype ), {

			constructor: GLTFLoader,

			load: function ( url, onLoad, onProgress, onError ) {

				var scope = this;

				var resourcePath;

				if ( this.resourcePath !== '' ) {

					resourcePath = this.resourcePath;

				} else if ( this.path !== '' ) {

					resourcePath = this.path;

				} else {

					resourcePath = LoaderUtils.extractUrlBase( url );

				}

				// Tells the LoadingManager to track an extra item, which resolves after
				// the model is fully loaded. This means the count of items loaded will
				// be incorrect, but ensures manager.onLoad() does not fire early.
				this.manager.itemStart( url );

				var _onError = function ( e ) {

					if ( onError ) {

						onError( e );

					} else {

						console.error( e );

					}

					scope.manager.itemError( url );
					scope.manager.itemEnd( url );

				};

				var loader = new FileLoader( this.manager );

				loader.setPath( this.path );
				loader.setResponseType( 'arraybuffer' );
				loader.setRequestHeader( this.requestHeader );
				loader.setWithCredentials( this.withCredentials );

				loader.load( url, function ( data ) {

					try {

						scope.parse( data, resourcePath, function ( gltf ) {

							onLoad( gltf );

							scope.manager.itemEnd( url );

						}, _onError );

					} catch ( e ) {

						_onError( e );

					}

				}, onProgress, _onError );

			},

			setDRACOLoader: function ( dracoLoader ) {

				this.dracoLoader = dracoLoader;
				return this;

			},

			setDDSLoader: function ( ddsLoader ) {

				this.ddsLoader = ddsLoader;
				return this;

			},

			setKTX2Loader: function ( ktx2Loader ) {

				this.ktx2Loader = ktx2Loader;
				return this;

			},

			setMeshoptDecoder: function ( meshoptDecoder ) {

				this.meshoptDecoder = meshoptDecoder;
				return this;

			},

			register: function ( callback ) {

				if ( this.pluginCallbacks.indexOf( callback ) === - 1 ) {

					this.pluginCallbacks.push( callback );

				}

				return this;

			},

			unregister: function ( callback ) {

				if ( this.pluginCallbacks.indexOf( callback ) !== - 1 ) {

					this.pluginCallbacks.splice( this.pluginCallbacks.indexOf( callback ), 1 );

				}

				return this;

			},

			parse: function ( data, path, onLoad, onError ) {

				var content;
				var extensions = {};
				var plugins = {};

				if ( typeof data === 'string' ) {

					content = data;

				} else {

					var magic = LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );

					if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) {

						try {

							extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data );

						} catch ( error ) {

							if ( onError ) onError( error );
							return;

						}

						content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content;

					} else {

						content = LoaderUtils.decodeText( new Uint8Array( data ) );

					}

				}

				var json = JSON.parse( content );

				if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) {

					if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported.' ) );
					return;

				}

				var parser = new GLTFParser( json, {

					path: path || this.resourcePath || '',
					crossOrigin: this.crossOrigin,
					manager: this.manager,
					ktx2Loader: this.ktx2Loader,
					meshoptDecoder: this.meshoptDecoder

				} );

				parser.fileLoader.setRequestHeader( this.requestHeader );

				for ( var i = 0; i < this.pluginCallbacks.length; i ++ ) {

					var plugin = this.pluginCallbacks[ i ]( parser );
					plugins[ plugin.name ] = plugin;

					// Workaround to avoid determining as unknown extension
					// in addUnknownExtensionsToUserData().
					// Remove this workaround if we move all the existing
					// extension handlers to plugin system
					extensions[ plugin.name ] = true;

				}

				if ( json.extensionsUsed ) {

					for ( var i = 0; i < json.extensionsUsed.length; ++ i ) {

						var extensionName = json.extensionsUsed[ i ];
						var extensionsRequired = json.extensionsRequired || [];

						switch ( extensionName ) {

							case EXTENSIONS.KHR_MATERIALS_UNLIT:
								extensions[ extensionName ] = new GLTFMaterialsUnlitExtension();
								break;

							case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS:
								extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension();
								break;

							case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION:
								extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader );
								break;

							case EXTENSIONS.MSFT_TEXTURE_DDS:
								extensions[ extensionName ] = new GLTFTextureDDSExtension( this.ddsLoader );
								break;

							case EXTENSIONS.KHR_TEXTURE_TRANSFORM:
								extensions[ extensionName ] = new GLTFTextureTransformExtension();
								break;

							case EXTENSIONS.KHR_MESH_QUANTIZATION:
								extensions[ extensionName ] = new GLTFMeshQuantizationExtension();
								break;

							default:

								if ( extensionsRequired.indexOf( extensionName ) >= 0 && plugins[ extensionName ] === undefined ) {

									console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' );

								}

						}

					}

				}

				parser.setExtensions( extensions );
				parser.setPlugins( plugins );
				parser.parse( onLoad, onError );

			}

		} );

		/* GLTFREGISTRY */

		function GLTFRegistry() {

			var objects = {};

			return	{

				get: function ( key ) {

					return objects[ key ];

				},

				add: function ( key, object ) {

					objects[ key ] = object;

				},

				remove: function ( key ) {

					delete objects[ key ];

				},

				removeAll: function () {

					objects = {};

				}

			};

		}

		/*********************************/
		/********** EXTENSIONS ***********/
		/*********************************/

		var EXTENSIONS = {
			KHR_BINARY_GLTF: 'KHR_binary_glTF',
			KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression',
			KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual',
			KHR_MATERIALS_CLEARCOAT: 'KHR_materials_clearcoat',
			KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness',
			KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission',
			KHR_MATERIALS_UNLIT: 'KHR_materials_unlit',
			KHR_TEXTURE_BASISU: 'KHR_texture_basisu',
			KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform',
			KHR_MESH_QUANTIZATION: 'KHR_mesh_quantization',
			EXT_TEXTURE_WEBP: 'EXT_texture_webp',
			EXT_MESHOPT_COMPRESSION: 'EXT_meshopt_compression',
			MSFT_TEXTURE_DDS: 'MSFT_texture_dds'
		};

		/**
		 * DDS Texture Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds
		 *
		 */
		function GLTFTextureDDSExtension( ddsLoader ) {

			if ( ! ddsLoader ) {

				throw new Error( 'THREE.GLTFLoader: Attempting to load .dds texture without importing DDSLoader' );

			}

			this.name = EXTENSIONS.MSFT_TEXTURE_DDS;
			this.ddsLoader = ddsLoader;

		}

		/**
		 * Punctual Lights Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual
		 */
		function GLTFLightsExtension( parser ) {

			this.parser = parser;
			this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL;

			// Object3D instance caches
			this.cache = { refs: {}, uses: {} };

		}

		GLTFLightsExtension.prototype._markDefs = function () {

			var parser = this.parser;
			var nodeDefs = this.parser.json.nodes || [];

			for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) {

				var nodeDef = nodeDefs[ nodeIndex ];

				if ( nodeDef.extensions
					&& nodeDef.extensions[ this.name ]
					&& nodeDef.extensions[ this.name ].light !== undefined ) {

					parser._addNodeRef( this.cache, nodeDef.extensions[ this.name ].light );

				}

			}

		};

		GLTFLightsExtension.prototype._loadLight = function ( lightIndex ) {

			var parser = this.parser;
			var cacheKey = 'light:' + lightIndex;
			var dependency = parser.cache.get( cacheKey );

			if ( dependency ) return dependency;

			var json = parser.json;
			var extensions = ( json.extensions && json.extensions[ this.name ] ) || {};
			var lightDefs = extensions.lights || [];
			var lightDef = lightDefs[ lightIndex ];
			var lightNode;

			var color = new Color( 0xffffff );

			if ( lightDef.color !== undefined ) color.fromArray( lightDef.color );

			var range = lightDef.range !== undefined ? lightDef.range : 0;

			switch ( lightDef.type ) {

				case 'directional':
					lightNode = new DirectionalLight( color );
					lightNode.target.position.set( 0, 0, - 1 );
					lightNode.add( lightNode.target );
					break;

				case 'point':
					lightNode = new PointLight( color );
					lightNode.distance = range;
					break;

				case 'spot':
					lightNode = new SpotLight( color );
					lightNode.distance = range;
					// Handle spotlight properties.
					lightDef.spot = lightDef.spot || {};
					lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0;
					lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0;
					lightNode.angle = lightDef.spot.outerConeAngle;
					lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle;
					lightNode.target.position.set( 0, 0, - 1 );
					lightNode.add( lightNode.target );
					break;

				default:
					throw new Error( 'THREE.GLTFLoader: Unexpected light type: ' + lightDef.type );

			}

			// Some lights (e.g. spot) default to a position other than the origin. Reset the position
			// here, because node-level parsing will only override position if explicitly specified.
			lightNode.position.set( 0, 0, 0 );

			lightNode.decay = 2;

			if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity;

			lightNode.name = parser.createUniqueName( lightDef.name || ( 'light_' + lightIndex ) );

			dependency = Promise.resolve( lightNode );

			parser.cache.add( cacheKey, dependency );

			return dependency;

		};

		GLTFLightsExtension.prototype.createNodeAttachment = function ( nodeIndex ) {

			var self = this;
			var parser = this.parser;
			var json = parser.json;
			var nodeDef = json.nodes[ nodeIndex ];
			var lightDef = ( nodeDef.extensions && nodeDef.extensions[ this.name ] ) || {};
			var lightIndex = lightDef.light;

			if ( lightIndex === undefined ) return null;

			return this._loadLight( lightIndex ).then( function ( light ) {

				return parser._getNodeRef( self.cache, lightIndex, light );

			} );

		};

		/**
		 * Unlit Materials Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit
		 */
		function GLTFMaterialsUnlitExtension() {

			this.name = EXTENSIONS.KHR_MATERIALS_UNLIT;

		}

		GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () {

			return MeshBasicMaterial;

		};

		GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, materialDef, parser ) {

			var pending = [];

			materialParams.color = new Color( 1.0, 1.0, 1.0 );
			materialParams.opacity = 1.0;

			var metallicRoughness = materialDef.pbrMetallicRoughness;

			if ( metallicRoughness ) {

				if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {

					var array = metallicRoughness.baseColorFactor;

					materialParams.color.fromArray( array );
					materialParams.opacity = array[ 3 ];

				}

				if ( metallicRoughness.baseColorTexture !== undefined ) {

					pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) );

				}

			}

			return Promise.all( pending );

		};

		/**
		 * Clearcoat Materials Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_clearcoat
		 */
		function GLTFMaterialsClearcoatExtension( parser ) {

			this.parser = parser;
			this.name = EXTENSIONS.KHR_MATERIALS_CLEARCOAT;

		}

		GLTFMaterialsClearcoatExtension.prototype.getMaterialType = function ( materialIndex ) {

			var parser = this.parser;
			var materialDef = parser.json.materials[ materialIndex ];

			if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;

			return MeshPhysicalMaterial;

		};

		GLTFMaterialsClearcoatExtension.prototype.extendMaterialParams = function ( materialIndex, materialParams ) {

			var parser = this.parser;
			var materialDef = parser.json.materials[ materialIndex ];

			if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {

				return Promise.resolve();

			}

			var pending = [];

			var extension = materialDef.extensions[ this.name ];

			if ( extension.clearcoatFactor !== undefined ) {

				materialParams.clearcoat = extension.clearcoatFactor;

			}

			if ( extension.clearcoatTexture !== undefined ) {

				pending.push( parser.assignTexture( materialParams, 'clearcoatMap', extension.clearcoatTexture ) );

			}

			if ( extension.clearcoatRoughnessFactor !== undefined ) {

				materialParams.clearcoatRoughness = extension.clearcoatRoughnessFactor;

			}

			if ( extension.clearcoatRoughnessTexture !== undefined ) {

				pending.push( parser.assignTexture( materialParams, 'clearcoatRoughnessMap', extension.clearcoatRoughnessTexture ) );

			}

			if ( extension.clearcoatNormalTexture !== undefined ) {

				pending.push( parser.assignTexture( materialParams, 'clearcoatNormalMap', extension.clearcoatNormalTexture ) );

				if ( extension.clearcoatNormalTexture.scale !== undefined ) {

					var scale = extension.clearcoatNormalTexture.scale;

					materialParams.clearcoatNormalScale = new Vector2( scale, scale );

				}

			}

			return Promise.all( pending );

		};

		/**
		 * Transmission Materials Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_transmission
		 * Draft: https://github.com/KhronosGroup/glTF/pull/1698
		 */
		function GLTFMaterialsTransmissionExtension( parser ) {

			this.parser = parser;
			this.name = EXTENSIONS.KHR_MATERIALS_TRANSMISSION;

		}

		GLTFMaterialsTransmissionExtension.prototype.getMaterialType = function ( materialIndex ) {

			var parser = this.parser;
			var materialDef = parser.json.materials[ materialIndex ];

			if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;

			return MeshPhysicalMaterial;

		};

		GLTFMaterialsTransmissionExtension.prototype.extendMaterialParams = function ( materialIndex, materialParams ) {

			var parser = this.parser;
			var materialDef = parser.json.materials[ materialIndex ];

			if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {

				return Promise.resolve();

			}

			var pending = [];

			var extension = materialDef.extensions[ this.name ];

			if ( extension.transmissionFactor !== undefined ) {

				materialParams.transmission = extension.transmissionFactor;

			}

			if ( extension.transmissionTexture !== undefined ) {

				pending.push( parser.assignTexture( materialParams, 'transmissionMap', extension.transmissionTexture ) );

			}

			return Promise.all( pending );

		};

		/**
		 * BasisU Texture Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_basisu
		 */
		function GLTFTextureBasisUExtension( parser ) {

			this.parser = parser;
			this.name = EXTENSIONS.KHR_TEXTURE_BASISU;

		}

		GLTFTextureBasisUExtension.prototype.loadTexture = function ( textureIndex ) {

			var parser = this.parser;
			var json = parser.json;

			var textureDef = json.textures[ textureIndex ];

			if ( ! textureDef.extensions || ! textureDef.extensions[ this.name ] ) {

				return null;

			}

			var extension = textureDef.extensions[ this.name ];
			var source = json.images[ extension.source ];
			var loader = parser.options.ktx2Loader;

			if ( ! loader ) {

				if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) {

					throw new Error( 'THREE.GLTFLoader: setKTX2Loader must be called before loading KTX2 textures' );

				} else {

					// Assumes that the extension is optional and that a fallback texture is present
					return null;

				}

			}

			return parser.loadTextureImage( textureIndex, source, loader );

		};

		/**
		 * WebP Texture Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_texture_webp
		 */
		function GLTFTextureWebPExtension( parser ) {

			this.parser = parser;
			this.name = EXTENSIONS.EXT_TEXTURE_WEBP;
			this.isSupported = null;

		}

		GLTFTextureWebPExtension.prototype.loadTexture = function ( textureIndex ) {

			var name = this.name;
			var parser = this.parser;
			var json = parser.json;

			var textureDef = json.textures[ textureIndex ];

			if ( ! textureDef.extensions || ! textureDef.extensions[ name ] ) {

				return null;

			}

			var extension = textureDef.extensions[ name ];
			var source = json.images[ extension.source ];
			var loader = source.uri ? parser.options.manager.getHandler( source.uri ) : parser.textureLoader;

			return this.detectSupport().then( function ( isSupported ) {

				if ( isSupported ) return parser.loadTextureImage( textureIndex, source, loader );

				if ( json.extensionsRequired && json.extensionsRequired.indexOf( name ) >= 0 ) {

					throw new Error( 'THREE.GLTFLoader: WebP required by asset but unsupported.' );

				}

				// Fall back to PNG or JPEG.
				return parser.loadTexture( textureIndex );

			} );

		};

		GLTFTextureWebPExtension.prototype.detectSupport = function () {

			if ( ! this.isSupported ) {

				this.isSupported = new Promise( function ( resolve ) {

					var image = new Image();

					// Lossy test image. Support for lossy images doesn't guarantee support for all
					// WebP images, unfortunately.
					image.src = 'data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA';

					image.onload = image.onerror = function () {

						resolve( image.height === 1 );

					};

				} );

			}

			return this.isSupported;

		};

		/**
		* meshopt BufferView Compression Extension
		*
		* Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/EXT_meshopt_compression
		*/
		function GLTFMeshoptCompression( parser ) {

			this.name = EXTENSIONS.EXT_MESHOPT_COMPRESSION;
			this.parser = parser;

		}

		GLTFMeshoptCompression.prototype.loadBufferView = function ( index ) {

			var json = this.parser.json;
			var bufferView = json.bufferViews[ index ];

			if ( bufferView.extensions && bufferView.extensions[ this.name ] ) {

				var extensionDef = bufferView.extensions[ this.name ];

				var buffer = this.parser.getDependency( 'buffer', extensionDef.buffer );
				var decoder = this.parser.options.meshoptDecoder;

				if ( ! decoder || ! decoder.supported ) {

					if ( json.extensionsRequired && json.extensionsRequired.indexOf( this.name ) >= 0 ) {

						throw new Error( 'THREE.GLTFLoader: setMeshoptDecoder must be called before loading compressed files' );

					} else {

						// Assumes that the extension is optional and that fallback buffer data is present
						return null;

					}

				}

				return Promise.all( [ buffer, decoder.ready ] ).then( function ( res ) {

					var byteOffset = extensionDef.byteOffset || 0;
					var byteLength = extensionDef.byteLength || 0;

					var count = extensionDef.count;
					var stride = extensionDef.byteStride;

					var result = new ArrayBuffer( count * stride );
					var source = new Uint8Array( res[ 0 ], byteOffset, byteLength );

					decoder.decodeGltfBuffer( new Uint8Array( result ), count, stride, source, extensionDef.mode, extensionDef.filter );
					return result;

				} );

			} else {

				return null;

			}

		};

		/* BINARY EXTENSION */
		var BINARY_EXTENSION_HEADER_MAGIC = 'glTF';
		var BINARY_EXTENSION_HEADER_LENGTH = 12;
		var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 };

		function GLTFBinaryExtension( data ) {

			this.name = EXTENSIONS.KHR_BINARY_GLTF;
			this.content = null;
			this.body = null;

			var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH );

			this.header = {
				magic: LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ),
				version: headerView.getUint32( 4, true ),
				length: headerView.getUint32( 8, true )
			};

			if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) {

				throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' );

			} else if ( this.header.version < 2.0 ) {

				throw new Error( 'THREE.GLTFLoader: Legacy binary file detected.' );

			}

			var chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH );
			var chunkIndex = 0;

			while ( chunkIndex < chunkView.byteLength ) {

				var chunkLength = chunkView.getUint32( chunkIndex, true );
				chunkIndex += 4;

				var chunkType = chunkView.getUint32( chunkIndex, true );
				chunkIndex += 4;

				if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) {

					var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength );
					this.content = LoaderUtils.decodeText( contentArray );

				} else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) {

					var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex;
					this.body = data.slice( byteOffset, byteOffset + chunkLength );

				}

				// Clients must ignore chunks with unknown types.

				chunkIndex += chunkLength;

			}

			if ( this.content === null ) {

				throw new Error( 'THREE.GLTFLoader: JSON content not found.' );

			}

		}

		/**
		 * DRACO Mesh Compression Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_draco_mesh_compression
		 */
		function GLTFDracoMeshCompressionExtension( json, dracoLoader ) {

			if ( ! dracoLoader ) {

				throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' );

			}

			this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION;
			this.json = json;
			this.dracoLoader = dracoLoader;
			this.dracoLoader.preload();

		}

		GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) {

			var json = this.json;
			var dracoLoader = this.dracoLoader;
			var bufferViewIndex = primitive.extensions[ this.name ].bufferView;
			var gltfAttributeMap = primitive.extensions[ this.name ].attributes;
			var threeAttributeMap = {};
			var attributeNormalizedMap = {};
			var attributeTypeMap = {};

			for ( var attributeName in gltfAttributeMap ) {

				var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase();

				threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ];

			}

			for ( attributeName in primitive.attributes ) {

				var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase();

				if ( gltfAttributeMap[ attributeName ] !== undefined ) {

					var accessorDef = json.accessors[ primitive.attributes[ attributeName ] ];
					var componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ];

					attributeTypeMap[ threeAttributeName ] = componentType;
					attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true;

				}

			}

			return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) {

				return new Promise( function ( resolve ) {

					dracoLoader.decodeDracoFile( bufferView, function ( geometry ) {

						for ( var attributeName in geometry.attributes ) {

							var attribute = geometry.attributes[ attributeName ];
							var normalized = attributeNormalizedMap[ attributeName ];

							if ( normalized !== undefined ) attribute.normalized = normalized;

						}

						resolve( geometry );

					}, threeAttributeMap, attributeTypeMap );

				} );

			} );

		};

		/**
		 * Texture Transform Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform
		 */
		function GLTFTextureTransformExtension() {

			this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM;

		}

		GLTFTextureTransformExtension.prototype.extendTexture = function ( texture, transform ) {

			texture = texture.clone();

			if ( transform.offset !== undefined ) {

				texture.offset.fromArray( transform.offset );

			}

			if ( transform.rotation !== undefined ) {

				texture.rotation = transform.rotation;

			}

			if ( transform.scale !== undefined ) {

				texture.repeat.fromArray( transform.scale );

			}

			if ( transform.texCoord !== undefined ) {

				console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' );

			}

			texture.needsUpdate = true;

			return texture;

		};

		/**
		 * Specular-Glossiness Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness
		 */

		/**
		 * A sub class of StandardMaterial with some of the functionality
		 * changed via the `onBeforeCompile` callback
		 * @pailhead
		 */

		function GLTFMeshStandardSGMaterial( params ) {

			MeshStandardMaterial.call( this );

			this.isGLTFSpecularGlossinessMaterial = true;

			//various chunks that need replacing
			var specularMapParsFragmentChunk = [
				'#ifdef USE_SPECULARMAP',
				'	uniform sampler2D specularMap;',
				'#endif'
			].join( '\n' );

			var glossinessMapParsFragmentChunk = [
				'#ifdef USE_GLOSSINESSMAP',
				'	uniform sampler2D glossinessMap;',
				'#endif'
			].join( '\n' );

			var specularMapFragmentChunk = [
				'vec3 specularFactor = specular;',
				'#ifdef USE_SPECULARMAP',
				'	vec4 texelSpecular = texture2D( specularMap, vUv );',
				'	texelSpecular = sRGBToLinear( texelSpecular );',
				'	// reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture',
				'	specularFactor *= texelSpecular.rgb;',
				'#endif'
			].join( '\n' );

			var glossinessMapFragmentChunk = [
				'float glossinessFactor = glossiness;',
				'#ifdef USE_GLOSSINESSMAP',
				'	vec4 texelGlossiness = texture2D( glossinessMap, vUv );',
				'	// reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture',
				'	glossinessFactor *= texelGlossiness.a;',
				'#endif'
			].join( '\n' );

			var lightPhysicalFragmentChunk = [
				'PhysicalMaterial material;',
				'material.diffuseColor = diffuseColor.rgb * ( 1. - max( specularFactor.r, max( specularFactor.g, specularFactor.b ) ) );',
				'vec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );',
				'float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );',
				'material.specularRoughness = max( 1.0 - glossinessFactor, 0.0525 ); // 0.0525 corresponds to the base mip of a 256 cubemap.',
				'material.specularRoughness += geometryRoughness;',
				'material.specularRoughness = min( material.specularRoughness, 1.0 );',
				'material.specularColor = specularFactor;',
			].join( '\n' );

			var uniforms = {
				specular: { value: new Color().setHex( 0xffffff ) },
				glossiness: { value: 1 },
				specularMap: { value: null },
				glossinessMap: { value: null }
			};

			this._extraUniforms = uniforms;

			this.onBeforeCompile = function ( shader ) {

				for ( var uniformName in uniforms ) {

					shader.uniforms[ uniformName ] = uniforms[ uniformName ];

				}

				shader.fragmentShader = shader.fragmentShader
					.replace( 'uniform float roughness;', 'uniform vec3 specular;' )
					.replace( 'uniform float metalness;', 'uniform float glossiness;' )
					.replace( '#include <roughnessmap_pars_fragment>', specularMapParsFragmentChunk )
					.replace( '#include <metalnessmap_pars_fragment>', glossinessMapParsFragmentChunk )
					.replace( '#include <roughnessmap_fragment>', specularMapFragmentChunk )
					.replace( '#include <metalnessmap_fragment>', glossinessMapFragmentChunk )
					.replace( '#include <lights_physical_fragment>', lightPhysicalFragmentChunk );

			};

			Object.defineProperties( this, {

				specular: {
					get: function () {

						return uniforms.specular.value;

					},
					set: function ( v ) {

						uniforms.specular.value = v;

					}
				},

				specularMap: {
					get: function () {

						return uniforms.specularMap.value;

					},
					set: function ( v ) {

						uniforms.specularMap.value = v;

						if ( v ) {

							this.defines.USE_SPECULARMAP = ''; // USE_UV is set by the renderer for specular maps

						} else {

							delete this.defines.USE_SPECULARMAP;

						}

					}
				},

				glossiness: {
					get: function () {

						return uniforms.glossiness.value;

					},
					set: function ( v ) {

						uniforms.glossiness.value = v;

					}
				},

				glossinessMap: {
					get: function () {

						return uniforms.glossinessMap.value;

					},
					set: function ( v ) {

						uniforms.glossinessMap.value = v;

						if ( v ) {

							this.defines.USE_GLOSSINESSMAP = '';
							this.defines.USE_UV = '';

						} else {

							delete this.defines.USE_GLOSSINESSMAP;
							delete this.defines.USE_UV;

						}

					}
				}

			} );

			delete this.metalness;
			delete this.roughness;
			delete this.metalnessMap;
			delete this.roughnessMap;

			this.setValues( params );

		}

		GLTFMeshStandardSGMaterial.prototype = Object.create( MeshStandardMaterial.prototype );
		GLTFMeshStandardSGMaterial.prototype.constructor = GLTFMeshStandardSGMaterial;

		GLTFMeshStandardSGMaterial.prototype.copy = function ( source ) {

			MeshStandardMaterial.prototype.copy.call( this, source );
			this.specularMap = source.specularMap;
			this.specular.copy( source.specular );
			this.glossinessMap = source.glossinessMap;
			this.glossiness = source.glossiness;
			delete this.metalness;
			delete this.roughness;
			delete this.metalnessMap;
			delete this.roughnessMap;
			return this;

		};

		function GLTFMaterialsPbrSpecularGlossinessExtension() {

			return {

				name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS,

				specularGlossinessParams: [
					'color',
					'map',
					'lightMap',
					'lightMapIntensity',
					'aoMap',
					'aoMapIntensity',
					'emissive',
					'emissiveIntensity',
					'emissiveMap',
					'bumpMap',
					'bumpScale',
					'normalMap',
					'normalMapType',
					'displacementMap',
					'displacementScale',
					'displacementBias',
					'specularMap',
					'specular',
					'glossinessMap',
					'glossiness',
					'alphaMap',
					'envMap',
					'envMapIntensity',
					'refractionRatio',
				],

				getMaterialType: function () {

					return GLTFMeshStandardSGMaterial;

				},

				extendParams: function ( materialParams, materialDef, parser ) {

					var pbrSpecularGlossiness = materialDef.extensions[ this.name ];

					materialParams.color = new Color( 1.0, 1.0, 1.0 );
					materialParams.opacity = 1.0;

					var pending = [];

					if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) {

						var array = pbrSpecularGlossiness.diffuseFactor;

						materialParams.color.fromArray( array );
						materialParams.opacity = array[ 3 ];

					}

					if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) {

						pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture ) );

					}

					materialParams.emissive = new Color( 0.0, 0.0, 0.0 );
					materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0;
					materialParams.specular = new Color( 1.0, 1.0, 1.0 );

					if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) {

						materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor );

					}

					if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) {

						var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture;
						pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) );
						pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef ) );

					}

					return Promise.all( pending );

				},

				createMaterial: function ( materialParams ) {

					var material = new GLTFMeshStandardSGMaterial( materialParams );
					material.fog = true;

					material.color = materialParams.color;

					material.map = materialParams.map === undefined ? null : materialParams.map;

					material.lightMap = null;
					material.lightMapIntensity = 1.0;

					material.aoMap = materialParams.aoMap === undefined ? null : materialParams.aoMap;
					material.aoMapIntensity = 1.0;

					material.emissive = materialParams.emissive;
					material.emissiveIntensity = 1.0;
					material.emissiveMap = materialParams.emissiveMap === undefined ? null : materialParams.emissiveMap;

					material.bumpMap = materialParams.bumpMap === undefined ? null : materialParams.bumpMap;
					material.bumpScale = 1;

					material.normalMap = materialParams.normalMap === undefined ? null : materialParams.normalMap;
					material.normalMapType = TangentSpaceNormalMap;

					if ( materialParams.normalScale ) material.normalScale = materialParams.normalScale;

					material.displacementMap = null;
					material.displacementScale = 1;
					material.displacementBias = 0;

					material.specularMap = materialParams.specularMap === undefined ? null : materialParams.specularMap;
					material.specular = materialParams.specular;

					material.glossinessMap = materialParams.glossinessMap === undefined ? null : materialParams.glossinessMap;
					material.glossiness = materialParams.glossiness;

					material.alphaMap = null;

					material.envMap = materialParams.envMap === undefined ? null : materialParams.envMap;
					material.envMapIntensity = 1.0;

					material.refractionRatio = 0.98;

					return material;

				},

			};

		}

		/**
		 * Mesh Quantization Extension
		 *
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization
		 */
		function GLTFMeshQuantizationExtension() {

			this.name = EXTENSIONS.KHR_MESH_QUANTIZATION;

		}

		/*********************************/
		/********** INTERPOLATION ********/
		/*********************************/

		// Spline Interpolation
		// Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation
		function GLTFCubicSplineInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) {

			Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer );

		}

		GLTFCubicSplineInterpolant.prototype = Object.create( Interpolant.prototype );
		GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant;

		GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function ( index ) {

			// Copies a sample value to the result buffer. See description of glTF
			// CUBICSPLINE values layout in interpolate_() function below.

			var result = this.resultBuffer,
				values = this.sampleValues,
				valueSize = this.valueSize,
				offset = index * valueSize * 3 + valueSize;

			for ( var i = 0; i !== valueSize; i ++ ) {

				result[ i ] = values[ offset + i ];

			}

			return result;

		};

		GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_;

		GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_;

		GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) {

			var result = this.resultBuffer;
			var values = this.sampleValues;
			var stride = this.valueSize;

			var stride2 = stride * 2;
			var stride3 = stride * 3;

			var td = t1 - t0;

			var p = ( t - t0 ) / td;
			var pp = p * p;
			var ppp = pp * p;

			var offset1 = i1 * stride3;
			var offset0 = offset1 - stride3;

			var s2 = - 2 * ppp + 3 * pp;
			var s3 = ppp - pp;
			var s0 = 1 - s2;
			var s1 = s3 - pp + p;

			// Layout of keyframe output values for CUBICSPLINE animations:
			//   [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ]
			for ( var i = 0; i !== stride; i ++ ) {

				var p0 = values[ offset0 + i + stride ]; // splineVertex_k
				var m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k)
				var p1 = values[ offset1 + i + stride ]; // splineVertex_k+1
				var m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k)

				result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1;

			}

			return result;

		};

		/*********************************/
		/********** INTERNALS ************/
		/*********************************/

		/* CONSTANTS */

		var WEBGL_CONSTANTS = {
			FLOAT: 5126,
			//FLOAT_MAT2: 35674,
			FLOAT_MAT3: 35675,
			FLOAT_MAT4: 35676,
			FLOAT_VEC2: 35664,
			FLOAT_VEC3: 35665,
			FLOAT_VEC4: 35666,
			LINEAR: 9729,
			REPEAT: 10497,
			SAMPLER_2D: 35678,
			POINTS: 0,
			LINES: 1,
			LINE_LOOP: 2,
			LINE_STRIP: 3,
			TRIANGLES: 4,
			TRIANGLE_STRIP: 5,
			TRIANGLE_FAN: 6,
			UNSIGNED_BYTE: 5121,
			UNSIGNED_SHORT: 5123
		};

		var WEBGL_COMPONENT_TYPES = {
			5120: Int8Array,
			5121: Uint8Array,
			5122: Int16Array,
			5123: Uint16Array,
			5125: Uint32Array,
			5126: Float32Array
		};

		var WEBGL_FILTERS = {
			9728: NearestFilter,
			9729: LinearFilter,
			9984: NearestMipmapNearestFilter,
			9985: LinearMipmapNearestFilter,
			9986: NearestMipmapLinearFilter,
			9987: LinearMipmapLinearFilter
		};

		var WEBGL_WRAPPINGS = {
			33071: ClampToEdgeWrapping,
			33648: MirroredRepeatWrapping,
			10497: RepeatWrapping
		};

		var WEBGL_TYPE_SIZES = {
			'SCALAR': 1,
			'VEC2': 2,
			'VEC3': 3,
			'VEC4': 4,
			'MAT2': 4,
			'MAT3': 9,
			'MAT4': 16
		};

		var ATTRIBUTES = {
			POSITION: 'position',
			NORMAL: 'normal',
			TANGENT: 'tangent',
			TEXCOORD_0: 'uv',
			TEXCOORD_1: 'uv2',
			COLOR_0: 'color',
			WEIGHTS_0: 'skinWeight',
			JOINTS_0: 'skinIndex',
		};

		var PATH_PROPERTIES = {
			scale: 'scale',
			translation: 'position',
			rotation: 'quaternion',
			weights: 'morphTargetInfluences'
		};

		var INTERPOLATION = {
			CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each
			                        // keyframe track will be initialized with a default interpolation type, then modified.
			LINEAR: InterpolateLinear,
			STEP: InterpolateDiscrete
		};

		var ALPHA_MODES = {
			OPAQUE: 'OPAQUE',
			MASK: 'MASK',
			BLEND: 'BLEND'
		};

		/* UTILITY FUNCTIONS */

		function resolveURL( url, path ) {

			// Invalid URL
			if ( typeof url !== 'string' || url === '' ) return '';

			// Host Relative URL
			if ( /^https?:\/\//i.test( path ) && /^\//.test( url ) ) {

				path = path.replace( /(^https?:\/\/[^\/]+).*/i, '$1' );

			}

			// Absolute URL http://,https://,//
			if ( /^(https?:)?\/\//i.test( url ) ) return url;

			// Data URI
			if ( /^data:.*,.*$/i.test( url ) ) return url;

			// Blob URL
			if ( /^blob:.*$/i.test( url ) ) return url;

			// Relative URL
			return path + url;

		}

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material
		 */
		function createDefaultMaterial( cache ) {

			if ( cache[ 'DefaultMaterial' ] === undefined ) {

				cache[ 'DefaultMaterial' ] = new MeshStandardMaterial( {
					color: 0xFFFFFF,
					emissive: 0x000000,
					metalness: 1,
					roughness: 1,
					transparent: false,
					depthTest: true,
					side: FrontSide
				} );

			}

			return cache[ 'DefaultMaterial' ];

		}

		function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) {

			// Add unknown glTF extensions to an object's userData.

			for ( var name in objectDef.extensions ) {

				if ( knownExtensions[ name ] === undefined ) {

					object.userData.gltfExtensions = object.userData.gltfExtensions || {};
					object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ];

				}

			}

		}

		/**
		 * @param {Object3D|Material|BufferGeometry} object
		 * @param {GLTF.definition} gltfDef
		 */
		function assignExtrasToUserData( object, gltfDef ) {

			if ( gltfDef.extras !== undefined ) {

				if ( typeof gltfDef.extras === 'object' ) {

					Object.assign( object.userData, gltfDef.extras );

				} else {

					console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras );

				}

			}

		}

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets
		 *
		 * @param {BufferGeometry} geometry
		 * @param {Array<GLTF.Target>} targets
		 * @param {GLTFParser} parser
		 * @return {Promise<BufferGeometry>}
		 */
		function addMorphTargets( geometry, targets, parser ) {

			var hasMorphPosition = false;
			var hasMorphNormal = false;

			for ( var i = 0, il = targets.length; i < il; i ++ ) {

				var target = targets[ i ];

				if ( target.POSITION !== undefined ) hasMorphPosition = true;
				if ( target.NORMAL !== undefined ) hasMorphNormal = true;

				if ( hasMorphPosition && hasMorphNormal ) break;

			}

			if ( ! hasMorphPosition && ! hasMorphNormal ) return Promise.resolve( geometry );

			var pendingPositionAccessors = [];
			var pendingNormalAccessors = [];

			for ( var i = 0, il = targets.length; i < il; i ++ ) {

				var target = targets[ i ];

				if ( hasMorphPosition ) {

					var pendingAccessor = target.POSITION !== undefined
						? parser.getDependency( 'accessor', target.POSITION )
						: geometry.attributes.position;

					pendingPositionAccessors.push( pendingAccessor );

				}

				if ( hasMorphNormal ) {

					var pendingAccessor = target.NORMAL !== undefined
						? parser.getDependency( 'accessor', target.NORMAL )
						: geometry.attributes.normal;

					pendingNormalAccessors.push( pendingAccessor );

				}

			}

			return Promise.all( [
				Promise.all( pendingPositionAccessors ),
				Promise.all( pendingNormalAccessors )
			] ).then( function ( accessors ) {

				var morphPositions = accessors[ 0 ];
				var morphNormals = accessors[ 1 ];

				if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions;
				if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals;
				geometry.morphTargetsRelative = true;

				return geometry;

			} );

		}

		/**
		 * @param {Mesh} mesh
		 * @param {GLTF.Mesh} meshDef
		 */
		function updateMorphTargets( mesh, meshDef ) {

			mesh.updateMorphTargets();

			if ( meshDef.weights !== undefined ) {

				for ( var i = 0, il = meshDef.weights.length; i < il; i ++ ) {

					mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ];

				}

			}

			// .extras has user-defined data, so check that .extras.targetNames is an array.
			if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) {

				var targetNames = meshDef.extras.targetNames;

				if ( mesh.morphTargetInfluences.length === targetNames.length ) {

					mesh.morphTargetDictionary = {};

					for ( var i = 0, il = targetNames.length; i < il; i ++ ) {

						mesh.morphTargetDictionary[ targetNames[ i ] ] = i;

					}

				} else {

					console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' );

				}

			}

		}

		function createPrimitiveKey( primitiveDef ) {

			var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ];
			var geometryKey;

			if ( dracoExtension ) {

				geometryKey = 'draco:' + dracoExtension.bufferView
					+ ':' + dracoExtension.indices
					+ ':' + createAttributesKey( dracoExtension.attributes );

			} else {

				geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode;

			}

			return geometryKey;

		}

		function createAttributesKey( attributes ) {

			var attributesKey = '';

			var keys = Object.keys( attributes ).sort();

			for ( var i = 0, il = keys.length; i < il; i ++ ) {

				attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';';

			}

			return attributesKey;

		}

		/* GLTF PARSER */

		function GLTFParser( json, options ) {

			this.json = json || {};
			this.extensions = {};
			this.plugins = {};
			this.options = options || {};

			// loader object cache
			this.cache = new GLTFRegistry();

			// associations between Three.js objects and glTF elements
			this.associations = new Map();

			// BufferGeometry caching
			this.primitiveCache = {};

			// Object3D instance caches
			this.meshCache = { refs: {}, uses: {} };
			this.cameraCache = { refs: {}, uses: {} };
			this.lightCache = { refs: {}, uses: {} };

			// Track node names, to ensure no duplicates
			this.nodeNamesUsed = {};

			// Use an ImageBitmapLoader if imageBitmaps are supported. Moves much of the
			// expensive work of uploading a texture to the GPU off the main thread.
			if ( typeof createImageBitmap !== 'undefined' && /Firefox/.test( navigator.userAgent ) === false ) {

				this.textureLoader = new ImageBitmapLoader( this.options.manager );

			} else {

				this.textureLoader = new TextureLoader( this.options.manager );

			}

			this.textureLoader.setCrossOrigin( this.options.crossOrigin );

			this.fileLoader = new FileLoader( this.options.manager );
			this.fileLoader.setResponseType( 'arraybuffer' );

			if ( this.options.crossOrigin === 'use-credentials' ) {

				this.fileLoader.setWithCredentials( true );

			}

		}

		GLTFParser.prototype.setExtensions = function ( extensions ) {

			this.extensions = extensions;

		};

		GLTFParser.prototype.setPlugins = function ( plugins ) {

			this.plugins = plugins;

		};

		GLTFParser.prototype.parse = function ( onLoad, onError ) {

			var parser = this;
			var json = this.json;
			var extensions = this.extensions;

			// Clear the loader cache
			this.cache.removeAll();

			// Mark the special nodes/meshes in json for efficient parse
			this._invokeAll( function ( ext ) {

				return ext._markDefs && ext._markDefs();

			} );

			Promise.all( [

				this.getDependencies( 'scene' ),
				this.getDependencies( 'animation' ),
				this.getDependencies( 'camera' ),

			] ).then( function ( dependencies ) {

				var result = {
					scene: dependencies[ 0 ][ json.scene || 0 ],
					scenes: dependencies[ 0 ],
					animations: dependencies[ 1 ],
					cameras: dependencies[ 2 ],
					asset: json.asset,
					parser: parser,
					userData: {}
				};

				addUnknownExtensionsToUserData( extensions, result, json );

				assignExtrasToUserData( result, json );

				onLoad( result );

			} ).catch( onError );

		};

		/**
		 * Marks the special nodes/meshes in json for efficient parse.
		 */
		GLTFParser.prototype._markDefs = function () {

			var nodeDefs = this.json.nodes || [];
			var skinDefs = this.json.skins || [];
			var meshDefs = this.json.meshes || [];

			// Nothing in the node definition indicates whether it is a Bone or an
			// Object3D. Use the skins' joint references to mark bones.
			for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) {

				var joints = skinDefs[ skinIndex ].joints;

				for ( var i = 0, il = joints.length; i < il; i ++ ) {

					nodeDefs[ joints[ i ] ].isBone = true;

				}

			}

			// Iterate over all nodes, marking references to shared resources,
			// as well as skeleton joints.
			for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) {

				var nodeDef = nodeDefs[ nodeIndex ];

				if ( nodeDef.mesh !== undefined ) {

					this._addNodeRef( this.meshCache, nodeDef.mesh );

					// Nothing in the mesh definition indicates whether it is
					// a SkinnedMesh or Mesh. Use the node's mesh reference
					// to mark SkinnedMesh if node has skin.
					if ( nodeDef.skin !== undefined ) {

						meshDefs[ nodeDef.mesh ].isSkinnedMesh = true;

					}

				}

				if ( nodeDef.camera !== undefined ) {

					this._addNodeRef( this.cameraCache, nodeDef.camera );

				}

			}

		};

		/**
		 * Counts references to shared node / Object3D resources. These resources
		 * can be reused, or "instantiated", at multiple nodes in the scene
		 * hierarchy. Mesh, Camera, and Light instances are instantiated and must
		 * be marked. Non-scenegraph resources (like Materials, Geometries, and
		 * Textures) can be reused directly and are not marked here.
		 *
		 * Example: CesiumMilkTruck sample model reuses "Wheel" meshes.
		 */
		GLTFParser.prototype._addNodeRef = function ( cache, index ) {

			if ( index === undefined ) return;

			if ( cache.refs[ index ] === undefined ) {

				cache.refs[ index ] = cache.uses[ index ] = 0;

			}

			cache.refs[ index ] ++;

		};

		/** Returns a reference to a shared resource, cloning it if necessary. */
		GLTFParser.prototype._getNodeRef = function ( cache, index, object ) {

			if ( cache.refs[ index ] <= 1 ) return object;

			var ref = object.clone();

			ref.name += '_instance_' + ( cache.uses[ index ] ++ );

			return ref;

		};

		GLTFParser.prototype._invokeOne = function ( func ) {

			var extensions = Object.values( this.plugins );
			extensions.push( this );

			for ( var i = 0; i < extensions.length; i ++ ) {

				var result = func( extensions[ i ] );

				if ( result ) return result;

			}

		};

		GLTFParser.prototype._invokeAll = function ( func ) {

			var extensions = Object.values( this.plugins );
			extensions.unshift( this );

			var pending = [];

			for ( var i = 0; i < extensions.length; i ++ ) {

				var result = func( extensions[ i ] );

				if ( result ) pending.push( result );

			}

			return pending;

		};

		/**
		 * Requests the specified dependency asynchronously, with caching.
		 * @param {string} type
		 * @param {number} index
		 * @return {Promise<Object3D|Material|THREE.Texture|AnimationClip|ArrayBuffer|Object>}
		 */
		GLTFParser.prototype.getDependency = function ( type, index ) {

			var cacheKey = type + ':' + index;
			var dependency = this.cache.get( cacheKey );

			if ( ! dependency ) {

				switch ( type ) {

					case 'scene':
						dependency = this.loadScene( index );
						break;

					case 'node':
						dependency = this.loadNode( index );
						break;

					case 'mesh':
						dependency = this._invokeOne( function ( ext ) {

							return ext.loadMesh && ext.loadMesh( index );

						} );
						break;

					case 'accessor':
						dependency = this.loadAccessor( index );
						break;

					case 'bufferView':
						dependency = this._invokeOne( function ( ext ) {

							return ext.loadBufferView && ext.loadBufferView( index );

						} );
						break;

					case 'buffer':
						dependency = this.loadBuffer( index );
						break;

					case 'material':
						dependency = this._invokeOne( function ( ext ) {

							return ext.loadMaterial && ext.loadMaterial( index );

						} );
						break;

					case 'texture':
						dependency = this._invokeOne( function ( ext ) {

							return ext.loadTexture && ext.loadTexture( index );

						} );
						break;

					case 'skin':
						dependency = this.loadSkin( index );
						break;

					case 'animation':
						dependency = this.loadAnimation( index );
						break;

					case 'camera':
						dependency = this.loadCamera( index );
						break;

					default:
						throw new Error( 'Unknown type: ' + type );

				}

				this.cache.add( cacheKey, dependency );

			}

			return dependency;

		};

		/**
		 * Requests all dependencies of the specified type asynchronously, with caching.
		 * @param {string} type
		 * @return {Promise<Array<Object>>}
		 */
		GLTFParser.prototype.getDependencies = function ( type ) {

			var dependencies = this.cache.get( type );

			if ( ! dependencies ) {

				var parser = this;
				var defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || [];

				dependencies = Promise.all( defs.map( function ( def, index ) {

					return parser.getDependency( type, index );

				} ) );

				this.cache.add( type, dependencies );

			}

			return dependencies;

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
		 * @param {number} bufferIndex
		 * @return {Promise<ArrayBuffer>}
		 */
		GLTFParser.prototype.loadBuffer = function ( bufferIndex ) {

			var bufferDef = this.json.buffers[ bufferIndex ];
			var loader = this.fileLoader;

			if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) {

				throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' );

			}

			// If present, GLB container is required to be the first buffer.
			if ( bufferDef.uri === undefined && bufferIndex === 0 ) {

				return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body );

			}

			var options = this.options;

			return new Promise( function ( resolve, reject ) {

				loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () {

					reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) );

				} );

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
		 * @param {number} bufferViewIndex
		 * @return {Promise<ArrayBuffer>}
		 */
		GLTFParser.prototype.loadBufferView = function ( bufferViewIndex ) {

			var bufferViewDef = this.json.bufferViews[ bufferViewIndex ];

			return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) {

				var byteLength = bufferViewDef.byteLength || 0;
				var byteOffset = bufferViewDef.byteOffset || 0;
				return buffer.slice( byteOffset, byteOffset + byteLength );

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors
		 * @param {number} accessorIndex
		 * @return {Promise<BufferAttribute|InterleavedBufferAttribute>}
		 */
		GLTFParser.prototype.loadAccessor = function ( accessorIndex ) {

			var parser = this;
			var json = this.json;

			var accessorDef = this.json.accessors[ accessorIndex ];

			if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) {

				// Ignore empty accessors, which may be used to declare runtime
				// information about attributes coming from another source (e.g. Draco
				// compression extension).
				return Promise.resolve( null );

			}

			var pendingBufferViews = [];

			if ( accessorDef.bufferView !== undefined ) {

				pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) );

			} else {

				pendingBufferViews.push( null );

			}

			if ( accessorDef.sparse !== undefined ) {

				pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) );
				pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) );

			}

			return Promise.all( pendingBufferViews ).then( function ( bufferViews ) {

				var bufferView = bufferViews[ 0 ];

				var itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ];
				var TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ];

				// For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12.
				var elementBytes = TypedArray.BYTES_PER_ELEMENT;
				var itemBytes = elementBytes * itemSize;
				var byteOffset = accessorDef.byteOffset || 0;
				var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined;
				var normalized = accessorDef.normalized === true;
				var array, bufferAttribute;

				// The buffer is not interleaved if the stride is the item size in bytes.
				if ( byteStride && byteStride !== itemBytes ) {

					// Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer
					// This makes sure that IBA.count reflects accessor.count properly
					var ibSlice = Math.floor( byteOffset / byteStride );
					var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count;
					var ib = parser.cache.get( ibCacheKey );

					if ( ! ib ) {

						array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes );

						// Integer parameters to IB/IBA are in array elements, not bytes.
						ib = new InterleavedBuffer( array, byteStride / elementBytes );

						parser.cache.add( ibCacheKey, ib );

					}

					bufferAttribute = new InterleavedBufferAttribute( ib, itemSize, ( byteOffset % byteStride ) / elementBytes, normalized );

				} else {

					if ( bufferView === null ) {

						array = new TypedArray( accessorDef.count * itemSize );

					} else {

						array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize );

					}

					bufferAttribute = new BufferAttribute( array, itemSize, normalized );

				}

				// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors
				if ( accessorDef.sparse !== undefined ) {

					var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR;
					var TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ];

					var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0;
					var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0;

					var sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices );
					var sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize );

					if ( bufferView !== null ) {

						// Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes.
						bufferAttribute = new BufferAttribute( bufferAttribute.array.slice(), bufferAttribute.itemSize, bufferAttribute.normalized );

					}

					for ( var i = 0, il = sparseIndices.length; i < il; i ++ ) {

						var index = sparseIndices[ i ];

						bufferAttribute.setX( index, sparseValues[ i * itemSize ] );
						if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] );
						if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] );
						if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] );
						if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' );

					}

				}

				return bufferAttribute;

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures
		 * @param {number} textureIndex
		 * @return {Promise<THREE.Texture>}
		 */
		GLTFParser.prototype.loadTexture = function ( textureIndex ) {

			var parser = this;
			var json = this.json;
			var options = this.options;

			var textureDef = json.textures[ textureIndex ];

			var textureExtensions = textureDef.extensions || {};

			var source;

			if ( textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] ) {

				source = json.images[ textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].source ];

			} else {

				source = json.images[ textureDef.source ];

			}

			var loader;

			if ( source.uri ) {

				loader = options.manager.getHandler( source.uri );

			}

			if ( ! loader ) {

				loader = textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ]
					? parser.extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].ddsLoader
					: this.textureLoader;

			}

			return this.loadTextureImage( textureIndex, source, loader );

		};

		GLTFParser.prototype.loadTextureImage = function ( textureIndex, source, loader ) {

			var parser = this;
			var json = this.json;
			var options = this.options;

			var textureDef = json.textures[ textureIndex ];

			var URL = self.URL || self.webkitURL;

			var sourceURI = source.uri;
			var isObjectURL = false;
			var hasAlpha = true;

			if ( source.mimeType === 'image/jpeg' ) hasAlpha = false;

			if ( source.bufferView !== undefined ) {

				// Load binary image data from bufferView, if provided.

				sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) {

					if ( source.mimeType === 'image/png' ) {

						// Inspect the PNG 'IHDR' chunk to determine whether the image could have an
						// alpha channel. This check is conservative — the image could have an alpha
						// channel with all values == 1, and the indexed type (colorType == 3) only
						// sometimes contains alpha.
						//
						// https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header
						var colorType = new DataView( bufferView, 25, 1 ).getUint8( 0, false );
						hasAlpha = colorType === 6 || colorType === 4 || colorType === 3;

					}

					isObjectURL = true;
					var blob = new Blob( [ bufferView ], { type: source.mimeType } );
					sourceURI = URL.createObjectURL( blob );
					return sourceURI;

				} );

			}

			return Promise.resolve( sourceURI ).then( function ( sourceURI ) {

				return new Promise( function ( resolve, reject ) {

					var onLoad = resolve;

					if ( loader.isImageBitmapLoader === true ) {

						onLoad = function ( imageBitmap ) {

							resolve( new CanvasTexture( imageBitmap ) );

						};

					}

					loader.load( resolveURL( sourceURI, options.path ), onLoad, undefined, reject );

				} );

			} ).then( function ( texture ) {

				// Clean up resources and configure Texture.

				if ( isObjectURL === true ) {

					URL.revokeObjectURL( sourceURI );

				}

				texture.flipY = false;

				if ( textureDef.name ) texture.name = textureDef.name;

				// When there is definitely no alpha channel in the texture, set RGBFormat to save space.
				if ( ! hasAlpha ) texture.format = RGBFormat;

				var samplers = json.samplers || {};
				var sampler = samplers[ textureDef.sampler ] || {};

				texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || LinearFilter;
				texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || LinearMipmapLinearFilter;
				texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || RepeatWrapping;
				texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || RepeatWrapping;

				parser.associations.set( texture, {
					type: 'textures',
					index: textureIndex
				} );

				return texture;

			} );

		};

		/**
		 * Asynchronously assigns a texture to the given material parameters.
		 * @param {Object} materialParams
		 * @param {string} mapName
		 * @param {Object} mapDef
		 * @return {Promise}
		 */
		GLTFParser.prototype.assignTexture = function ( materialParams, mapName, mapDef ) {

			var parser = this;

			return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) {

				// Materials sample aoMap from UV set 1 and other maps from UV set 0 - this can't be configured
				// However, we will copy UV set 0 to UV set 1 on demand for aoMap
				if ( mapDef.texCoord !== undefined && mapDef.texCoord != 0 && ! ( mapName === 'aoMap' && mapDef.texCoord == 1 ) ) {

					console.warn( 'THREE.GLTFLoader: Custom UV set ' + mapDef.texCoord + ' for texture ' + mapName + ' not yet supported.' );

				}

				if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) {

					var transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined;

					if ( transform ) {

						var gltfReference = parser.associations.get( texture );
						texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform );
						parser.associations.set( texture, gltfReference );

					}

				}

				materialParams[ mapName ] = texture;

			} );

		};

		/**
		 * Assigns final material to a Mesh, Line, or Points instance. The instance
		 * already has a material (generated from the glTF material options alone)
		 * but reuse of the same glTF material may require multiple threejs materials
		 * to accomodate different primitive types, defines, etc. New materials will
		 * be created if necessary, and reused from a cache.
		 * @param  {Object3D} mesh Mesh, Line, or Points instance.
		 */
		GLTFParser.prototype.assignFinalMaterial = function ( mesh ) {

			var geometry = mesh.geometry;
			var material = mesh.material;

			var useVertexTangents = geometry.attributes.tangent !== undefined;
			var useVertexColors = geometry.attributes.color !== undefined;
			var useFlatShading = geometry.attributes.normal === undefined;
			var useSkinning = mesh.isSkinnedMesh === true;
			var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0;
			var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined;

			if ( mesh.isPoints ) {

				var cacheKey = 'PointsMaterial:' + material.uuid;

				var pointsMaterial = this.cache.get( cacheKey );

				if ( ! pointsMaterial ) {

					pointsMaterial = new PointsMaterial();
					Material.prototype.copy.call( pointsMaterial, material );
					pointsMaterial.color.copy( material.color );
					pointsMaterial.map = material.map;
					pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px

					this.cache.add( cacheKey, pointsMaterial );

				}

				material = pointsMaterial;

			} else if ( mesh.isLine ) {

				var cacheKey = 'LineBasicMaterial:' + material.uuid;

				var lineMaterial = this.cache.get( cacheKey );

				if ( ! lineMaterial ) {

					lineMaterial = new LineBasicMaterial();
					Material.prototype.copy.call( lineMaterial, material );
					lineMaterial.color.copy( material.color );

					this.cache.add( cacheKey, lineMaterial );

				}

				material = lineMaterial;

			}

			// Clone the material if it will be modified
			if ( useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets ) {

				var cacheKey = 'ClonedMaterial:' + material.uuid + ':';

				if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:';
				if ( useSkinning ) cacheKey += 'skinning:';
				if ( useVertexTangents ) cacheKey += 'vertex-tangents:';
				if ( useVertexColors ) cacheKey += 'vertex-colors:';
				if ( useFlatShading ) cacheKey += 'flat-shading:';
				if ( useMorphTargets ) cacheKey += 'morph-targets:';
				if ( useMorphNormals ) cacheKey += 'morph-normals:';

				var cachedMaterial = this.cache.get( cacheKey );

				if ( ! cachedMaterial ) {

					cachedMaterial = material.clone();

					if ( useSkinning ) cachedMaterial.skinning = true;
					if ( useVertexTangents ) cachedMaterial.vertexTangents = true;
					if ( useVertexColors ) cachedMaterial.vertexColors = true;
					if ( useFlatShading ) cachedMaterial.flatShading = true;
					if ( useMorphTargets ) cachedMaterial.morphTargets = true;
					if ( useMorphNormals ) cachedMaterial.morphNormals = true;

					this.cache.add( cacheKey, cachedMaterial );

					this.associations.set( cachedMaterial, this.associations.get( material ) );

				}

				material = cachedMaterial;

			}

			// workarounds for mesh and geometry

			if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) {

				geometry.setAttribute( 'uv2', geometry.attributes.uv );

			}

			// https://github.com/mrdoob/three.js/issues/11438#issuecomment-507003995
			if ( material.normalScale && ! useVertexTangents ) {

				material.normalScale.y = - material.normalScale.y;

			}

			if ( material.clearcoatNormalScale && ! useVertexTangents ) {

				material.clearcoatNormalScale.y = - material.clearcoatNormalScale.y;

			}

			mesh.material = material;

		};

		GLTFParser.prototype.getMaterialType = function ( /* materialIndex */ ) {

			return MeshStandardMaterial;

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials
		 * @param {number} materialIndex
		 * @return {Promise<Material>}
		 */
		GLTFParser.prototype.loadMaterial = function ( materialIndex ) {

			var parser = this;
			var json = this.json;
			var extensions = this.extensions;
			var materialDef = json.materials[ materialIndex ];

			var materialType;
			var materialParams = {};
			var materialExtensions = materialDef.extensions || {};

			var pending = [];

			if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) {

				var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ];
				materialType = sgExtension.getMaterialType();
				pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) );

			} else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) {

				var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ];
				materialType = kmuExtension.getMaterialType();
				pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) );

			} else {

				// Specification:
				// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material

				var metallicRoughness = materialDef.pbrMetallicRoughness || {};

				materialParams.color = new Color( 1.0, 1.0, 1.0 );
				materialParams.opacity = 1.0;

				if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {

					var array = metallicRoughness.baseColorFactor;

					materialParams.color.fromArray( array );
					materialParams.opacity = array[ 3 ];

				}

				if ( metallicRoughness.baseColorTexture !== undefined ) {

					pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) );

				}

				materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0;
				materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0;

				if ( metallicRoughness.metallicRoughnessTexture !== undefined ) {

					pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) );
					pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) );

				}

				materialType = this._invokeOne( function ( ext ) {

					return ext.getMaterialType && ext.getMaterialType( materialIndex );

				} );

				pending.push( Promise.all( this._invokeAll( function ( ext ) {

					return ext.extendMaterialParams && ext.extendMaterialParams( materialIndex, materialParams );

				} ) ) );

			}

			if ( materialDef.doubleSided === true ) {

				materialParams.side = DoubleSide;

			}

			var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE;

			if ( alphaMode === ALPHA_MODES.BLEND ) {

				materialParams.transparent = true;

				// See: https://github.com/mrdoob/three.js/issues/17706
				materialParams.depthWrite = false;

			} else {

				materialParams.transparent = false;

				if ( alphaMode === ALPHA_MODES.MASK ) {

					materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5;

				}

			}

			if ( materialDef.normalTexture !== undefined && materialType !== MeshBasicMaterial ) {

				pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) );

				materialParams.normalScale = new Vector2( 1, 1 );

				if ( materialDef.normalTexture.scale !== undefined ) {

					materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale );

				}

			}

			if ( materialDef.occlusionTexture !== undefined && materialType !== MeshBasicMaterial ) {

				pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) );

				if ( materialDef.occlusionTexture.strength !== undefined ) {

					materialParams.aoMapIntensity = materialDef.occlusionTexture.strength;

				}

			}

			if ( materialDef.emissiveFactor !== undefined && materialType !== MeshBasicMaterial ) {

				materialParams.emissive = new Color().fromArray( materialDef.emissiveFactor );

			}

			if ( materialDef.emissiveTexture !== undefined && materialType !== MeshBasicMaterial ) {

				pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture ) );

			}

			return Promise.all( pending ).then( function () {

				var material;

				if ( materialType === GLTFMeshStandardSGMaterial ) {

					material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams );

				} else {

					material = new materialType( materialParams );

				}

				if ( materialDef.name ) material.name = materialDef.name;

				// baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding.
				if ( material.map ) material.map.encoding = sRGBEncoding;
				if ( material.emissiveMap ) material.emissiveMap.encoding = sRGBEncoding;

				assignExtrasToUserData( material, materialDef );

				parser.associations.set( material, { type: 'materials', index: materialIndex } );

				if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef );

				return material;

			} );

		};

		/** When Object3D instances are targeted by animation, they need unique names. */
		GLTFParser.prototype.createUniqueName = function ( originalName ) {

			var sanitizedName = PropertyBinding.sanitizeNodeName( originalName || '' );

			var name = sanitizedName;

			for ( var i = 1; this.nodeNamesUsed[ name ]; ++ i ) {

				name = sanitizedName + '_' + i;

			}

			this.nodeNamesUsed[ name ] = true;

			return name;

		};

		/**
		 * @param {BufferGeometry} geometry
		 * @param {GLTF.Primitive} primitiveDef
		 * @param {GLTFParser} parser
		 */
		function computeBounds( geometry, primitiveDef, parser ) {

			var attributes = primitiveDef.attributes;

			var box = new Box3();

			if ( attributes.POSITION !== undefined ) {

				var accessor = parser.json.accessors[ attributes.POSITION ];

				var min = accessor.min;
				var max = accessor.max;

				// glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement.

				if ( min !== undefined && max !== undefined ) {

					box.set(
						new Vector3( min[ 0 ], min[ 1 ], min[ 2 ] ),
						new Vector3( max[ 0 ], max[ 1 ], max[ 2 ] ) );

				} else {

					console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' );

					return;

				}

			} else {

				return;

			}

			var targets = primitiveDef.targets;

			if ( targets !== undefined ) {

				var maxDisplacement = new Vector3();
				var vector = new Vector3();

				for ( var i = 0, il = targets.length; i < il; i ++ ) {

					var target = targets[ i ];

					if ( target.POSITION !== undefined ) {

						var accessor = parser.json.accessors[ target.POSITION ];
						var min = accessor.min;
						var max = accessor.max;

						// glTF requires 'min' and 'max', but VRM (which extends glTF) currently ignores that requirement.

						if ( min !== undefined && max !== undefined ) {

							// we need to get max of absolute components because target weight is [-1,1]
							vector.setX( Math.max( Math.abs( min[ 0 ] ), Math.abs( max[ 0 ] ) ) );
							vector.setY( Math.max( Math.abs( min[ 1 ] ), Math.abs( max[ 1 ] ) ) );
							vector.setZ( Math.max( Math.abs( min[ 2 ] ), Math.abs( max[ 2 ] ) ) );

							// Note: this assumes that the sum of all weights is at most 1. This isn't quite correct - it's more conservative
							// to assume that each target can have a max weight of 1. However, for some use cases - notably, when morph targets
							// are used to implement key-frame animations and as such only two are active at a time - this results in very large
							// boxes. So for now we make a box that's sometimes a touch too small but is hopefully mostly of reasonable size.
							maxDisplacement.max( vector );

						} else {

							console.warn( 'THREE.GLTFLoader: Missing min/max properties for accessor POSITION.' );

						}

					}

				}

				// As per comment above this box isn't conservative, but has a reasonable size for a very large number of morph targets.
				box.expandByVector( maxDisplacement );

			}

			geometry.boundingBox = box;

			var sphere = new Sphere();

			box.getCenter( sphere.center );
			sphere.radius = box.min.distanceTo( box.max ) / 2;

			geometry.boundingSphere = sphere;

		}

		/**
		 * @param {BufferGeometry} geometry
		 * @param {GLTF.Primitive} primitiveDef
		 * @param {GLTFParser} parser
		 * @return {Promise<BufferGeometry>}
		 */
		function addPrimitiveAttributes( geometry, primitiveDef, parser ) {

			var attributes = primitiveDef.attributes;

			var pending = [];

			function assignAttributeAccessor( accessorIndex, attributeName ) {

				return parser.getDependency( 'accessor', accessorIndex )
					.then( function ( accessor ) {

						geometry.setAttribute( attributeName, accessor );

					} );

			}

			for ( var gltfAttributeName in attributes ) {

				var threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase();

				// Skip attributes already provided by e.g. Draco extension.
				if ( threeAttributeName in geometry.attributes ) continue;

				pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) );

			}

			if ( primitiveDef.indices !== undefined && ! geometry.index ) {

				var accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) {

					geometry.setIndex( accessor );

				} );

				pending.push( accessor );

			}

			assignExtrasToUserData( geometry, primitiveDef );

			computeBounds( geometry, primitiveDef, parser );

			return Promise.all( pending ).then( function () {

				return primitiveDef.targets !== undefined
					? addMorphTargets( geometry, primitiveDef.targets, parser )
					: geometry;

			} );

		}

		/**
		 * @param {BufferGeometry} geometry
		 * @param {Number} drawMode
		 * @return {BufferGeometry}
		 */
		function toTrianglesDrawMode( geometry, drawMode ) {

			var index = geometry.getIndex();

			// generate index if not present

			if ( index === null ) {

				var indices = [];

				var position = geometry.getAttribute( 'position' );

				if ( position !== undefined ) {

					for ( var i = 0; i < position.count; i ++ ) {

						indices.push( i );

					}

					geometry.setIndex( indices );
					index = geometry.getIndex();

				} else {

					console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Undefined position attribute. Processing not possible.' );
					return geometry;

				}

			}

			//

			var numberOfTriangles = index.count - 2;
			var newIndices = [];

			if ( drawMode === TriangleFanDrawMode ) {

				// gl.TRIANGLE_FAN

				for ( var i = 1; i <= numberOfTriangles; i ++ ) {

					newIndices.push( index.getX( 0 ) );
					newIndices.push( index.getX( i ) );
					newIndices.push( index.getX( i + 1 ) );

				}

			} else {

				// gl.TRIANGLE_STRIP

				for ( var i = 0; i < numberOfTriangles; i ++ ) {

					if ( i % 2 === 0 ) {

						newIndices.push( index.getX( i ) );
						newIndices.push( index.getX( i + 1 ) );
						newIndices.push( index.getX( i + 2 ) );


					} else {

						newIndices.push( index.getX( i + 2 ) );
						newIndices.push( index.getX( i + 1 ) );
						newIndices.push( index.getX( i ) );

					}

				}

			}

			if ( ( newIndices.length / 3 ) !== numberOfTriangles ) {

				console.error( 'THREE.GLTFLoader.toTrianglesDrawMode(): Unable to generate correct amount of triangles.' );

			}

			// build final geometry

			var newGeometry = geometry.clone();
			newGeometry.setIndex( newIndices );

			return newGeometry;

		}

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry
		 *
		 * Creates BufferGeometries from primitives.
		 *
		 * @param {Array<GLTF.Primitive>} primitives
		 * @return {Promise<Array<BufferGeometry>>}
		 */
		GLTFParser.prototype.loadGeometries = function ( primitives ) {

			var parser = this;
			var extensions = this.extensions;
			var cache = this.primitiveCache;

			function createDracoPrimitive( primitive ) {

				return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]
					.decodePrimitive( primitive, parser )
					.then( function ( geometry ) {

						return addPrimitiveAttributes( geometry, primitive, parser );

					} );

			}

			var pending = [];

			for ( var i = 0, il = primitives.length; i < il; i ++ ) {

				var primitive = primitives[ i ];
				var cacheKey = createPrimitiveKey( primitive );

				// See if we've already created this geometry
				var cached = cache[ cacheKey ];

				if ( cached ) {

					// Use the cached geometry if it exists
					pending.push( cached.promise );

				} else {

					var geometryPromise;

					if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) {

						// Use DRACO geometry if available
						geometryPromise = createDracoPrimitive( primitive );

					} else {

						// Otherwise create a new geometry
						geometryPromise = addPrimitiveAttributes( new BufferGeometry(), primitive, parser );

					}

					// Cache this geometry
					cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise };

					pending.push( geometryPromise );

				}

			}

			return Promise.all( pending );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes
		 * @param {number} meshIndex
		 * @return {Promise<Group|Mesh|SkinnedMesh>}
		 */
		GLTFParser.prototype.loadMesh = function ( meshIndex ) {

			var parser = this;
			var json = this.json;
			var extensions = this.extensions;

			var meshDef = json.meshes[ meshIndex ];
			var primitives = meshDef.primitives;

			var pending = [];

			for ( var i = 0, il = primitives.length; i < il; i ++ ) {

				var material = primitives[ i ].material === undefined
					? createDefaultMaterial( this.cache )
					: this.getDependency( 'material', primitives[ i ].material );

				pending.push( material );

			}

			pending.push( parser.loadGeometries( primitives ) );

			return Promise.all( pending ).then( function ( results ) {

				var materials = results.slice( 0, results.length - 1 );
				var geometries = results[ results.length - 1 ];

				var meshes = [];

				for ( var i = 0, il = geometries.length; i < il; i ++ ) {

					var geometry = geometries[ i ];
					var primitive = primitives[ i ];

					// 1. create Mesh

					var mesh;

					var material = materials[ i ];

					if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES ||
						primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
						primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ||
						primitive.mode === undefined ) {

						// .isSkinnedMesh isn't in glTF spec. See ._markDefs()
						mesh = meshDef.isSkinnedMesh === true
							? new SkinnedMesh( geometry, material )
							: new Mesh( geometry, material );

						if ( mesh.isSkinnedMesh === true && ! mesh.geometry.attributes.skinWeight.normalized ) {

							// we normalize floating point skin weight array to fix malformed assets (see #15319)
							// it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs
							mesh.normalizeSkinWeights();

						}

						if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) {

							mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleStripDrawMode );

						} else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) {

							mesh.geometry = toTrianglesDrawMode( mesh.geometry, TriangleFanDrawMode );

						}

					} else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) {

						mesh = new LineSegments( geometry, material );

					} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) {

						mesh = new Line( geometry, material );

					} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) {

						mesh = new LineLoop( geometry, material );

					} else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) {

						mesh = new Points( geometry, material );

					} else {

						throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode );

					}

					if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) {

						updateMorphTargets( mesh, meshDef );

					}

					mesh.name = parser.createUniqueName( meshDef.name || ( 'mesh_' + meshIndex ) );

					assignExtrasToUserData( mesh, meshDef );

					if ( primitive.extensions ) addUnknownExtensionsToUserData( extensions, mesh, primitive );

					parser.assignFinalMaterial( mesh );

					meshes.push( mesh );

				}

				if ( meshes.length === 1 ) {

					return meshes[ 0 ];

				}

				var group = new Group();

				for ( var i = 0, il = meshes.length; i < il; i ++ ) {

					group.add( meshes[ i ] );

				}

				return group;

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras
		 * @param {number} cameraIndex
		 * @return {Promise<THREE.Camera>}
		 */
		GLTFParser.prototype.loadCamera = function ( cameraIndex ) {

			var camera;
			var cameraDef = this.json.cameras[ cameraIndex ];
			var params = cameraDef[ cameraDef.type ];

			if ( ! params ) {

				console.warn( 'THREE.GLTFLoader: Missing camera parameters.' );
				return;

			}

			if ( cameraDef.type === 'perspective' ) {

				camera = new PerspectiveCamera( MathUtils.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 );

			} else if ( cameraDef.type === 'orthographic' ) {

				camera = new OrthographicCamera( - params.xmag, params.xmag, params.ymag, - params.ymag, params.znear, params.zfar );

			}

			if ( cameraDef.name ) camera.name = this.createUniqueName( cameraDef.name );

			assignExtrasToUserData( camera, cameraDef );

			return Promise.resolve( camera );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins
		 * @param {number} skinIndex
		 * @return {Promise<Object>}
		 */
		GLTFParser.prototype.loadSkin = function ( skinIndex ) {

			var skinDef = this.json.skins[ skinIndex ];

			var skinEntry = { joints: skinDef.joints };

			if ( skinDef.inverseBindMatrices === undefined ) {

				return Promise.resolve( skinEntry );

			}

			return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) {

				skinEntry.inverseBindMatrices = accessor;

				return skinEntry;

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
		 * @param {number} animationIndex
		 * @return {Promise<AnimationClip>}
		 */
		GLTFParser.prototype.loadAnimation = function ( animationIndex ) {

			var json = this.json;

			var animationDef = json.animations[ animationIndex ];

			var pendingNodes = [];
			var pendingInputAccessors = [];
			var pendingOutputAccessors = [];
			var pendingSamplers = [];
			var pendingTargets = [];

			for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) {

				var channel = animationDef.channels[ i ];
				var sampler = animationDef.samplers[ channel.sampler ];
				var target = channel.target;
				var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated.
				var input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input;
				var output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output;

				pendingNodes.push( this.getDependency( 'node', name ) );
				pendingInputAccessors.push( this.getDependency( 'accessor', input ) );
				pendingOutputAccessors.push( this.getDependency( 'accessor', output ) );
				pendingSamplers.push( sampler );
				pendingTargets.push( target );

			}

			return Promise.all( [

				Promise.all( pendingNodes ),
				Promise.all( pendingInputAccessors ),
				Promise.all( pendingOutputAccessors ),
				Promise.all( pendingSamplers ),
				Promise.all( pendingTargets )

			] ).then( function ( dependencies ) {

				var nodes = dependencies[ 0 ];
				var inputAccessors = dependencies[ 1 ];
				var outputAccessors = dependencies[ 2 ];
				var samplers = dependencies[ 3 ];
				var targets = dependencies[ 4 ];

				var tracks = [];

				for ( var i = 0, il = nodes.length; i < il; i ++ ) {

					var node = nodes[ i ];
					var inputAccessor = inputAccessors[ i ];
					var outputAccessor = outputAccessors[ i ];
					var sampler = samplers[ i ];
					var target = targets[ i ];

					if ( node === undefined ) continue;

					node.updateMatrix();
					node.matrixAutoUpdate = true;

					var TypedKeyframeTrack;

					switch ( PATH_PROPERTIES[ target.path ] ) {

						case PATH_PROPERTIES.weights:

							TypedKeyframeTrack = NumberKeyframeTrack;
							break;

						case PATH_PROPERTIES.rotation:

							TypedKeyframeTrack = QuaternionKeyframeTrack;
							break;

						case PATH_PROPERTIES.position:
						case PATH_PROPERTIES.scale:
						default:

							TypedKeyframeTrack = VectorKeyframeTrack;
							break;

					}

					var targetName = node.name ? node.name : node.uuid;

					var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : InterpolateLinear;

					var targetNames = [];

					if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) {

						// Node may be a Group (glTF mesh with several primitives) or a Mesh.
						node.traverse( function ( object ) {

							if ( object.isMesh === true && object.morphTargetInfluences ) {

								targetNames.push( object.name ? object.name : object.uuid );

							}

						} );

					} else {

						targetNames.push( targetName );

					}

					var outputArray = outputAccessor.array;

					if ( outputAccessor.normalized ) {

						var scale;

						if ( outputArray.constructor === Int8Array ) {

							scale = 1 / 127;

						} else if ( outputArray.constructor === Uint8Array ) {

							scale = 1 / 255;

						} else if ( outputArray.constructor == Int16Array ) {

							scale = 1 / 32767;

						} else if ( outputArray.constructor === Uint16Array ) {

							scale = 1 / 65535;

						} else {

							throw new Error( 'THREE.GLTFLoader: Unsupported output accessor component type.' );

						}

						var scaled = new Float32Array( outputArray.length );

						for ( var j = 0, jl = outputArray.length; j < jl; j ++ ) {

							scaled[ j ] = outputArray[ j ] * scale;

						}

						outputArray = scaled;

					}

					for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) {

						var track = new TypedKeyframeTrack(
							targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ],
							inputAccessor.array,
							outputArray,
							interpolation
						);

						// Override interpolation with custom factory method.
						if ( sampler.interpolation === 'CUBICSPLINE' ) {

							track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) {

								// A CUBICSPLINE keyframe in glTF has three output values for each input value,
								// representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize()
								// must be divided by three to get the interpolant's sampleSize argument.

								return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result );

							};

							// Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants.
							track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true;

						}

						tracks.push( track );

					}

				}

				var name = animationDef.name ? animationDef.name : 'animation_' + animationIndex;

				return new AnimationClip( name, undefined, tracks );

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy
		 * @param {number} nodeIndex
		 * @return {Promise<Object3D>}
		 */
		GLTFParser.prototype.loadNode = function ( nodeIndex ) {

			var json = this.json;
			var extensions = this.extensions;
			var parser = this;

			var nodeDef = json.nodes[ nodeIndex ];

			// reserve node's name before its dependencies, so the root has the intended name.
			var nodeName = nodeDef.name ? parser.createUniqueName( nodeDef.name ) : '';

			return ( function () {

				var pending = [];

				if ( nodeDef.mesh !== undefined ) {

					pending.push( parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) {

						var node = parser._getNodeRef( parser.meshCache, nodeDef.mesh, mesh );

						// if weights are provided on the node, override weights on the mesh.
						if ( nodeDef.weights !== undefined ) {

							node.traverse( function ( o ) {

								if ( ! o.isMesh ) return;

								for ( var i = 0, il = nodeDef.weights.length; i < il; i ++ ) {

									o.morphTargetInfluences[ i ] = nodeDef.weights[ i ];

								}

							} );

						}

						return node;

					} ) );

				}

				if ( nodeDef.camera !== undefined ) {

					pending.push( parser.getDependency( 'camera', nodeDef.camera ).then( function ( camera ) {

						return parser._getNodeRef( parser.cameraCache, nodeDef.camera, camera );

					} ) );

				}

				parser._invokeAll( function ( ext ) {

					return ext.createNodeAttachment && ext.createNodeAttachment( nodeIndex );

				} ).forEach( function ( promise ) {

					pending.push( promise );

				} );

				return Promise.all( pending );

			}() ).then( function ( objects ) {

				var node;

				// .isBone isn't in glTF spec. See ._markDefs
				if ( nodeDef.isBone === true ) {

					node = new Bone();

				} else if ( objects.length > 1 ) {

					node = new Group();

				} else if ( objects.length === 1 ) {

					node = objects[ 0 ];

				} else {

					node = new Object3D();

				}

				if ( node !== objects[ 0 ] ) {

					for ( var i = 0, il = objects.length; i < il; i ++ ) {

						node.add( objects[ i ] );

					}

				}

				if ( nodeDef.name ) {

					node.userData.name = nodeDef.name;
					node.name = nodeName;

				}

				assignExtrasToUserData( node, nodeDef );

				if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef );

				if ( nodeDef.matrix !== undefined ) {

					var matrix = new Matrix4();
					matrix.fromArray( nodeDef.matrix );
					node.applyMatrix4( matrix );

				} else {

					if ( nodeDef.translation !== undefined ) {

						node.position.fromArray( nodeDef.translation );

					}

					if ( nodeDef.rotation !== undefined ) {

						node.quaternion.fromArray( nodeDef.rotation );

					}

					if ( nodeDef.scale !== undefined ) {

						node.scale.fromArray( nodeDef.scale );

					}

				}

				parser.associations.set( node, { type: 'nodes', index: nodeIndex } );

				return node;

			} );

		};

		/**
		 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes
		 * @param {number} sceneIndex
		 * @return {Promise<Group>}
		 */
		GLTFParser.prototype.loadScene = function () {

			// scene node hierachy builder

			function buildNodeHierachy( nodeId, parentObject, json, parser ) {

				var nodeDef = json.nodes[ nodeId ];

				return parser.getDependency( 'node', nodeId ).then( function ( node ) {

					if ( nodeDef.skin === undefined ) return node;

					// build skeleton here as well

					var skinEntry;

					return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) {

						skinEntry = skin;

						var pendingJoints = [];

						for ( var i = 0, il = skinEntry.joints.length; i < il; i ++ ) {

							pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) );

						}

						return Promise.all( pendingJoints );

					} ).then( function ( jointNodes ) {

						node.traverse( function ( mesh ) {

							if ( ! mesh.isMesh ) return;

							var bones = [];
							var boneInverses = [];

							for ( var j = 0, jl = jointNodes.length; j < jl; j ++ ) {

								var jointNode = jointNodes[ j ];

								if ( jointNode ) {

									bones.push( jointNode );

									var mat = new Matrix4();

									if ( skinEntry.inverseBindMatrices !== undefined ) {

										mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 );

									}

									boneInverses.push( mat );

								} else {

									console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] );

								}

							}

							mesh.bind( new Skeleton( bones, boneInverses ), mesh.matrixWorld );

						} );

						return node;

					} );

				} ).then( function ( node ) {

					// build node hierachy

					parentObject.add( node );

					var pending = [];

					if ( nodeDef.children ) {

						var children = nodeDef.children;

						for ( var i = 0, il = children.length; i < il; i ++ ) {

							var child = children[ i ];
							pending.push( buildNodeHierachy( child, node, json, parser ) );

						}

					}

					return Promise.all( pending );

				} );

			}

			return function loadScene( sceneIndex ) {

				var json = this.json;
				var extensions = this.extensions;
				var sceneDef = this.json.scenes[ sceneIndex ];
				var parser = this;

				// Loader returns Group, not Scene.
				// See: https://github.com/mrdoob/three.js/issues/18342#issuecomment-578981172
				var scene = new Group();
				if ( sceneDef.name ) scene.name = parser.createUniqueName( sceneDef.name );

				assignExtrasToUserData( scene, sceneDef );

				if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef );

				var nodeIds = sceneDef.nodes || [];

				var pending = [];

				for ( var i = 0, il = nodeIds.length; i < il; i ++ ) {

					pending.push( buildNodeHierachy( nodeIds[ i ], scene, json, parser ) );

				}

				return Promise.all( pending ).then( function () {

					return scene;

				} );

			};

		}();

		return GLTFLoader;

	} )();

	/**
	 * @webxr-input-profiles/motion-controllers 1.0.0 https://github.com/immersive-web/webxr-input-profiles
	 */

	const Constants = {
	  Handedness: Object.freeze({
	    NONE: 'none',
	    LEFT: 'left',
	    RIGHT: 'right'
	  }),

	  ComponentState: Object.freeze({
	    DEFAULT: 'default',
	    TOUCHED: 'touched',
	    PRESSED: 'pressed'
	  }),

	  ComponentProperty: Object.freeze({
	    BUTTON: 'button',
	    X_AXIS: 'xAxis',
	    Y_AXIS: 'yAxis',
	    STATE: 'state'
	  }),

	  ComponentType: Object.freeze({
	    TRIGGER: 'trigger',
	    SQUEEZE: 'squeeze',
	    TOUCHPAD: 'touchpad',
	    THUMBSTICK: 'thumbstick',
	    BUTTON: 'button'
	  }),

	  ButtonTouchThreshold: 0.05,

	  AxisTouchThreshold: 0.1,

	  VisualResponseProperty: Object.freeze({
	    TRANSFORM: 'transform',
	    VISIBILITY: 'visibility'
	  })
	};

	/**
	 * @description Static helper function to fetch a JSON file and turn it into a JS object
	 * @param {string} path - Path to JSON file to be fetched
	 */
	async function fetchJsonFile(path) {
	  const response = await fetch(path);
	  if (!response.ok) {
	    throw new Error(response.statusText);
	  } else {
	    return response.json();
	  }
	}

	async function fetchProfilesList(basePath) {
	  if (!basePath) {
	    throw new Error('No basePath supplied');
	  }

	  const profileListFileName = 'profilesList.json';
	  const profilesList = await fetchJsonFile(`${basePath}/${profileListFileName}`);
	  return profilesList;
	}

	async function fetchProfile(xrInputSource, basePath, defaultProfile = null, getAssetPath = true) {
	  if (!xrInputSource) {
	    throw new Error('No xrInputSource supplied');
	  }

	  if (!basePath) {
	    throw new Error('No basePath supplied');
	  }

	  // Get the list of profiles
	  const supportedProfilesList = await fetchProfilesList(basePath);

	  // Find the relative path to the first requested profile that is recognized
	  let match;
	  xrInputSource.profiles.some((profileId) => {
	    const supportedProfile = supportedProfilesList[profileId];
	    if (supportedProfile) {
	      match = {
	        profileId,
	        profilePath: `${basePath}/${supportedProfile.path}`,
	        deprecated: !!supportedProfile.deprecated
	      };
	    }
	    return !!match;
	  });

	  if (!match) {
	    if (!defaultProfile) {
	      throw new Error('No matching profile name found');
	    }

	    const supportedProfile = supportedProfilesList[defaultProfile];
	    if (!supportedProfile) {
	      throw new Error(`No matching profile name found and default profile "${defaultProfile}" missing.`);
	    }

	    match = {
	      profileId: defaultProfile,
	      profilePath: `${basePath}/${supportedProfile.path}`,
	      deprecated: !!supportedProfile.deprecated
	    };
	  }

	  const profile = await fetchJsonFile(match.profilePath);

	  let assetPath;
	  if (getAssetPath) {
	    let layout;
	    if (xrInputSource.handedness === 'any') {
	      layout = profile.layouts[Object.keys(profile.layouts)[0]];
	    } else {
	      layout = profile.layouts[xrInputSource.handedness];
	    }
	    if (!layout) {
	      throw new Error(
	        `No matching handedness, ${xrInputSource.handedness}, in profile ${match.profileId}`
	      );
	    }

	    if (layout.assetPath) {
	      assetPath = match.profilePath.replace('profile.json', layout.assetPath);
	    }
	  }

	  return { profile, assetPath };
	}

	/** @constant {Object} */
	const defaultComponentValues = {
	  xAxis: 0,
	  yAxis: 0,
	  button: 0,
	  state: Constants.ComponentState.DEFAULT
	};

	/**
	 * @description Converts an X, Y coordinate from the range -1 to 1 (as reported by the Gamepad
	 * API) to the range 0 to 1 (for interpolation). Also caps the X, Y values to be bounded within
	 * a circle. This ensures that thumbsticks are not animated outside the bounds of their physical
	 * range of motion and touchpads do not report touch locations off their physical bounds.
	 * @param {number} x The original x coordinate in the range -1 to 1
	 * @param {number} y The original y coordinate in the range -1 to 1
	 */
	function normalizeAxes(x = 0, y = 0) {
	  let xAxis = x;
	  let yAxis = y;

	  // Determine if the point is outside the bounds of the circle
	  // and, if so, place it on the edge of the circle
	  const hypotenuse = Math.sqrt((x * x) + (y * y));
	  if (hypotenuse > 1) {
	    const theta = Math.atan2(y, x);
	    xAxis = Math.cos(theta);
	    yAxis = Math.sin(theta);
	  }

	  // Scale and move the circle so values are in the interpolation range.  The circle's origin moves
	  // from (0, 0) to (0.5, 0.5). The circle's radius scales from 1 to be 0.5.
	  const result = {
	    normalizedXAxis: (xAxis * 0.5) + 0.5,
	    normalizedYAxis: (yAxis * 0.5) + 0.5
	  };
	  return result;
	}

	/**
	 * Contains the description of how the 3D model should visually respond to a specific user input.
	 * This is accomplished by initializing the object with the name of a node in the 3D model and
	 * property that need to be modified in response to user input, the name of the nodes representing
	 * the allowable range of motion, and the name of the input which triggers the change. In response
	 * to the named input changing, this object computes the appropriate weighting to use for
	 * interpolating between the range of motion nodes.
	 */
	class VisualResponse {
	  constructor(visualResponseDescription) {
	    this.componentProperty = visualResponseDescription.componentProperty;
	    this.states = visualResponseDescription.states;
	    this.valueNodeName = visualResponseDescription.valueNodeName;
	    this.valueNodeProperty = visualResponseDescription.valueNodeProperty;

	    if (this.valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM) {
	      this.minNodeName = visualResponseDescription.minNodeName;
	      this.maxNodeName = visualResponseDescription.maxNodeName;
	    }

	    // Initializes the response's current value based on default data
	    this.value = 0;
	    this.updateFromComponent(defaultComponentValues);
	  }

	  /**
	   * Computes the visual response's interpolation weight based on component state
	   * @param {Object} componentValues - The component from which to update
	   * @param {number} xAxis - The reported X axis value of the component
	   * @param {number} yAxis - The reported Y axis value of the component
	   * @param {number} button - The reported value of the component's button
	   * @param {string} state - The component's active state
	   */
	  updateFromComponent({
	    xAxis, yAxis, button, state
	  }) {
	    const { normalizedXAxis, normalizedYAxis } = normalizeAxes(xAxis, yAxis);
	    switch (this.componentProperty) {
	      case Constants.ComponentProperty.X_AXIS:
	        this.value = (this.states.includes(state)) ? normalizedXAxis : 0.5;
	        break;
	      case Constants.ComponentProperty.Y_AXIS:
	        this.value = (this.states.includes(state)) ? normalizedYAxis : 0.5;
	        break;
	      case Constants.ComponentProperty.BUTTON:
	        this.value = (this.states.includes(state)) ? button : 0;
	        break;
	      case Constants.ComponentProperty.STATE:
	        if (this.valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY) {
	          this.value = (this.states.includes(state));
	        } else {
	          this.value = this.states.includes(state) ? 1.0 : 0.0;
	        }
	        break;
	      default:
	        throw new Error(`Unexpected visualResponse componentProperty ${this.componentProperty}`);
	    }
	  }
	}

	class Component {
	  /**
	   * @param {Object} componentId - Id of the component
	   * @param {Object} componentDescription - Description of the component to be created
	   */
	  constructor(componentId, componentDescription) {
	    if (!componentId
	     || !componentDescription
	     || !componentDescription.visualResponses
	     || !componentDescription.gamepadIndices
	     || Object.keys(componentDescription.gamepadIndices).length === 0) {
	      throw new Error('Invalid arguments supplied');
	    }

	    this.id = componentId;
	    this.type = componentDescription.type;
	    this.rootNodeName = componentDescription.rootNodeName;
	    this.touchPointNodeName = componentDescription.touchPointNodeName;

	    // Build all the visual responses for this component
	    this.visualResponses = {};
	    Object.keys(componentDescription.visualResponses).forEach((responseName) => {
	      const visualResponse = new VisualResponse(componentDescription.visualResponses[responseName]);
	      this.visualResponses[responseName] = visualResponse;
	    });

	    // Set default values
	    this.gamepadIndices = Object.assign({}, componentDescription.gamepadIndices);

	    this.values = {
	      state: Constants.ComponentState.DEFAULT,
	      button: (this.gamepadIndices.button !== undefined) ? 0 : undefined,
	      xAxis: (this.gamepadIndices.xAxis !== undefined) ? 0 : undefined,
	      yAxis: (this.gamepadIndices.yAxis !== undefined) ? 0 : undefined
	    };
	  }

	  get data() {
	    const data = { id: this.id, ...this.values };
	    return data;
	  }

	  /**
	   * @description Poll for updated data based on current gamepad state
	   * @param {Object} gamepad - The gamepad object from which the component data should be polled
	   */
	  updateFromGamepad(gamepad) {
	    // Set the state to default before processing other data sources
	    this.values.state = Constants.ComponentState.DEFAULT;

	    // Get and normalize button
	    if (this.gamepadIndices.button !== undefined
	        && gamepad.buttons.length > this.gamepadIndices.button) {
	      const gamepadButton = gamepad.buttons[this.gamepadIndices.button];
	      this.values.button = gamepadButton.value;
	      this.values.button = (this.values.button < 0) ? 0 : this.values.button;
	      this.values.button = (this.values.button > 1) ? 1 : this.values.button;

	      // Set the state based on the button
	      if (gamepadButton.pressed || this.values.button === 1) {
	        this.values.state = Constants.ComponentState.PRESSED;
	      } else if (gamepadButton.touched || this.values.button > Constants.ButtonTouchThreshold) {
	        this.values.state = Constants.ComponentState.TOUCHED;
	      }
	    }

	    // Get and normalize x axis value
	    if (this.gamepadIndices.xAxis !== undefined
	        && gamepad.axes.length > this.gamepadIndices.xAxis) {
	      this.values.xAxis = gamepad.axes[this.gamepadIndices.xAxis];
	      this.values.xAxis = (this.values.xAxis < -1) ? -1 : this.values.xAxis;
	      this.values.xAxis = (this.values.xAxis > 1) ? 1 : this.values.xAxis;

	      // If the state is still default, check if the xAxis makes it touched
	      if (this.values.state === Constants.ComponentState.DEFAULT
	        && Math.abs(this.values.xAxis) > Constants.AxisTouchThreshold) {
	        this.values.state = Constants.ComponentState.TOUCHED;
	      }
	    }

	    // Get and normalize Y axis value
	    if (this.gamepadIndices.yAxis !== undefined
	        && gamepad.axes.length > this.gamepadIndices.yAxis) {
	      this.values.yAxis = gamepad.axes[this.gamepadIndices.yAxis];
	      this.values.yAxis = (this.values.yAxis < -1) ? -1 : this.values.yAxis;
	      this.values.yAxis = (this.values.yAxis > 1) ? 1 : this.values.yAxis;

	      // If the state is still default, check if the yAxis makes it touched
	      if (this.values.state === Constants.ComponentState.DEFAULT
	        && Math.abs(this.values.yAxis) > Constants.AxisTouchThreshold) {
	        this.values.state = Constants.ComponentState.TOUCHED;
	      }
	    }

	    // Update the visual response weights based on the current component data
	    Object.values(this.visualResponses).forEach((visualResponse) => {
	      visualResponse.updateFromComponent(this.values);
	    });
	  }
	}

	/**
	  * @description Builds a motion controller with components and visual responses based on the
	  * supplied profile description. Data is polled from the xrInputSource's gamepad.
	  * @author Nell Waliczek / https://github.com/NellWaliczek
	*/
	class MotionController {
	  /**
	   * @param {Object} xrInputSource - The XRInputSource to build the MotionController around
	   * @param {Object} profile - The best matched profile description for the supplied xrInputSource
	   * @param {Object} assetUrl
	   */
	  constructor(xrInputSource, profile, assetUrl) {
	    if (!xrInputSource) {
	      throw new Error('No xrInputSource supplied');
	    }

	    if (!profile) {
	      throw new Error('No profile supplied');
	    }

	    this.xrInputSource = xrInputSource;
	    this.assetUrl = assetUrl;
	    this.id = profile.profileId;

	    // Build child components as described in the profile description
	    this.layoutDescription = profile.layouts[xrInputSource.handedness];
	    this.components = {};
	    Object.keys(this.layoutDescription.components).forEach((componentId) => {
	      const componentDescription = this.layoutDescription.components[componentId];
	      this.components[componentId] = new Component(componentId, componentDescription);
	    });

	    // Initialize components based on current gamepad state
	    this.updateFromGamepad();
	  }

	  get gripSpace() {
	    return this.xrInputSource.gripSpace;
	  }

	  get targetRaySpace() {
	    return this.xrInputSource.targetRaySpace;
	  }

	  /**
	   * @description Returns a subset of component data for simplified debugging
	   */
	  get data() {
	    const data = [];
	    Object.values(this.components).forEach((component) => {
	      data.push(component.data);
	    });
	    return data;
	  }

	  /**
	   * @description Poll for updated data based on current gamepad state
	   */
	  updateFromGamepad() {
	    Object.values(this.components).forEach((component) => {
	      component.updateFromGamepad(this.xrInputSource.gamepad);
	    });
	  }
	}

	const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles';
	const DEFAULT_PROFILE = 'generic-trigger';

	function XRControllerModel( ) {

		Object3D.call( this );

		this.motionController = null;
		this.envMap = null;

	}

	XRControllerModel.prototype = Object.assign( Object.create( Object3D.prototype ), {

		constructor: XRControllerModel,

		setEnvironmentMap: function ( envMap ) {

			if ( this.envMap == envMap ) {

				return this;

			}

			this.envMap = envMap;
			this.traverse( ( child ) => {

				if ( child.isMesh ) {

					child.material.envMap = this.envMap;
					child.material.needsUpdate = true;

				}

			} );

			return this;

		},

		/**
		 * Polls data from the XRInputSource and updates the model's components to match
		 * the real world data
		 */
		updateMatrixWorld: function ( force ) {

			Object3D.prototype.updateMatrixWorld.call( this, force );

			if ( ! this.motionController ) return;

			// Cause the MotionController to poll the Gamepad for data
			this.motionController.updateFromGamepad();

			// Update the 3D model to reflect the button, thumbstick, and touchpad state
			Object.values( this.motionController.components ).forEach( ( component ) => {

				// Update node data based on the visual responses' current states
				Object.values( component.visualResponses ).forEach( ( visualResponse ) => {

					const { valueNode, minNode, maxNode, value, valueNodeProperty } = visualResponse;

					// Skip if the visual response node is not found. No error is needed,
					// because it will have been reported at load time.
					if ( ! valueNode ) return;

					// Calculate the new properties based on the weight supplied
					if ( valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY ) {

						valueNode.visible = value;

					} else if ( valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM ) {

						Quaternion.slerp(
							minNode.quaternion,
							maxNode.quaternion,
							valueNode.quaternion,
							value
						);

						valueNode.position.lerpVectors(
							minNode.position,
							maxNode.position,
							value
						);

					}

				} );

			} );

		}

	} );

	/**
	 * Walks the model's tree to find the nodes needed to animate the components and
	 * saves them to the motionContoller components for use in the frame loop. When
	 * touchpads are found, attaches a touch dot to them.
	 */
	function findNodes( motionController, scene ) {

		// Loop through the components and find the nodes needed for each components' visual responses
		Object.values( motionController.components ).forEach( ( component ) => {

			const { type, touchPointNodeName, visualResponses } = component;

			if ( type === Constants.ComponentType.TOUCHPAD ) {

				component.touchPointNode = scene.getObjectByName( touchPointNodeName );
				if ( component.touchPointNode ) {

					// Attach a touch dot to the touchpad.
					const sphereGeometry = new SphereBufferGeometry( 0.001 );
					const material = new MeshBasicMaterial( { color: 0x0000FF } );
					const sphere = new Mesh( sphereGeometry, material );
					component.touchPointNode.add( sphere );

				} else {

					console.warn( `Could not find touch dot, ${component.touchPointNodeName}, in touchpad component ${component.id}` );

				}

			}

			// Loop through all the visual responses to be applied to this component
			Object.values( visualResponses ).forEach( ( visualResponse ) => {

				const { valueNodeName, minNodeName, maxNodeName, valueNodeProperty } = visualResponse;

				// If animating a transform, find the two nodes to be interpolated between.
				if ( valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM ) {

					visualResponse.minNode = scene.getObjectByName( minNodeName );
					visualResponse.maxNode = scene.getObjectByName( maxNodeName );

					// If the extents cannot be found, skip this animation
					if ( ! visualResponse.minNode ) {

						console.warn( `Could not find ${minNodeName} in the model` );
						return;

					}

					if ( ! visualResponse.maxNode ) {

						console.warn( `Could not find ${maxNodeName} in the model` );
						return;

					}

				}

				// If the target node cannot be found, skip this animation
				visualResponse.valueNode = scene.getObjectByName( valueNodeName );
				if ( ! visualResponse.valueNode ) {

					console.warn( `Could not find ${valueNodeName} in the model` );

				}

			} );

		} );

	}

	function addAssetSceneToControllerModel( controllerModel, scene ) {

		// Find the nodes needed for animation and cache them on the motionController.
		findNodes( controllerModel.motionController, scene );

		// Apply any environment map that the mesh already has set.
		if ( controllerModel.envMap ) {

			scene.traverse( ( child ) => {

				if ( child.isMesh ) {

					child.material.envMap = controllerModel.envMap;
					child.material.needsUpdate = true;

				}

			} );

		}

		// Add the glTF scene to the controllerModel.
		controllerModel.add( scene );

	}

	var XRControllerModelFactory = ( function () {

		function XRControllerModelFactory( gltfLoader = null ) {

			this.gltfLoader = gltfLoader;
			this.path = DEFAULT_PROFILES_PATH;
			this._assetCache = {};

			// If a GLTFLoader wasn't supplied to the constructor create a new one.
			if ( ! this.gltfLoader ) {

				this.gltfLoader = new GLTFLoader();

			}

		}

		XRControllerModelFactory.prototype = {

			constructor: XRControllerModelFactory,

			createControllerModel: function ( controller ) {

				const controllerModel = new XRControllerModel();
				let scene = null;

				controller.addEventListener( 'connected', ( event ) => {

					const xrInputSource = event.data;

					if ( xrInputSource.targetRayMode !== 'tracked-pointer' || ! xrInputSource.gamepad ) return;

					fetchProfile( xrInputSource, this.path, DEFAULT_PROFILE ).then( ( { profile, assetPath } ) => {

						controllerModel.motionController = new MotionController(
							xrInputSource,
							profile,
							assetPath
						);

						const cachedAsset = this._assetCache[ controllerModel.motionController.assetUrl ];
						if ( cachedAsset ) {

							scene = cachedAsset.scene.clone();

							addAssetSceneToControllerModel( controllerModel, scene );

						} else {

							if ( ! this.gltfLoader ) {

								throw new Error( 'GLTFLoader not set.' );

							}

							this.gltfLoader.setPath( '' );
							this.gltfLoader.load( controllerModel.motionController.assetUrl, ( asset ) => {

								this._assetCache[ controllerModel.motionController.assetUrl ] = asset;

								scene = asset.scene.clone();

								addAssetSceneToControllerModel( controllerModel, scene );

							},
							null,
							() => {

								throw new Error( `Asset ${controllerModel.motionController.assetUrl} missing or malformed.` );

							} );

						}

					} ).catch( ( err ) => {

						console.warn( err );

					} );

				} );

				controller.addEventListener( 'disconnected', () => {

					controllerModel.motionController = null;
					controllerModel.remove( scene );
					scene = null;

				} );

				return controllerModel;

			}

		};

		return XRControllerModelFactory;

	} )();

	let fakeCam = new PerspectiveCamera();

	function toScene(vec, ref){
		let node = ref.clone();
		node.updateMatrix();
		node.updateMatrixWorld();

		let result = vec.clone().applyMatrix4(node.matrix);
		result.z -= 0.8 * node.scale.x;

		return result;
	};

	function computeMove(vrControls, controller){

		if(!controller || !controller.inputSource || !controller.inputSource.gamepad){
			return null;
		}

		let pad = controller.inputSource.gamepad;

		let axes = pad.axes;
		// [0,1] are for touchpad, [2,3] for thumbsticks?
		let y = 0;
		if(axes.length === 2){
			y = axes[1];
		}else if(axes.length === 4){
			y = axes[3];
		}

		y = Math.sign(y) * (2 * y) ** 2;

		let maxSize = 0;
		for(let pc of viewer.scene.pointclouds){
			let size = pc.boundingBox.min.distanceTo(pc.boundingBox.max);
			maxSize = Math.max(maxSize, size);
		}
		let multiplicator = Math.pow(maxSize, 0.5) / 2;

		let scale = vrControls.node.scale.x;
		let moveSpeed = viewer.getMoveSpeed();
		let amount = multiplicator * y * (moveSpeed ** 0.5) / scale;


		let rotation = new Quaternion().setFromEuler(controller.rotation);
		let dir = new Vector3(0, 0, -1);
		dir.applyQuaternion(rotation);

		let move = dir.clone().multiplyScalar(amount);

		let p1 = vrControls.toScene(controller.position);
		let p2 = vrControls.toScene(controller.position.clone().add(move));

		move = p2.clone().sub(p1);
		
		return move;
	};


	class FlyMode{

		constructor(vrControls){
			this.moveFactor = 1;
			this.dbgLabel = null;
		}

		start(vrControls){
			if(!this.dbgLabel){
				this.dbgLabel = new Potree.TextSprite("abc");
				this.dbgLabel.name = "debug label";
				vrControls.viewer.sceneVR.add(this.dbgLabel);
				this.dbgLabel.visible = false;
			}
		}
		
		end(){

		}

		update(vrControls, delta){

			let primary = vrControls.cPrimary;
			let secondary = vrControls.cSecondary;

			let move1 = computeMove(vrControls, primary);
			let move2 = computeMove(vrControls, secondary);


			if(!move1){
				move1 = new Vector3();
			}

			if(!move2){
				move2 = new Vector3();
			}

			let move = move1.clone().add(move2);

			move.multiplyScalar(-delta * this.moveFactor);
			vrControls.node.position.add(move);
			

			let scale = vrControls.node.scale.x;

			let camVR = vrControls.viewer.renderer.xr.getCamera(fakeCam);
			
			let vrPos = camVR.getWorldPosition(new Vector3());
			let vrDir = camVR.getWorldDirection(new Vector3());
			let vrTarget = vrPos.clone().add(vrDir.multiplyScalar(scale));

			let scenePos = toScene(vrPos, vrControls.node);
			let sceneDir = toScene(vrPos.clone().add(vrDir), vrControls.node).sub(scenePos);
			sceneDir.normalize().multiplyScalar(scale);
			let sceneTarget = scenePos.clone().add(sceneDir);

			vrControls.viewer.scene.view.setView(scenePos, sceneTarget);

			if(Potree.debug.message){
				this.dbgLabel.visible = true;
				this.dbgLabel.setText(Potree.debug.message);
				this.dbgLabel.scale.set(0.1, 0.1, 0.1);
				this.dbgLabel.position.copy(primary.position);
			}
		}
	};

	class TranslationMode{

		constructor(){
			this.controller = null;
			this.startPos = null;
			this.debugLine = null;
		}

		start(vrControls){
			this.controller = vrControls.triggered.values().next().value;
			this.startPos = vrControls.node.position.clone();
		}
		
		end(vrControls){

		}

		update(vrControls, delta){

			let start = this.controller.start.position;
			let end = this.controller.position;

			start = vrControls.toScene(start);
			end = vrControls.toScene(end);

			let diff = end.clone().sub(start);
			diff.set(-diff.x, -diff.y, -diff.z);

			let pos = new Vector3().addVectors(this.startPos, diff);

			vrControls.node.position.copy(pos);
		}

	};

	class RotScaleMode{

		constructor(){
			this.line = null;
			this.startState = null;
		}

		start(vrControls){
			if(!this.line){
				this.line = Potree.Utils.debugLine(
					vrControls.viewer.sceneVR, 
					new Vector3(0, 0, 0),
					new Vector3(0, 0, 0),
					0xffff00,
				);

				this.dbgLabel = new Potree.TextSprite("abc");
				this.dbgLabel.scale.set(0.1, 0.1, 0.1);
				vrControls.viewer.sceneVR.add(this.dbgLabel);
			}

			this.line.node.visible = true;

			this.startState = vrControls.node.clone();
		}

		end(vrControls){
			this.line.node.visible = false;
			this.dbgLabel.visible = false;
		}

		update(vrControls, delta){

			let start_c1 = vrControls.cPrimary.start.position.clone();
			let start_c2 = vrControls.cSecondary.start.position.clone();
			let start_center = start_c1.clone().add(start_c2).multiplyScalar(0.5);
			let start_c1_c2 = start_c2.clone().sub(start_c1);
			let end_c1 = vrControls.cPrimary.position.clone();
			let end_c2 = vrControls.cSecondary.position.clone();
			let end_center = end_c1.clone().add(end_c2).multiplyScalar(0.5);
			let end_c1_c2 = end_c2.clone().sub(end_c1);

			let d1 = start_c1_c2.length();
			let d2 = end_c1_c2.length();

			let angleStart = new Vector2(start_c1_c2.x, start_c1_c2.z).angle();
			let angleEnd = new Vector2(end_c1_c2.x, end_c1_c2.z).angle();
			let angleDiff = angleEnd - angleStart;
			
			let scale = d2 / d1;

			let node = this.startState.clone();
			node.updateMatrix();
			node.matrixAutoUpdate = false;

			let mToOrigin = new Matrix4().makeTranslation(...toScene(start_center, this.startState).multiplyScalar(-1).toArray());
			let mToStart = new Matrix4().makeTranslation(...toScene(start_center, this.startState).toArray());
			let mRotate = new Matrix4().makeRotationZ(angleDiff);
			let mScale = new Matrix4().makeScale(1 / scale, 1 / scale, 1 / scale);

			node.applyMatrix4(mToOrigin);
			node.applyMatrix4(mRotate);
			node.applyMatrix4(mScale);
			node.applyMatrix4(mToStart);

			let oldScenePos = toScene(start_center, this.startState);
			let newScenePos = toScene(end_center, node);
			let toNew = oldScenePos.clone().sub(newScenePos);
			let mToNew = new Matrix4().makeTranslation(...toNew.toArray());
			node.applyMatrix4(mToNew);

			node.matrix.decompose(node.position, node.quaternion, node.scale );

			vrControls.node.position.copy(node.position);
			vrControls.node.quaternion.copy(node.quaternion);
			vrControls.node.scale.copy(node.scale);
			vrControls.node.updateMatrix();

			{
				let scale = vrControls.node.scale.x;
				let camVR = vrControls.viewer.renderer.xr.getCamera(fakeCam);
				
				let vrPos = camVR.getWorldPosition(new Vector3());
				let vrDir = camVR.getWorldDirection(new Vector3());
				let vrTarget = vrPos.clone().add(vrDir.multiplyScalar(scale));

				let scenePos = toScene(vrPos, this.startState);
				let sceneDir = toScene(vrPos.clone().add(vrDir), this.startState).sub(scenePos);
				sceneDir.normalize().multiplyScalar(scale);
				let sceneTarget = scenePos.clone().add(sceneDir);

				vrControls.viewer.scene.view.setView(scenePos, sceneTarget);
				vrControls.viewer.setMoveSpeed(scale);
			}

			{ // update "GUI"
				this.line.set(end_c1, end_c2);

				let scale = vrControls.node.scale.x;
				this.dbgLabel.visible = true;
				this.dbgLabel.position.copy(end_center);
				this.dbgLabel.setText(`scale: 1 : ${scale.toFixed(2)}`);
				this.dbgLabel.scale.set(0.05, 0.05, 0.05);
			}

		}

	};


	class VRControls extends EventDispatcher{

		constructor(viewer){
			super(viewer);

			this.viewer = viewer;

			viewer.addEventListener("vr_start", this.onStart.bind(this));
			viewer.addEventListener("vr_end", this.onEnd.bind(this));

			this.node = new Object3D();
			this.node.up.set(0, 0, 1);
			this.triggered = new Set();

			let xr = viewer.renderer.xr;

			{ // lights
				
				const light = new PointLight( 0xffffff, 5, 0, 1 );
				light.position.set(0, 2, 0);
				this.viewer.sceneVR.add(light);
			}

			this.menu = null;

			const controllerModelFactory = new XRControllerModelFactory();

			let sg = new SphereGeometry(1, 32, 32);
			let sm = new MeshNormalMaterial();

			{ // setup primary controller
				let controller = xr.getController(0);

				let grip = xr.getControllerGrip(0);
				grip.name = "grip(0)";

				// ADD CONTROLLERMODEL
				grip.add( controllerModelFactory.createControllerModel( grip ) );
				this.viewer.sceneVR.add(grip);

				// ADD SPHERE
				let sphere = new Mesh(sg, sm);
				sphere.scale.set(0.005, 0.005, 0.005);

				controller.add(sphere);
				controller.visible = true;
				this.viewer.sceneVR.add(controller);

				{ // ADD LINE
					
					let lineGeometry = new LineGeometry();

					lineGeometry.setPositions([
						0, 0, -0.15,
						0, 0, 0.05,
					]);

					let lineMaterial = new LineMaterial({ 
						color: 0xff0000, 
						linewidth: 2, 
						resolution:  new Vector2(1000, 1000),
					});

					const line = new Line2(lineGeometry, lineMaterial);
					
					controller.add(line);
				}


				controller.addEventListener( 'connected', function ( event ) {
					const xrInputSource = event.data;
					controller.inputSource = xrInputSource;
					// initInfo(controller);
				});

				controller.addEventListener( 'selectstart', () => {this.onTriggerStart(controller);});
				controller.addEventListener( 'selectend', () => {this.onTriggerEnd(controller);});

				this.cPrimary =  controller;

			}

			{ // setup secondary controller
				let controller = xr.getController(1);

				let grip = xr.getControllerGrip(1);

				// ADD CONTROLLER MODEL
				let model = controllerModelFactory.createControllerModel( grip );
				grip.add(model);
				this.viewer.sceneVR.add( grip );

				// ADD SPHERE
				let sphere = new Mesh(sg, sm);
				sphere.scale.set(0.005, 0.005, 0.005);
				controller.add(sphere);
				controller.visible = true;
				this.viewer.sceneVR.add(controller);

				{ // ADD LINE
					
					let lineGeometry = new LineGeometry();

					lineGeometry.setPositions([
						0, 0, -0.15,
						0, 0, 0.05,
					]);

					let lineMaterial = new LineMaterial({ 
						color: 0xff0000, 
						linewidth: 2, 
						resolution:  new Vector2(1000, 1000),
					});

					const line = new Line2(lineGeometry, lineMaterial);
					
					controller.add(line);
				}

				controller.addEventListener( 'connected', (event) => {
					const xrInputSource = event.data;
					controller.inputSource = xrInputSource;
					this.initMenu(controller);
				});

				controller.addEventListener( 'selectstart', () => {this.onTriggerStart(controller);});
				controller.addEventListener( 'selectend', () => {this.onTriggerEnd(controller);});

				this.cSecondary =  controller;
			}

			this.mode_fly = new FlyMode();
			this.mode_translate = new TranslationMode();
			this.mode_rotScale = new RotScaleMode();
			this.setMode(this.mode_fly);
		}

		createSlider(label, min, max){

			let sg = new SphereGeometry(1, 8, 8);
			let cg = new CylinderGeometry(1, 1, 1, 8);
			let matHandle = new MeshBasicMaterial({color: 0xff0000});
			let matScale = new MeshBasicMaterial({color: 0xff4444});
			let matValue = new MeshNormalMaterial();

			let node = new Object3D("slider");
			let nLabel = new Potree.TextSprite(`${label}: 0`);
			let nMax = new Mesh(sg, matHandle);
			let nMin = new Mesh(sg, matHandle);
			let nValue = new Mesh(sg, matValue);
			let nScale = new Mesh(cg, matScale);

			nLabel.scale.set(0.2, 0.2, 0.2);
			nLabel.position.set(0, 0.35, 0);

			nMax.scale.set(0.02, 0.02, 0.02);
			nMax.position.set(0, 0.25, 0);

			nMin.scale.set(0.02, 0.02, 0.02);
			nMin.position.set(0, -0.25, 0);

			nValue.scale.set(0.02, 0.02, 0.02);
			nValue.position.set(0, 0, 0);

			nScale.scale.set(0.005, 0.5, 0.005);

			node.add(nLabel);
			node.add(nMax);
			node.add(nMin);
			node.add(nValue);
			node.add(nScale);

			return node;
		}

		createInfo(){ 

			let texture = new TextureLoader().load(`${Potree.resourcePath}/images/vr_controller_help.jpg`);
			let plane = new PlaneBufferGeometry(1, 1, 1, 1);
			let infoMaterial = new MeshBasicMaterial({map: texture});
			let infoNode = new Mesh(plane, infoMaterial);

			return infoNode;
		}

		initMenu(controller){

			if(this.menu){
				return;
			}

			let node = new Object3D("vr menu");

			// let nSlider = this.createSlider("speed", 0, 1);
			// let nInfo = this.createInfo();

			// // node.add(nSlider);
			// node.add(nInfo);

			// {
			// 	node.rotation.set(-1.5, 0, 0)
			// 	node.scale.set(0.3, 0.3, 0.3);
			// 	node.position.set(-0.2, -0.002, -0.1)

			// 	// nInfo.position.set(0.5, 0, 0);
			// 	nInfo.scale.set(0.8, 0.6, 0);

			// 	// controller.add(node);
			// }

			// node.position.set(-0.3, 1.2, 0.2);
			// node.scale.set(0.3, 0.2, 0.3);
			// node.lookAt(new THREE.Vector3(0, 1.5, 0.1));

			// this.viewer.sceneVR.add(node);

			this.menu = node;

			// window.vrSlider = nSlider;
			window.vrMenu = node;

		}


		toScene(vec){
			let camVR = this.getCamera();

			let mat = camVR.matrixWorld;
			let result = vec.clone().applyMatrix4(mat);

			return result;
		}

		toVR(vec){
			let camVR = this.getCamera();

			let mat = camVR.matrixWorld.clone();
			mat.invert();
			let result = vec.clone().applyMatrix4(mat);

			return result;
		}

		setMode(mode){

			if(this.mode === mode){
				return;
			}

			if(this.mode){
				this.mode.end(this);
			}

			for(let controller of [this.cPrimary, this.cSecondary]){

				let start = {
					position: controller.position.clone(),
					rotation: controller.rotation.clone(),
				};

				controller.start = start;
			}
			
			this.mode = mode;
			this.mode.start(this);
		}

		onTriggerStart(controller){
			this.triggered.add(controller);

			if(this.triggered.size === 0){
				this.setMode(this.mode_fly);
			}else if(this.triggered.size === 1){
				this.setMode(this.mode_translate);
			}else if(this.triggered.size === 2){
				this.setMode(this.mode_rotScale);
			}
		}

		onTriggerEnd(controller){
			this.triggered.delete(controller);

			if(this.triggered.size === 0){
				this.setMode(this.mode_fly);
			}else if(this.triggered.size === 1){
				this.setMode(this.mode_translate);
			}else if(this.triggered.size === 2){
				this.setMode(this.mode_rotScale);
			}
		}

		onStart(){

			let position = this.viewer.scene.view.position.clone();
			let direction = this.viewer.scene.view.direction;
			direction.multiplyScalar(-1);

			let target = position.clone().add(direction);
			target.z = position.z;

			let scale = this.viewer.getMoveSpeed();

			this.node.position.copy(position);
			this.node.lookAt(target);
			this.node.scale.set(scale, scale, scale);
			this.node.updateMatrix();
			this.node.updateMatrixWorld();
		}

		onEnd(){
			
		}


		setScene(scene){
			this.scene = scene;
		}

		getCamera(){
			let reference = this.viewer.scene.getActiveCamera();
			let camera = new PerspectiveCamera();

			// let scale = this.node.scale.x;
			let scale = this.viewer.getMoveSpeed();
			//camera.near = 0.01 / scale;
			camera.near = 0.1;
			camera.far = 1000;
			// camera.near = reference.near / scale;
			// camera.far = reference.far / scale;
			camera.up.set(0, 0, 1);
			camera.lookAt(new Vector3(0, -1, 0));
			camera.updateMatrix();
			camera.updateMatrixWorld();

			camera.position.copy(this.node.position);
			camera.rotation.copy(this.node.rotation);
			camera.scale.set(scale, scale, scale);
			camera.updateMatrix();
			camera.updateMatrixWorld();
			camera.matrixAutoUpdate = false;
			camera.parent = camera;

			return camera;
		}

		update(delta){

			

			// if(this.mode === this.mode_fly){
			// 	let ray = new THREE.Ray(origin, direction);
				
			// 	for(let object of this.selectables){

			// 		if(object.intersectsRay(ray)){
			// 			object.onHit(ray);
			// 		}

			// 	}

			// }

			this.mode.update(this, delta);

			

		}
	};

	// Adapted from three.js VRButton


	class VRButton {

		constructor(){
			this.onStartListeners = [];
			this.onEndListeners = [];
			this.element = null;
		}

		onStart(callback){
			this.onStartListeners.push(callback);
		}

		onEnd(callback){
			this.onEndListeners.push(callback);
		}

		static async createButton( renderer, options ) {

			if ( options ) {

				console.error( 'THREE.VRButton: The "options" parameter has been removed. Please set the reference space type via renderer.xr.setReferenceSpaceType() instead.' );

			}

			const button = new VRButton();
			const element = document.createElement( 'button' );

			button.element = element;

			function setEnter(){
				button.element.innerHTML = `
				<div style="font-size: 0.5em;">ENTER</div>
				<div style="font-weight: bold;">VR</div>
			`;
			}

			function setExit(){
				button.element.innerHTML = `
				<div style="font-size: 0.5em;">EXIT</div>
				<div style="font-weight: bold;">VR</div>
			`;
			}

			function showEnterVR( /*device*/ ) {

				let currentSession = null;

				function onSessionStarted( session ) {

					session.addEventListener( 'end', onSessionEnded );

					for(let listener of button.onStartListeners){
						listener();
					}


					renderer.xr.setSession( session );
					setExit();

					currentSession = session;

				}

				function onSessionEnded( /*event*/ ) {

					currentSession.removeEventListener( 'end', onSessionEnded );

					for(let listener of button.onEndListeners){
						listener();
					}

					setEnter();

					currentSession = null;

				}

				//

				button.element.style.display = '';

				button.element.style.cursor = 'pointer';

				setEnter();

				button.element.onmouseenter = function () {

					button.element.style.opacity = '1.0';

				};

				button.element.onmouseleave = function () {

					button.element.style.opacity = '0.7';

				};

				button.element.onclick = function () {

					if ( currentSession === null ) {

						// WebXR's requestReferenceSpace only works if the corresponding feature
						// was requested at session creation time. For simplicity, just ask for
						// the interesting ones as optional features, but be aware that the
						// requestReferenceSpace call will fail if it turns out to be unavailable.
						// ('local' is always available for immersive sessions and doesn't need to
						// be requested separately.)

						const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking' ] };
						navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted );

					} else {

						currentSession.end();

					}

				};

			}

			function stylizeElement( element ) {

				element.style.position = 'absolute';
				element.style.bottom = '20px';
				element.style.padding = '12px 6px';
				element.style.border = '1px solid #fff';
				element.style.borderRadius = '4px';
				element.style.background = 'rgba(0,0,0,0.1)';
				element.style.color = '#fff';
				element.style.font = 'normal 13px sans-serif';
				element.style.textAlign = 'center';
				element.style.opacity = '0.7';
				element.style.outline = 'none';
				element.style.zIndex = '999';

			}

			if ( 'xr' in navigator ) {

				button.element.id = 'VRButton';
				button.element.style.display = 'none';

				stylizeElement( button.element );

				let supported = await navigator.xr.isSessionSupported( 'immersive-vr' );

				if(supported){
					showEnterVR();

					return button;
				}else {
					return null;
				}

			} else {

				if ( window.isSecureContext === false ) {

					console.log("WEBXR NEEDS HTTPS");

				} else {

					console.log("WEBXR not available");

				}

				return null;



			}

		}

	}

	class Viewer extends EventDispatcher{
		
		constructor(domElement, args = {}){
			super();

			this.renderArea = domElement;
			this.guiLoaded = false;
			this.guiLoadTasks = [];

			this.onVrListeners = [];

			this.messages = [];
			this.elMessages = $(`
		<div id="message_listing" 
			style="position: absolute; z-index: 1000; left: 10px; bottom: 10px">
		</div>`);
			$(domElement).append(this.elMessages);
			
			try{

			{ // generate missing dom hierarchy
				if ($(domElement).find('#potree_map').length === 0) {
					let potreeMap = $(`
					<div id="potree_map" class="mapBox" style="position: absolute; left: 50px; top: 50px; width: 400px; height: 400px; display: none">
						<div id="potree_map_header" style="position: absolute; width: 100%; height: 25px; top: 0px; background-color: rgba(0,0,0,0.5); z-index: 1000; border-top-left-radius: 3px; border-top-right-radius: 3px;">
						</div>
						<div id="potree_map_content" class="map" style="position: absolute; z-index: 100; top: 25px; width: 100%; height: calc(100% - 25px); border: 2px solid rgba(0,0,0,0.5); box-sizing: border-box;"></div>
					</div>
				`);
					$(domElement).append(potreeMap);
				}

				if ($(domElement).find('#potree_description').length === 0) {
					let potreeDescription = $(`<div id="potree_description" class="potree_info_text"></div>`);
					$(domElement).append(potreeDescription);
				}

				if ($(domElement).find('#potree_annotations').length === 0) {
					let potreeAnnotationContainer = $(`
					<div id="potree_annotation_container" 
						style="position: absolute; z-index: 100000; width: 100%; height: 100%; pointer-events: none;"></div>`);
					$(domElement).append(potreeAnnotationContainer);
				}

				if ($(domElement).find('#potree_quick_buttons').length === 0) {
					let potreeMap = $(`
					<div id="potree_quick_buttons" class="quick_buttons_container" style="">
					</div>
				`);

					// {
					// 	let imgMenuToggle = document.createElement('img');
					// 	imgMenuToggle.src = new URL(Potree.resourcePath + '/icons/menu_button.svg').href;
					// 	imgMenuToggle.onclick = this.toggleSidebar;
					// 	// imgMenuToggle.classList.add('potree_menu_toggle');

					// 	potreeMap.append(imgMenuToggle);
					// }

					// {
					// 	let imgMenuToggle = document.createElement('img');
					// 	imgMenuToggle.src = new URL(Potree.resourcePath + '/icons/menu_button.svg').href;
					// 	imgMenuToggle.onclick = this.toggleSidebar;
					// 	// imgMenuToggle.classList.add('potree_menu_toggle');

					// 	potreeMap.append(imgMenuToggle);
					// }

					// {
					// 	let imgMenuToggle = document.createElement('img');
					// 	imgMenuToggle.src = new URL(Potree.resourcePath + '/icons/menu_button.svg').href;
					// 	imgMenuToggle.onclick = this.toggleSidebar;
					// 	// imgMenuToggle.classList.add('potree_menu_toggle');

					// 	potreeMap.append(imgMenuToggle);
					// }

					

					$(domElement).append(potreeMap);
				}
			}

			this.pointCloudLoadedCallback = args.onPointCloudLoaded || function () {};

			// if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
			//	defaultSettings.navigation = "Orbit";
			// }

			this.server = null;

			this.fov = 60;
			this.isFlipYZ = false;
			this.useDEMCollisions = false;
			this.generateDEM = false;
			this.minNodeSize = 60;
			this.edlStrength = 1.0;
			this.edlRadius = 1.4;
			this.edlOpacity = 1.0;
			this.useEDL = false;
			this.description = "";

			this.classifications = ClassificationScheme.DEFAULT;

			this.moveSpeed = 10;

			this.lengthUnit = LengthUnits.METER;
			this.lengthUnitDisplay = LengthUnits.METER;

			this.showBoundingBox = false;
			this.showAnnotations = true;
			this.freeze = false;
			this.clipTask = ClipTask.HIGHLIGHT;
			this.clipMethod = ClipMethod.INSIDE_ANY;

			this.elevationGradientRepeat = ElevationGradientRepeat.CLAMP;

			this.filterReturnNumberRange = [0, 7];
			this.filterNumberOfReturnsRange = [0, 7];
			this.filterGPSTimeRange = [-Infinity, Infinity];
			this.filterPointSourceIDRange = [0, 65535];

			this.potreeRenderer = null;
			this.edlRenderer = null;
			this.renderer = null;
			this.pRenderer = null;

			this.scene = null;
			this.sceneVR = null;
			this.overlay = null;
			this.overlayCamera = null;

			this.inputHandler = null;
			this.controls = null;

			this.clippingTool =  null;
			this.transformationTool = null;
			this.navigationCube = null;
			this.compass = null;
			
			this.skybox = null;
			this.clock = new Clock();
			this.background = null;

			this.initThree();

			if(args.noDragAndDrop){
				
			}else {
				this.initDragAndDrop();
			}

			if(typeof Stats !== "undefined"){
				this.stats = new Stats();
				this.stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom
				document.body.appendChild( this.stats.dom );
			}

			{
				let canvas = this.renderer.domElement;
				canvas.addEventListener("webglcontextlost", (e) => {
					console.log(e);
					this.postMessage("WebGL context lost. \u2639");

					let gl = this.renderer.getContext();
					let error = gl.getError();
					console.log(error);
				}, false);
			}

			{
				this.overlay = new Scene();
				this.overlayCamera = new OrthographicCamera(
					0, 1,
					1, 0,
					-1000, 1000
				);
			}
			
			this.pRenderer = new Renderer(this.renderer);
			
			{
				let near = 2.5;
				let far = 10.0;
				let fov = 90;
				
				this.shadowTestCam = new PerspectiveCamera(90, 1, near, far);
				this.shadowTestCam.position.set(3.50, -2.80, 8.561);
				this.shadowTestCam.lookAt(new Vector3(0, 0, 4.87));
			}
			

			let scene = new Scene$1(this.renderer);
			
			{ // create VR scene
				this.sceneVR = new Scene();

				// let texture = new THREE.TextureLoader().load(`${Potree.resourcePath}/images/vr_controller_help.jpg`);

				// let plane = new THREE.PlaneBufferGeometry(1, 1, 1, 1);
				// let infoMaterial = new THREE.MeshBasicMaterial({map: texture});
				// let infoNode = new THREE.Mesh(plane, infoMaterial);
				// infoNode.position.set(-0.5, 1, 0);
				// infoNode.scale.set(0.4, 0.3, 1);
				// infoNode.lookAt(0, 1, 0)
				// this.sceneVR.add(infoNode);

				// window.infoNode = infoNode;
			}

			this.setScene(scene);

			{
				this.inputHandler = new InputHandler(this);
				this.inputHandler.setScene(this.scene);

				this.clippingTool = new ClippingTool(this);
				this.transformationTool = new TransformationTool(this);
				this.navigationCube = new NavigationCube(this);
				this.navigationCube.visible = false;

				this.compass = new Compass(this);
				
				this.createControls();

				this.clippingTool.setScene(this.scene);
				
				let onPointcloudAdded = (e) => {
					if (this.scene.pointclouds.length === 1) {
						let speed = e.pointcloud.boundingBox.getSize(new Vector3()).length();
						speed = speed / 5;
						this.setMoveSpeed(speed);
					}
				};

				let onVolumeRemoved = (e) => {
					this.inputHandler.deselect(e.volume);
				};

				this.addEventListener('scene_changed', (e) => {
					this.inputHandler.setScene(e.scene);
					this.clippingTool.setScene(this.scene);
					
					if(!e.scene.hasEventListener("pointcloud_added", onPointcloudAdded)){
						e.scene.addEventListener("pointcloud_added", onPointcloudAdded);
					}

					if(!e.scene.hasEventListener("volume_removed", onPointcloudAdded)){
						e.scene.addEventListener("volume_removed", onVolumeRemoved);
					}
					
				});

				this.scene.addEventListener("volume_removed", onVolumeRemoved);
				this.scene.addEventListener('pointcloud_added', onPointcloudAdded);
			}

			{ // set defaults
				this.setFOV(60);
				this.setEDLEnabled(false);
				this.setEDLRadius(1.4);
				this.setEDLStrength(0.4);
				this.setEDLOpacity(1.0);
				this.setClipTask(ClipTask.HIGHLIGHT);
				this.setClipMethod(ClipMethod.INSIDE_ANY);
				this.setPointBudget(1*1000*1000);
				this.setShowBoundingBox(false);
				this.setFreeze(false);
				this.setControls(this.orbitControls);
				this.setBackground('gradient');

				this.scaleFactor = 1;

				this.loadSettingsFromURL();
			}

			// start rendering!
			//if(args.useDefaultRenderLoop === undefined || args.useDefaultRenderLoop === true){
				//requestAnimationFrame(this.loop.bind(this));
			//}

			this.renderer.setAnimationLoop(this.loop.bind(this));

			this.loadGUI = this.loadGUI.bind(this);

			this.annotationTool = new AnnotationTool(this);
			this.measuringTool = new MeasuringTool(this);
			this.profileTool = new ProfileTool(this);
			this.volumeTool = new VolumeTool(this);

			}catch(e){
				this.onCrash(e);
			}
		}

		onCrash(error){

			$(this.renderArea).empty();

			if ($(this.renderArea).find('#potree_failpage').length === 0) {
				let elFailPage = $(`
			<div id="#potree_failpage" class="potree_failpage"> 
				
				<h1>Potree Encountered An Error </h1>

				<p>
				This may happen if your browser or graphics card is not supported.
				<br>
				We recommend to use 
				<a href="https://www.google.com/chrome/browser" target="_blank" style="color:initial">Chrome</a>
				or 
				<a href="https://www.mozilla.org/" target="_blank">Firefox</a>.
				</p>

				<p>
				Please also visit <a href="http://webglreport.com/" target="_blank">webglreport.com</a> and 
				check whether your system supports WebGL.
				</p>
				<p>
				If you are already using one of the recommended browsers and WebGL is enabled, 
				consider filing an issue report at <a href="https://github.com/potree/potree/issues" target="_blank">github</a>,<br>
				including your operating system, graphics card, browser and browser version, as well as the 
				error message below.<br>
				Please do not report errors on unsupported browsers.
				</p>

				<pre id="potree_error_console" style="width: 100%; height: 100%"></pre>
				
			</div>`);

				let elErrorMessage = elFailPage.find('#potree_error_console');
				elErrorMessage.html(error.stack);

				$(this.renderArea).append(elFailPage);
			}

			throw error;
		}

		// ------------------------------------------------------------------------------------
		// Viewer API
		// ------------------------------------------------------------------------------------

		setScene (scene) {
			if (scene === this.scene) {
				return;
			}

			let oldScene = this.scene;
			this.scene = scene;

			this.dispatchEvent({
				type: 'scene_changed',
				oldScene: oldScene,
				scene: scene
			});

			{ // Annotations
				$('.annotation').detach();

				// for(let annotation of this.scene.annotations){
				//	this.renderArea.appendChild(annotation.domElement[0]);
				// }

				this.scene.annotations.traverse(annotation => {
					this.renderArea.appendChild(annotation.domElement[0]);
				});

				if (!this.onAnnotationAdded) {
					this.onAnnotationAdded = e => {
					// console.log("annotation added: " + e.annotation.title);

						e.annotation.traverse(node => {

							$("#potree_annotation_container").append(node.domElement);
							//this.renderArea.appendChild(node.domElement[0]);
							node.scene = this.scene;
						});
					};
				}

				if (oldScene) {
					oldScene.annotations.removeEventListener('annotation_added', this.onAnnotationAdded);
				}
				this.scene.annotations.addEventListener('annotation_added', this.onAnnotationAdded);
			}
		};

		setControls(controls){
			if (controls !== this.controls) {
				if (this.controls) {
					this.controls.enabled = false;
					this.inputHandler.removeInputListener(this.controls);
				}

				this.controls = controls;
				this.controls.enabled = true;
				this.inputHandler.addInputListener(this.controls);
			}
		}

		getControls () {

			if(this.renderer.xr.isPresenting){
				return this.vrControls;
			}else {
				return this.controls;
			}
			
		}

		getMinNodeSize () {
			return this.minNodeSize;
		};

		setMinNodeSize (value) {
			if (this.minNodeSize !== value) {
				this.minNodeSize = value;
				this.dispatchEvent({'type': 'minnodesize_changed', 'viewer': this});
			}
		};

		getBackground () {
			return this.background;
		}

		setBackground(bg){
			if (this.background === bg) {
				return;
			}

			if(bg === "skybox"){
				this.skybox = Utils.loadSkybox(new URL(Potree.resourcePath + '/textures/skybox2/').href);
			}

			this.background = bg;
			this.dispatchEvent({'type': 'background_changed', 'viewer': this});
		}

		setDescription (value) {
			this.description = value;
			
			$('#potree_description').html(value);
			//$('#potree_description').text(value);
		}

		getDescription(){
			return this.description;
		}

		setShowBoundingBox (value) {
			if (this.showBoundingBox !== value) {
				this.showBoundingBox = value;
				this.dispatchEvent({'type': 'show_boundingbox_changed', 'viewer': this});
			}
		};

		getShowBoundingBox () {
			return this.showBoundingBox;
		};

		setMoveSpeed (value) {
			if (this.moveSpeed !== value) {
				this.moveSpeed = value;
				this.dispatchEvent({'type': 'move_speed_changed', 'viewer': this, 'speed': value});
			}
		};

		getMoveSpeed () {
			return this.moveSpeed;
		};

		setWeightClassification (w) {
			for (let i = 0; i < this.scene.pointclouds.length; i++) {
				this.scene.pointclouds[i].material.weightClassification = w;
				this.dispatchEvent({'type': 'attribute_weights_changed' + i, 'viewer': this});
			}
		};

		setFreeze (value) {
			value = Boolean(value);
			if (this.freeze !== value) {
				this.freeze = value;
				this.dispatchEvent({'type': 'freeze_changed', 'viewer': this});
			}
		};

		getFreeze () {
			return this.freeze;
		};

		getClipTask(){
			return this.clipTask;
		}

		getClipMethod(){
			return this.clipMethod;
		}

		setClipTask(value){
			if(this.clipTask !== value){

				this.clipTask = value;

				this.dispatchEvent({
					type: "cliptask_changed", 
					viewer: this});		
			}
		}

		setClipMethod(value){
			if(this.clipMethod !== value){

				this.clipMethod = value;
				
				this.dispatchEvent({
					type: "clipmethod_changed",
					viewer: this});
			}
		}

		setElevationGradientRepeat(value){
			if(this.elevationGradientRepeat !== value){

				this.elevationGradientRepeat = value;

				this.dispatchEvent({
					type: "elevation_gradient_repeat_changed", 
					viewer: this});
			}
		}

		setPointBudget (value) {
			if (Potree.pointBudget !== value) {
				Potree.pointBudget = parseInt(value);
				this.dispatchEvent({'type': 'point_budget_changed', 'viewer': this});
			}
		};

		getPointBudget () {
			return Potree.pointBudget;
		};

		setShowAnnotations (value) {
			if (this.showAnnotations !== value) {
				this.showAnnotations = value;
				this.dispatchEvent({'type': 'show_annotations_changed', 'viewer': this});
			}
		}

		getShowAnnotations () {
			return this.showAnnotations;
		}
		
		setDEMCollisionsEnabled(value){
			if(this.useDEMCollisions !== value){
				this.useDEMCollisions = value;
				this.dispatchEvent({'type': 'use_demcollisions_changed', 'viewer': this});
			};
		};

		getDEMCollisionsEnabled () {
			return this.useDEMCollisions;
		};

		setEDLEnabled (value) {
			value = Boolean(value) && Features.SHADER_EDL.isSupported();

			if (this.useEDL !== value) {
				this.useEDL = value;
				this.dispatchEvent({'type': 'use_edl_changed', 'viewer': this});
			}
		};

		getEDLEnabled () {
			return this.useEDL;
		};

		setEDLRadius (value) {
			if (this.edlRadius !== value) {
				this.edlRadius = value;
				this.dispatchEvent({'type': 'edl_radius_changed', 'viewer': this});
			}
		};

		getEDLRadius () {
			return this.edlRadius;
		};

		setEDLStrength (value) {
			if (this.edlStrength !== value) {
				this.edlStrength = value;
				this.dispatchEvent({'type': 'edl_strength_changed', 'viewer': this});
			}
		};

		getEDLStrength () {
			return this.edlStrength;
		};

		setEDLOpacity (value) {
			if (this.edlOpacity !== value) {
				this.edlOpacity = value;
				this.dispatchEvent({'type': 'edl_opacity_changed', 'viewer': this});
			}
		};

		getEDLOpacity () {
			return this.edlOpacity;
		};

		setFOV (value) {
			if (this.fov !== value) {
				this.fov = value;
				this.dispatchEvent({'type': 'fov_changed', 'viewer': this});
			}
		};

		getFOV () {
			return this.fov;
		};

		disableAnnotations () {
			this.scene.annotations.traverse(annotation => {
				annotation.domElement.css('pointer-events', 'none');

				// return annotation.visible;
			});
		};

		enableAnnotations () {
			this.scene.annotations.traverse(annotation => {
				annotation.domElement.css('pointer-events', 'auto');

				// return annotation.visible;
			});
		}

		setClassifications(classifications){
			this.classifications = classifications;

			this.dispatchEvent({'type': 'classifications_changed', 'viewer': this});
		}

		setClassificationVisibility (key, value) {
			if (!this.classifications[key]) {
				this.classifications[key] = {visible: value, name: 'no name'};
				this.dispatchEvent({'type': 'classification_visibility_changed', 'viewer': this});
			} else if (this.classifications[key].visible !== value) {
				this.classifications[key].visible = value;
				this.dispatchEvent({'type': 'classification_visibility_changed', 'viewer': this});
			}
		}

		toggleAllClassificationsVisibility(){

			let numVisible = 0;
			let numItems = 0;
			for(const key of Object.keys(this.classifications)){
				if(this.classifications[key].visible){
					numVisible++;
				}
				numItems++;
			}

			let visible = true;
			if(numVisible === numItems){
				visible = false;
			}

			let somethingChanged = false;

			for(const key of Object.keys(this.classifications)){
				if(this.classifications[key].visible !== visible){
					this.classifications[key].visible = visible;
					somethingChanged = true;
				}
			}

			if(somethingChanged){
				this.dispatchEvent({'type': 'classification_visibility_changed', 'viewer': this});
			}
		}

		setFilterReturnNumberRange(from, to){
			this.filterReturnNumberRange = [from, to];
			this.dispatchEvent({'type': 'filter_return_number_range_changed', 'viewer': this});
		}

		setFilterNumberOfReturnsRange(from, to){
			this.filterNumberOfReturnsRange = [from, to];
			this.dispatchEvent({'type': 'filter_number_of_returns_range_changed', 'viewer': this});
		}

		setFilterGPSTimeRange(from, to){
			this.filterGPSTimeRange = [from, to];
			this.dispatchEvent({'type': 'filter_gps_time_range_changed', 'viewer': this});
		}

		setFilterPointSourceIDRange(from, to){
			this.filterPointSourceIDRange = [from, to];
			this.dispatchEvent({'type': 'filter_point_source_id_range_changed', 'viewer': this});
		}

		setLengthUnit (value) {
			switch (value) {
				case 'm':
					this.lengthUnit = LengthUnits.METER;
					this.lengthUnitDisplay = LengthUnits.METER;
					break;
				case 'ft':
					this.lengthUnit = LengthUnits.FEET;
					this.lengthUnitDisplay = LengthUnits.FEET;
					break;
				case 'in':
					this.lengthUnit = LengthUnits.INCH;
					this.lengthUnitDisplay = LengthUnits.INCH;
					break;
			}

			this.dispatchEvent({ 'type': 'length_unit_changed', 'viewer': this, value: value});
		};

		setLengthUnitAndDisplayUnit(lengthUnitValue, lengthUnitDisplayValue) {
			switch (lengthUnitValue) {
				case 'm':
					this.lengthUnit = LengthUnits.METER;
					break;
				case 'ft':
					this.lengthUnit = LengthUnits.FEET;
					break;
				case 'in':
					this.lengthUnit = LengthUnits.INCH;
					break;
			}

			switch (lengthUnitDisplayValue) {
				case 'm':
					this.lengthUnitDisplay = LengthUnits.METER;
					break;
				case 'ft':
					this.lengthUnitDisplay = LengthUnits.FEET;
					break;
				case 'in':
					this.lengthUnitDisplay = LengthUnits.INCH;
					break;
			}

			this.dispatchEvent({ 'type': 'length_unit_changed', 'viewer': this, value: lengthUnitValue });
		};

		zoomTo(node, factor, animationDuration = 0){
			let view = this.scene.view;

			let camera = this.scene.cameraP.clone();
			camera.rotation.copy(this.scene.cameraP.rotation);
			camera.rotation.order = "ZXY";
			camera.rotation.x = Math.PI / 2 + view.pitch;
			camera.rotation.z = view.yaw;
			camera.updateMatrix();
			camera.updateMatrixWorld();
			camera.zoomTo(node, factor);

			let bs;
			if (node.boundingSphere) {
				bs = node.boundingSphere;
			} else if (node.geometry && node.geometry.boundingSphere) {
				bs = node.geometry.boundingSphere;
			} else {
				bs = node.boundingBox.getBoundingSphere(new Sphere());
			}
			bs = bs.clone().applyMatrix4(node.matrixWorld); 

			let startPosition = view.position.clone();
			let endPosition = camera.position.clone();
			let startTarget = view.getPivot();
			let endTarget = bs.center;
			let startRadius = view.radius;
			let endRadius = endPosition.distanceTo(endTarget);

			let easing = TWEEN.Easing.Quartic.Out;

			{ // animate camera position
				let pos = startPosition.clone();
				let tween = new TWEEN.Tween(pos).to(endPosition, animationDuration);
				tween.easing(easing);

				tween.onUpdate(() => {
					view.position.copy(pos);
				});

				tween.start();
			}

			{ // animate camera target
				let target = startTarget.clone();
				let tween = new TWEEN.Tween(target).to(endTarget, animationDuration);
				tween.easing(easing);
				tween.onUpdate(() => {
					view.lookAt(target);
				});
				tween.onComplete(() => {
					view.lookAt(target);
					this.dispatchEvent({type: 'focusing_finished', target: this});
				});

				this.dispatchEvent({type: 'focusing_started', target: this});
				tween.start();
			}
		};

		moveToGpsTimeVicinity(time){
			const result = Potree.Utils.findClosestGpsTime(time, viewer);

			const box  = result.node.pointcloud.deepestNodeAt(result.position).getBoundingBox();
			const diameter = box.min.distanceTo(box.max);

			const camera = this.scene.getActiveCamera();
			const offset = camera.getWorldDirection(new Vector3()).multiplyScalar(diameter);
			const newCamPos = result.position.clone().sub(offset);

			this.scene.view.position.copy(newCamPos);
			this.scene.view.lookAt(result.position);
		}

		showAbout () {
			$(function () {
				$('#about-panel').dialog();
			});
		};

		getBoundingBox (pointclouds) {
			return this.scene.getBoundingBox(pointclouds);
		};

		getGpsTimeExtent(){
			const range = [Infinity, -Infinity];

			for(const pointcloud of this.scene.pointclouds){
				const attributes = pointcloud.pcoGeometry.pointAttributes.attributes;
				const aGpsTime = attributes.find(a => a.name === "gps-time");

				if(aGpsTime){
					range[0] = Math.min(range[0], aGpsTime.range[0]);
					range[1] = Math.max(range[1], aGpsTime.range[1]);
				}
			}

			return range;
		}

		fitToScreen (factor = 1, animationDuration = 0) {
			let box = this.getBoundingBox(this.scene.pointclouds);

			let node = new Object3D();
			node.boundingBox = box;

			this.zoomTo(node, factor, animationDuration);
			this.controls.stop();
		};

		toggleNavigationCube() {
			this.navigationCube.visible = !this.navigationCube.visible;
		}

		setView(view) {
			if(!view) return;

			switch(view) {
				case "F":
					this.setFrontView();
					break;
				case "B":
					this.setBackView();
					break;
				case "L":
					this.setLeftView();
					break;
				case "R":
					this.setRightView();
					break;
				case "U":
					this.setTopView();
					break;
				case "D":
					this.setBottomView();
					break;
			}
		}
		
		setTopView(){
			this.scene.view.yaw = 0;
			this.scene.view.pitch = -Math.PI / 2;

			this.fitToScreen();
		};
		
		setBottomView(){
			this.scene.view.yaw = -Math.PI;
			this.scene.view.pitch = Math.PI / 2;
			
			this.fitToScreen();
		};

		setFrontView(){
			this.scene.view.yaw = 0;
			this.scene.view.pitch = 0;

			this.fitToScreen();
		};
		
		setBackView(){
			this.scene.view.yaw = Math.PI;
			this.scene.view.pitch = 0;
			
			this.fitToScreen();
		};

		setLeftView(){
			this.scene.view.yaw = -Math.PI / 2;
			this.scene.view.pitch = 0;

			this.fitToScreen();
		};

		setRightView () {
			this.scene.view.yaw = Math.PI / 2;
			this.scene.view.pitch = 0;

			this.fitToScreen();
		};

		flipYZ () {
			this.isFlipYZ = !this.isFlipYZ;

			// TODO flipyz
			console.log('TODO');
		}
		
		setCameraMode(mode){
			this.scene.cameraMode = mode;

			for(let pointcloud of this.scene.pointclouds) {
				pointcloud.material.useOrthographicCamera = mode == CameraMode.ORTHOGRAPHIC;
			}
		}

		getProjection(){
			const pointcloud = this.scene.pointclouds[0];

			if(pointcloud){
				return pointcloud.projection;
			}else {
				return null;
			}
		}

		async loadProject(url){

			const response = await fetch(url);
		
			const text = await response.text();
			const json = lib.parse(text);
			// const json = JSON.parse(text);

			if(json.type === "Potree"){
				Potree.loadProject(viewer, json);
			}

			//Potree.loadProject(this, url);
		}

		saveProject(){
			return Potree.saveProject(this);
		}
		
		loadSettingsFromURL(){
			if(Utils.getParameterByName("pointSize")){
				this.setPointSize(parseFloat(Utils.getParameterByName("pointSize")));
			}
			
			if(Utils.getParameterByName("FOV")){
				this.setFOV(parseFloat(Utils.getParameterByName("FOV")));
			}
			
			if(Utils.getParameterByName("opacity")){
				this.setOpacity(parseFloat(Utils.getParameterByName("opacity")));
			}
			
			if(Utils.getParameterByName("edlEnabled")){
				let enabled = Utils.getParameterByName("edlEnabled") === "true";
				this.setEDLEnabled(enabled);
			}

			if (Utils.getParameterByName('edlRadius')) {
				this.setEDLRadius(parseFloat(Utils.getParameterByName('edlRadius')));
			}

			if (Utils.getParameterByName('edlStrength')) {
				this.setEDLStrength(parseFloat(Utils.getParameterByName('edlStrength')));
			}

			if (Utils.getParameterByName('pointBudget')) {
				this.setPointBudget(parseFloat(Utils.getParameterByName('pointBudget')));
			}

			if (Utils.getParameterByName('showBoundingBox')) {
				let enabled = Utils.getParameterByName('showBoundingBox') === 'true';
				if (enabled) {
					this.setShowBoundingBox(true);
				} else {
					this.setShowBoundingBox(false);
				}
			}

			if (Utils.getParameterByName('material')) {
				let material = Utils.getParameterByName('material');
				this.setMaterial(material);
			}

			if (Utils.getParameterByName('pointSizing')) {
				let sizing = Utils.getParameterByName('pointSizing');
				this.setPointSizing(sizing);
			}

			if (Utils.getParameterByName('quality')) {
				let quality = Utils.getParameterByName('quality');
				this.setQuality(quality);
			}

			if (Utils.getParameterByName('position')) {
				let value = Utils.getParameterByName('position');
				value = value.replace('[', '').replace(']', '');
				let tokens = value.split(';');
				let x = parseFloat(tokens[0]);
				let y = parseFloat(tokens[1]);
				let z = parseFloat(tokens[2]);

				this.scene.view.position.set(x, y, z);
			}

			if (Utils.getParameterByName('target')) {
				let value = Utils.getParameterByName('target');
				value = value.replace('[', '').replace(']', '');
				let tokens = value.split(';');
				let x = parseFloat(tokens[0]);
				let y = parseFloat(tokens[1]);
				let z = parseFloat(tokens[2]);

				this.scene.view.lookAt(new Vector3(x, y, z));
			}

			if (Utils.getParameterByName('background')) {
				let value = Utils.getParameterByName('background');
				this.setBackground(value);
			}

			// if(Utils.getParameterByName("elevationRange")){
			//	let value = Utils.getParameterByName("elevationRange");
			//	value = value.replace("[", "").replace("]", "");
			//	let tokens = value.split(";");
			//	let x = parseFloat(tokens[0]);
			//	let y = parseFloat(tokens[1]);
			//
			//	this.setElevationRange(x, y);
			//	//this.scene.view.target.set(x, y, z);
			// }
		};

		// ------------------------------------------------------------------------------------
		// Viewer Internals
		// ------------------------------------------------------------------------------------

		createControls () {
			{ // create FIRST PERSON CONTROLS
				this.fpControls = new FirstPersonControls(this);
				this.fpControls.enabled = false;
				this.fpControls.addEventListener('start', this.disableAnnotations.bind(this));
				this.fpControls.addEventListener('end', this.enableAnnotations.bind(this));
			}

			// { // create GEO CONTROLS
			//	this.geoControls = new GeoControls(this.scene.camera, this.renderer.domElement);
			//	this.geoControls.enabled = false;
			//	this.geoControls.addEventListener("start", this.disableAnnotations.bind(this));
			//	this.geoControls.addEventListener("end", this.enableAnnotations.bind(this));
			//	this.geoControls.addEventListener("move_speed_changed", (event) => {
			//		this.setMoveSpeed(this.geoControls.moveSpeed);
			//	});
			// }

			{ // create ORBIT CONTROLS
				this.orbitControls = new OrbitControls(this);
				this.orbitControls.enabled = false;
				this.orbitControls.addEventListener('start', this.disableAnnotations.bind(this));
				this.orbitControls.addEventListener('end', this.enableAnnotations.bind(this));
			}

			{ // create EARTH CONTROLS
				this.earthControls = new EarthControls(this);
				this.earthControls.enabled = false;
				this.earthControls.addEventListener('start', this.disableAnnotations.bind(this));
				this.earthControls.addEventListener('end', this.enableAnnotations.bind(this));
			}

			{ // create DEVICE ORIENTATION CONTROLS
				this.deviceControls = new DeviceOrientationControls(this);
				this.deviceControls.enabled = false;
				this.deviceControls.addEventListener('start', this.disableAnnotations.bind(this));
				this.deviceControls.addEventListener('end', this.enableAnnotations.bind(this));
			}

			{ // create VR CONTROLS
				this.vrControls = new VRControls(this);
				this.vrControls.enabled = false;
				this.vrControls.addEventListener('start', this.disableAnnotations.bind(this));
				this.vrControls.addEventListener('end', this.enableAnnotations.bind(this));
			}


		};

		toggleSidebar () {
			let renderArea = $('#potree_render_area');
			let isVisible = renderArea.css('left') !== '0px';

			if (isVisible) {
				renderArea.css('left', '0px');
			} else {
				renderArea.css('left', '300px');
			}
		};

		toggleMap () {
			// let map = $('#potree_map');
			// map.toggle(100);

			if (this.mapView) {
				this.mapView.toggle();
			}
		};

		onGUILoaded(callback){
			if(this.guiLoaded){
				callback();
			}else {
				this.guiLoadTasks.push(callback);
			}
		}

		promiseGuiLoaded(){
			return new Promise( resolve => {

				if(this.guiLoaded){
					resolve();
				}else {
					this.guiLoadTasks.push(resolve);
				}
			
			});
		}

		loadGUI(callback){

			if(callback){
				this.onGUILoaded(callback);
			}

			let viewer = this;
			let sidebarContainer = $('#potree_sidebar_container');
			sidebarContainer.load(new URL(Potree.scriptPath + '/sidebar.html').href, () => {
				sidebarContainer.css('width', '300px');
				sidebarContainer.css('height', '100%');

				let imgMenuToggle = document.createElement('img');
				imgMenuToggle.src = new URL(Potree.resourcePath + '/icons/menu_button.svg').href;
				imgMenuToggle.onclick = this.toggleSidebar;
				imgMenuToggle.classList.add('potree_menu_toggle');

				let imgMapToggle = document.createElement('img');
				imgMapToggle.src = new URL(Potree.resourcePath + '/icons/map_icon.png').href;
				imgMapToggle.style.display = 'none';
				imgMapToggle.onclick = e => { this.toggleMap(); };
				imgMapToggle.id = 'potree_map_toggle';

				

				let elButtons = $("#potree_quick_buttons").get(0);

				elButtons.append(imgMenuToggle);
				elButtons.append(imgMapToggle);


				VRButton.createButton(this.renderer).then(vrButton => {

					if(vrButton == null){
						console.log("VR not supported or active.");

						return;
					}

					this.renderer.xr.enabled = true;

					let element = vrButton.element;

					element.style.position = "";
					element.style.bottom = "";
					element.style.left = "";
					element.style.margin = "4px";
					element.style.fontSize = "100%";
					element.style.width = "2.5em";
					element.style.height = "2.5em";
					element.style.padding = "0";
					element.style.textShadow = "black 2px 2px 2px";
					element.style.display = "block";

					elButtons.append(element);

					vrButton.onStart(() => {
						this.dispatchEvent({type: "vr_start"});
					});

					vrButton.onEnd(() => {
						this.dispatchEvent({type: "vr_end"});
					});
				});

				this.mapView = new MapView(this);
				this.mapView.init();

				i18n.init({
					lng: 'en',
					resGetPath: Potree.resourcePath + '/lang/__lng__/__ns__.json',
					preload: ['en', 'fr', 'de', 'jp', 'se', 'es'],
					getAsync: true,
					debug: false
				}, function (t) {
					// Start translation once everything is loaded
					$('body').i18n();
				});

				$(() => {
					//initSidebar(this);
					let sidebar = new Sidebar(this);
					sidebar.init();

					this.sidebar = sidebar;

					//if (callback) {
					//	$(callback);
					//}

					let elProfile = $('<div>').load(new URL(Potree.scriptPath + '/profile.html').href, () => {
						$(document.body).append(elProfile.children());
						this.profileWindow = new ProfileWindow(this);
						this.profileWindowController = new ProfileWindowController(this);

						$('#profile_window').draggable({
							handle: $('#profile_titlebar'),
							containment: $(document.body)
						});
						$('#profile_window').resizable({
							containment: $(document.body),
							handles: 'n, e, s, w'
						});

						$(() => {
							this.guiLoaded = true;
							for(let task of this.guiLoadTasks){
								task();
							}

						});
					});

					

				});

				
			});

			return this.promiseGuiLoaded();
		}

		setLanguage (lang) {
			i18n.setLng(lang);
			$('body').i18n();
		}

		setServer (server) {
			this.server = server;
		}

		initDragAndDrop(){
			function allowDrag(e) {
				e.dataTransfer.dropEffect = 'copy';
				e.preventDefault();
			}

			let dropHandler = async (event) => {
				console.log(event);
				event.preventDefault();

				for(const item of event.dataTransfer.items){
					console.log(item);

					if(item.kind !== "file"){
						continue;
					}

					const file = item.getAsFile();

					const isJson = file.name.toLowerCase().endsWith(".json");
					const isGeoPackage = file.name.toLowerCase().endsWith(".gpkg");

					if(isJson){
						try{

							const text = await file.text();
							const json = JSON.parse(text);

							if(json.type === "Potree"){
								Potree.loadProject(viewer, json);
							}
						}catch(e){
							console.error("failed to parse the dropped file as JSON");
							console.error(e);
						}
					}else if(isGeoPackage){
						const hasPointcloud = viewer.scene.pointclouds.length > 0;

						if(!hasPointcloud){
							let msg = "At least one point cloud is needed that specifies the ";
							msg += "coordinate reference system before loading vector data.";
							console.error(msg);
						}else {

							proj4.defs("WGS84", "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs");
							proj4.defs("pointcloud", this.getProjection());
							let transform = proj4("WGS84", "pointcloud");

							const buffer = await file.arrayBuffer();

							const params = {
								transform: transform,
								source: file.name,
							};
							
							const geo = await Potree.GeoPackageLoader.loadBuffer(buffer, params);
							viewer.scene.addGeopackage(geo);
						}
					}
					
				}
			};


			$("body")[0].addEventListener("dragenter", allowDrag);
			$("body")[0].addEventListener("dragover", allowDrag);
			$("body")[0].addEventListener("drop", dropHandler);
		}

		initThree () {

			console.log(`initializing three.js ${REVISION}`);

			let width = this.renderArea.clientWidth;
			let height = this.renderArea.clientHeight;

			let contextAttributes = {
				alpha: true,
				depth: true,
				stencil: false,
				antialias: false,
				//premultipliedAlpha: _premultipliedAlpha,
				preserveDrawingBuffer: true,
				powerPreference: "high-performance",
			};

			// let contextAttributes = {
			// 	alpha: false,
			// 	preserveDrawingBuffer: true,
			// };

			// let contextAttributes = {
			// 	alpha: false,
			// 	preserveDrawingBuffer: true,
			// };

			let canvas = document.createElement("canvas");

			let context = canvas.getContext('webgl', contextAttributes );

			this.renderer = new WebGLRenderer({
				alpha: true, 
				premultipliedAlpha: false,
				canvas: canvas,
				context: context});
			this.renderer.sortObjects = false;
			this.renderer.setSize(width, height);
			this.renderer.autoClear = false;
			this.renderArea.appendChild(this.renderer.domElement);
			this.renderer.domElement.tabIndex = '2222';
			this.renderer.domElement.style.position = 'absolute';
			this.renderer.domElement.addEventListener('mousedown', () => {
				this.renderer.domElement.focus();
			});
			//this.renderer.domElement.focus();

			// NOTE: If extension errors occur, pass the string into this.renderer.extensions.get(x) before enabling
			// enable frag_depth extension for the interpolation shader, if available
			let gl = this.renderer.getContext();
			gl.getExtension('EXT_frag_depth');
			gl.getExtension('WEBGL_depth_texture');
			gl.getExtension('WEBGL_color_buffer_float'); 	// Enable explicitly for more portability, EXT_color_buffer_float is the proper name in WebGL 2
			
			if(gl.createVertexArray == null){
				let extVAO = gl.getExtension('OES_vertex_array_object');

				if(!extVAO){
					throw new Error("OES_vertex_array_object extension not supported");
				}

				gl.createVertexArray = extVAO.createVertexArrayOES.bind(extVAO);
				gl.bindVertexArray = extVAO.bindVertexArrayOES.bind(extVAO);
			}
			
		}

		updateAnnotations () {

			if(!this.visibleAnnotations){
				this.visibleAnnotations = new Set();
			}

			this.scene.annotations.updateBounds();
			this.scene.cameraP.updateMatrixWorld();
			this.scene.cameraO.updateMatrixWorld();
			
			let distances = [];

			let renderAreaSize = this.renderer.getSize(new Vector2());

			let viewer = this;

			let visibleNow = [];
			this.scene.annotations.traverse(annotation => {

				if (annotation === this.scene.annotations) {
					return true;
				}

				if (!annotation.visible) {
					return false;
				}

				annotation.scene = this.scene;

				let element = annotation.domElement;

				let position = annotation.position.clone();
				position.add(annotation.offset);
				if (!position) {
					position = annotation.boundingBox.getCenter(new Vector3());
				}

				let distance = viewer.scene.cameraP.position.distanceTo(position);
				let radius = annotation.boundingBox.getBoundingSphere(new Sphere()).radius;

				let screenPos = new Vector3();
				let screenSize = 0;

				{
					// SCREEN POS
					screenPos.copy(position).project(this.scene.getActiveCamera());
					screenPos.x = renderAreaSize.x * (screenPos.x + 1) / 2;
					screenPos.y = renderAreaSize.y * (1 - (screenPos.y + 1) / 2);


					// SCREEN SIZE
					if(viewer.scene.cameraMode == CameraMode.PERSPECTIVE) {
						let fov = Math.PI * viewer.scene.cameraP.fov / 180;
						let slope = Math.tan(fov / 2.0);
						let projFactor =  0.5 * renderAreaSize.y / (slope * distance);
						screenSize = radius * projFactor;
					} else {
						screenSize = Utils.projectedRadiusOrtho(radius, viewer.scene.cameraO.projectionMatrix, renderAreaSize.x, renderAreaSize.y);
					}
				}

				element.css("left", screenPos.x + "px");
				element.css("top", screenPos.y + "px");
				//element.css("display", "block");

				let zIndex = 10000000 - distance * (10000000 / this.scene.cameraP.far);
				if(annotation.descriptionVisible){
					zIndex += 10000000;
				}
				element.css("z-index", parseInt(zIndex));

				if(annotation.children.length > 0){
					let expand = screenSize > annotation.collapseThreshold || annotation.boundingBox.containsPoint(this.scene.getActiveCamera().position);
					annotation.expand = expand;

					if (!expand) {
						//annotation.display = (screenPos.z >= -1 && screenPos.z <= 1);
						let inFrustum = (screenPos.z >= -1 && screenPos.z <= 1);
						if(inFrustum){
							visibleNow.push(annotation);
						}
					}

					return expand;
				} else {
					//annotation.display = (screenPos.z >= -1 && screenPos.z <= 1);
					let inFrustum = (screenPos.z >= -1 && screenPos.z <= 1);
					if(inFrustum){
						visibleNow.push(annotation);
					}
				}
				
			});

			let notVisibleAnymore = new Set(this.visibleAnnotations);
			for(let annotation of visibleNow){
				annotation.display = true;
				
				notVisibleAnymore.delete(annotation);
			}
			this.visibleAnnotations = visibleNow;

			for(let annotation of notVisibleAnymore){
				annotation.display = false;
			}

		}

		updateMaterialDefaults(pointcloud){
			// PROBLEM STATEMENT:
			// * [min, max] of intensity, source id, etc. are computed as point clouds are loaded
			// * the point cloud material won't know the range it should use until some data is loaded
			// * users can modify the range at runtime, but sensible default ranges should be 
			//   applied even if no GUI is present
			// * display ranges shouldn't suddenly change even if the actual range changes over time.
			//   e.g. the root node has intensity range [1, 478]. One of the descendants increases range to 
			//   [0, 2047]. We should not automatically change to the new range because that would result
			//   in sudden and drastic changes of brightness. We should adjust the min/max of the sidebar slider.

			const material = pointcloud.material;

			const attIntensity = pointcloud.getAttribute("intensity");
			
			if(attIntensity != null && material.intensityRange[0] === Infinity){
				material.intensityRange = [...attIntensity.range];
			}

			// const attIntensity = pointcloud.getAttribute("intensity");
			// if(attIntensity && material.intensityRange[0] === Infinity){
			// 	material.intensityRange = [...attIntensity.range];
			// }

			// let attributes = pointcloud.getAttributes();

			// for(let attribute of attributes.attributes){
			// 	if(attribute.range){
			// 		let range = [...attribute.range];
			// 		material.computedRange.set(attribute.name, range);
			// 		//material.setRange(attribute.name, range);
			// 	}
			// }


		}

		update(delta, timestamp){

			if(Potree.measureTimings) performance.mark("update-start");

			this.dispatchEvent({
				type: 'update_start',
				delta: delta,
				timestamp: timestamp});

			
			const scene = this.scene;
			const camera = scene.getActiveCamera();
			const visiblePointClouds = this.scene.pointclouds.filter(pc => pc.visible);
			
			Potree.pointLoadLimit = Potree.pointBudget * 2;

			const lTarget = camera.position.clone().add(camera.getWorldDirection(new Vector3()).multiplyScalar(1000));
			this.scene.directionalLight.position.copy(camera.position);
			this.scene.directionalLight.lookAt(lTarget);


			for (let pointcloud of visiblePointClouds) {

				pointcloud.showBoundingBox = this.showBoundingBox;
				pointcloud.generateDEM = this.generateDEM;
				pointcloud.minimumNodePixelSize = this.minNodeSize;

				let material = pointcloud.material;

				material.uniforms.uFilterReturnNumberRange.value = this.filterReturnNumberRange;
				material.uniforms.uFilterNumberOfReturnsRange.value = this.filterNumberOfReturnsRange;
				material.uniforms.uFilterGPSTimeClipRange.value = this.filterGPSTimeRange;
				material.uniforms.uFilterPointSourceIDClipRange.value = this.filterPointSourceIDRange;

				material.classification = this.classifications;
				material.recomputeClassification();

				this.updateMaterialDefaults(pointcloud);
			}

			{
				if(this.showBoundingBox){
					let bbRoot = this.scene.scene.getObjectByName("potree_bounding_box_root");
					if(!bbRoot){
						let node = new Object3D();
						node.name = "potree_bounding_box_root";
						this.scene.scene.add(node);
						bbRoot = node;
					}

					let visibleBoxes = [];
					for(let pointcloud of this.scene.pointclouds){
						for(let node of pointcloud.visibleNodes.filter(vn => vn.boundingBoxNode !== undefined)){
							let box = node.boundingBoxNode;
							visibleBoxes.push(box);
						}
					}

					bbRoot.children = visibleBoxes;
				}
			}

			if (!this.freeze) {
				let result = Potree.updatePointClouds(scene.pointclouds, camera, this.renderer);


				// DEBUG - ONLY DISPLAY NODES THAT INTERSECT MOUSE
				//if(false){ 

				//	let renderer = viewer.renderer;
				//	let mouse = viewer.inputHandler.mouse;

				//	let nmouse = {
				//		x: (mouse.x / renderer.domElement.clientWidth) * 2 - 1,
				//		y: -(mouse.y / renderer.domElement.clientHeight) * 2 + 1
				//	};

				//	let pickParams = {};

				//	//if(params.pickClipped){
				//	//	pickParams.pickClipped = params.pickClipped;
				//	//}

				//	pickParams.x = mouse.x;
				//	pickParams.y = renderer.domElement.clientHeight - mouse.y;

				//	let raycaster = new THREE.Raycaster();
				//	raycaster.setFromCamera(nmouse, camera);
				//	let ray = raycaster.ray;

				//	for(let pointcloud of scene.pointclouds){
				//		let nodes = pointcloud.nodesOnRay(pointcloud.visibleNodes, ray);
				//		pointcloud.visibleNodes = nodes;

				//	}
				//}

				// const tStart = performance.now();
				// const worldPos = new THREE.Vector3();
				// const camPos = viewer.scene.getActiveCamera().getWorldPosition(new THREE.Vector3());
				// let lowestDistance = Infinity;
				// let numNodes = 0;

				// viewer.scene.scene.traverse(node => {
				// 	node.getWorldPosition(worldPos);

				// 	const distance = worldPos.distanceTo(camPos);

				// 	lowestDistance = Math.min(lowestDistance, distance);

				// 	numNodes++;

				// 	if(Number.isNaN(distance)){
				// 		console.error(":(");
				// 	}
				// });
				// const duration = (performance.now() - tStart).toFixed(2);

				// Potree.debug.computeNearDuration = duration;
				// Potree.debug.numNodes = numNodes;

				//console.log(lowestDistance.toString(2), duration);

				const tStart = performance.now();
				const campos = camera.position;
				let closestImage = Infinity;
				for(const images of this.scene.orientedImages){
					for(const image of images.images){
						const distance = image.mesh.position.distanceTo(campos);

						closestImage = Math.min(closestImage, distance);
					}
				}
				const tEnd = performance.now();

				if(result.lowestSpacing !== Infinity){
					let near = result.lowestSpacing * 10.0;
					let far = -this.getBoundingBox().applyMatrix4(camera.matrixWorldInverse).min.z;

					far = Math.max(far * 1.5, 10000);
					near = Math.min(100.0, Math.max(0.01, near));
					near = Math.min(near, closestImage);
					far = Math.max(far, near + 10000);

					if(near === Infinity){
						near = 0.1;
					}
					
					camera.near = near;
					camera.far = far;
				}else {
					// don't change near and far in this case
				}

				if(this.scene.cameraMode == CameraMode.ORTHOGRAPHIC) {
					camera.near = -camera.far;
				}
			} 
			
			this.scene.cameraP.fov = this.fov;
			
			let controls = this.getControls();
			if (controls === this.deviceControls) {
				this.controls.setScene(scene);
				this.controls.update(delta);

				this.scene.cameraP.position.copy(scene.view.position);
				this.scene.cameraO.position.copy(scene.view.position);
			} else if (controls !== null) {
				controls.setScene(scene);
				controls.update(delta);

				if(typeof debugDisabled === "undefined" ){
					this.scene.cameraP.position.copy(scene.view.position);
					this.scene.cameraP.rotation.order = "ZXY";
					this.scene.cameraP.rotation.x = Math.PI / 2 + this.scene.view.pitch;
					this.scene.cameraP.rotation.z = this.scene.view.yaw;
				}

				this.scene.cameraO.position.copy(scene.view.position);
				this.scene.cameraO.rotation.order = "ZXY";
				this.scene.cameraO.rotation.x = Math.PI / 2 + this.scene.view.pitch;
				this.scene.cameraO.rotation.z = this.scene.view.yaw;
			}
			
			camera.updateMatrix();
			camera.updateMatrixWorld();
			camera.matrixWorldInverse.copy(camera.matrixWorld).invert();

			{
				if(this._previousCamera === undefined){
					this._previousCamera = this.scene.getActiveCamera().clone();
					this._previousCamera.rotation.copy(this.scene.getActiveCamera().rotation);
				}

				if(!this._previousCamera.matrixWorld.equals(camera.matrixWorld)){
					this.dispatchEvent({
						type: "camera_changed",
						previous: this._previousCamera,
						camera: camera
					});
				}else if(!this._previousCamera.projectionMatrix.equals(camera.projectionMatrix)){
					this.dispatchEvent({
						type: "camera_changed",
						previous: this._previousCamera,
						camera: camera
					});
				}

				this._previousCamera = this.scene.getActiveCamera().clone();
				this._previousCamera.rotation.copy(this.scene.getActiveCamera().rotation);

			}

			{ // update clip boxes
				let boxes = [];
				
				// volumes with clipping enabled
				//boxes.push(...this.scene.volumes.filter(v => (v.clip)));
				boxes.push(...this.scene.volumes.filter(v => (v.clip && v instanceof BoxVolume)));

				// profile segments
				for(let profile of this.scene.profiles){
					boxes.push(...profile.boxes);
				}
				
				// Needed for .getInverse(), pre-empt a determinant of 0, see #815 / #816
				let degenerate = (box) => box.matrixWorld.determinant() !== 0;
				
				let clipBoxes = boxes.filter(degenerate).map( box => {
					box.updateMatrixWorld();
					
					let boxInverse = box.matrixWorld.clone().invert();
					let boxPosition = box.getWorldPosition(new Vector3());

					return {box: box, inverse: boxInverse, position: boxPosition};
				});

				let clipPolygons = this.scene.polygonClipVolumes.filter(vol => vol.initialized);
				
				// set clip volumes in material
				for(let pointcloud of visiblePointClouds){
					pointcloud.material.setClipBoxes(clipBoxes);
					pointcloud.material.setClipPolygons(clipPolygons, this.clippingTool.maxPolygonVertices);
					pointcloud.material.clipTask = this.clipTask;
					pointcloud.material.clipMethod = this.clipMethod;
				}
			}

			{
				for(let pointcloud of visiblePointClouds){
					pointcloud.material.elevationGradientRepeat = this.elevationGradientRepeat;
				}
			}
			
			{ // update navigation cube
				this.navigationCube.update(camera.rotation);
			}

			this.updateAnnotations();
			
			if(this.mapView){
				this.mapView.update(delta);
				if(this.mapView.sceneProjection){
					$( "#potree_map_toggle" ).css("display", "block");
					
				}
			}

			TWEEN.update(timestamp);

			this.dispatchEvent({
				type: 'update',
				delta: delta,
				timestamp: timestamp});
				
			if(Potree.measureTimings) {
				performance.mark("update-end");
				performance.measure("update", "update-start", "update-end");
			}
		}

		getPRenderer(){
			if(this.useHQ){
				if (!this.hqRenderer) {
					this.hqRenderer = new HQSplatRenderer(this);
				}
				this.hqRenderer.useEDL = this.useEDL;

				return this.hqRenderer;
			}else {
				if (this.useEDL && Features.SHADER_EDL.isSupported()) {
					if (!this.edlRenderer) {
						this.edlRenderer = new EDLRenderer(this);
					}

					return this.edlRenderer;
				} else {
					if (!this.potreeRenderer) {
						this.potreeRenderer = new PotreeRenderer(this);
					}

					return this.potreeRenderer;
				}
			}
		}

		renderVR(){

			let renderer = this.renderer;

			renderer.setClearColor(0x550000, 0);
			renderer.clear();

			let xr = renderer.xr;
			let dbg = new PerspectiveCamera();
			let xrCameras = xr.getCamera(dbg);

			if(xrCameras.cameras.length !== 2){
				return;
			}

			let makeCam = this.vrControls.getCamera.bind(this.vrControls);

			{ // clear framebuffer
				if(viewer.background === "skybox"){
					renderer.setClearColor(0xff0000, 1);
				}else if(viewer.background === "gradient"){
					renderer.setClearColor(0x112233, 1);
				}else if(viewer.background === "black"){
					renderer.setClearColor(0x000000, 1);
				}else if(viewer.background === "white"){
					renderer.setClearColor(0xFFFFFF, 1);
				}else {
					renderer.setClearColor(0x000000, 0);
				}

				renderer.clear();
			}

			// render background
			if(this.background === "skybox"){
				let {skybox} = this;

				let cam = makeCam();
				skybox.camera.rotation.copy(cam.rotation);
				skybox.camera.fov = cam.fov;
				skybox.camera.aspect = cam.aspect;
				
				// let dbg = new THREE.Object3D();
				let dbg = skybox.parent;
				// dbg.up.set(0, 0, 1);
				dbg.rotation.x = Math.PI / 2;

				// skybox.camera.parent = dbg;
				// dbg.children.push(skybox.camera);

				dbg.updateMatrix();
				dbg.updateMatrixWorld();

				skybox.camera.updateMatrix();
				skybox.camera.updateMatrixWorld();
				skybox.camera.updateProjectionMatrix();

				renderer.render(skybox.scene, skybox.camera);
				// renderer.render(skybox.scene, cam);
			}else if(this.background === "gradient"){
				// renderer.render(this.scene.sceneBG, this.scene.cameraBG);
			}

			this.renderer.xr.getSession().updateRenderState({
				depthNear: 0.1,
				depthFar: 10000
			});
			
			let cam = null;
			let view = null;

			{ // render world scene
				cam = makeCam();
				cam.position.z -= 0.8 * cam.scale.x;
				cam.parent = null;
				// cam.near = 0.05;
				cam.near = viewer.scene.getActiveCamera().near;
				cam.far = viewer.scene.getActiveCamera().far;
				cam.updateMatrix();
				cam.updateMatrixWorld();

				this.scene.scene.updateMatrix();
				this.scene.scene.updateMatrixWorld();
				this.scene.scene.matrixAutoUpdate = false;

				let camWorld = cam.matrixWorld.clone();
				view = camWorld.clone().invert();
				this.scene.scene.matrix.copy(view);
				this.scene.scene.matrixWorld.copy(view);

				cam.matrix.identity();
				cam.matrixWorld.identity();
				cam.matrixWorldInverse.identity();

				renderer.render(this.scene.scene, cam);

				this.scene.scene.matrixWorld.identity();

			}
			
			for(let pointcloud of this.scene.pointclouds){

				let viewport = xrCameras.cameras[0].viewport;

				pointcloud.material.useEDL = false;
				pointcloud.screenHeight = viewport.height;
				pointcloud.screenWidth = viewport.width;

				// automatically switch to paraboloids because they cause far less flickering in VR, 
				// when point sizes are larger than around 2 pixels
				// if(Features.SHADER_INTERPOLATION.isSupported()){
				// 	pointcloud.material.shape = Potree.PointShape.PARABOLOID;
				// }
			}
			
			// render point clouds
			for(let xrCamera of xrCameras.cameras){

				let v = xrCamera.viewport;
				renderer.setViewport(v.x, v.y, v.width, v.height);

				// xrCamera.fov = 90;

				{ // estimate VR fov
					let proj = xrCamera.projectionMatrix;
					let inv = proj.clone().invert();

					let p1 = new Vector4(0, 1, -1, 1).applyMatrix4(inv);
					let rad = p1.y;
					let fov = 180 * (rad / Math.PI);

					xrCamera.fov = fov;
				}

				for(let pointcloud of this.scene.pointclouds){
					const {material} = pointcloud;
					material.useEDL = false;
				}

				let vrWorld = view.clone().invert();
				vrWorld.multiply(xrCamera.matrixWorld);
				let vrView = vrWorld.clone().invert();

				this.pRenderer.render(this.scene.scenePointCloud, xrCamera, null, {
					viewOverride: vrView,
				});

			}

			{ // render VR scene
				let cam = makeCam();
				cam.parent = null;

				renderer.render(this.sceneVR, cam);
			}

			renderer.resetState();

		}

		renderDefault(){
			let pRenderer = this.getPRenderer();

			{ // resize
				const width = this.scaleFactor * this.renderArea.clientWidth;
				const height = this.scaleFactor * this.renderArea.clientHeight;

				this.renderer.setSize(width, height);
				const pixelRatio = this.renderer.getPixelRatio();
				const aspect = width / height;

				const scene = this.scene;

				scene.cameraP.aspect = aspect;
				scene.cameraP.updateProjectionMatrix();

				let frustumScale = this.scene.view.radius;
				scene.cameraO.left = -frustumScale;
				scene.cameraO.right = frustumScale;
				scene.cameraO.top = frustumScale * 1 / aspect;
				scene.cameraO.bottom = -frustumScale * 1 / aspect;
				scene.cameraO.updateProjectionMatrix();

				scene.cameraScreenSpace.top = 1/aspect;
				scene.cameraScreenSpace.bottom = -1/aspect;
				scene.cameraScreenSpace.updateProjectionMatrix();
			}

			pRenderer.clear();

			pRenderer.render(this.renderer);
			this.renderer.render(this.overlay, this.overlayCamera);
		}
		
		render(){
			if(Potree.measureTimings) performance.mark("render-start");

			try{

				const vrActive = this.renderer.xr.isPresenting;

				if(vrActive){
					this.renderVR();
				}else {
					this.renderDefault();
				}

			}catch(e){
				this.onCrash(e);
			}
			
			if(Potree.measureTimings){
				performance.mark("render-end");
				performance.measure("render", "render-start", "render-end");
			}
		}

		resolveTimings(timestamp){
			if(Potree.measureTimings){
				if(!this.toggle){
					this.toggle = timestamp;
				}
				let duration = timestamp - this.toggle;
				if(duration > 1000.0){
				
					let measures = performance.getEntriesByType("measure");
					
					let names = new Set();
					for(let measure of measures){
						names.add(measure.name);
					}
					
					let groups = new Map();
					for(let name of names){
						groups.set(name, {
							measures: [],
							sum: 0,
							n: 0,
							min: Infinity,
							max: -Infinity
						});
					}
					
					for(let measure of measures){
						let group = groups.get(measure.name);
						group.measures.push(measure);
						group.sum += measure.duration;
						group.n++;
						group.min = Math.min(group.min, measure.duration);
						group.max = Math.max(group.max, measure.duration);
					}

					let glQueries = Potree.resolveQueries(this.renderer.getContext());
					for(let [key, value] of glQueries){

						let group = {
							measures: value.map(v => {return {duration: v}}),
							sum: value.reduce( (a, i) => a + i, 0),
							n: value.length,
							min: Math.min(...value),
							max: Math.max(...value)
						};

						let groupname = `[tq] ${key}`;
						groups.set(groupname, group);
						names.add(groupname);
					}
					
					for(let [name, group] of groups){
						group.mean = group.sum / group.n;
						group.measures.sort( (a, b) => a.duration - b.duration );
						
						if(group.n === 1){
							group.median = group.measures[0].duration;
						}else if(group.n > 1){
							group.median = group.measures[parseInt(group.n / 2)].duration;
						}
						
					}
					
					let cn = Array.from(names).reduce( (a, i) => Math.max(a, i.length), 0) + 5;
					let cmin = 10;
					let cmed = 10;
					let cmax = 10;
					let csam = 6;
					
					let message = ` ${"NAME".padEnd(cn)} |` 
						+ ` ${"MIN".padStart(cmin)} |`
						+ ` ${"MEDIAN".padStart(cmed)} |`
						+ ` ${"MAX".padStart(cmax)} |`
						+ ` ${"SAMPLES".padStart(csam)} \n`;
					message += ` ${"-".repeat(message.length) }\n`;
					
					names = Array.from(names).sort();
					for(let name of names){
						let group = groups.get(name);
						let min = group.min.toFixed(3);
						let median = group.median.toFixed(3);
						let max = group.max.toFixed(3);
						let n = group.n;
						
						message += ` ${name.padEnd(cn)} |`
							+ ` ${min.padStart(cmin)} |`
							+ ` ${median.padStart(cmed)} |`
							+ ` ${max.padStart(cmax)} |`
							+ ` ${n.toString().padStart(csam)}\n`;
					}
					message += `\n`;
					console.log(message);
					
					performance.clearMarks();
					performance.clearMeasures();
					this.toggle = timestamp;
				}
			}
		}

		loop(timestamp){

			if(this.stats){
				this.stats.begin();
			}

			if(Potree.measureTimings){
				performance.mark("loop-start");
			}

			this.update(this.clock.getDelta(), timestamp);
			this.render();

			// let vrActive = viewer.renderer.xr.isPresenting;
			// if(vrActive){
			// 	this.update(this.clock.getDelta(), timestamp);
			// 	this.render();
			// }else{

			// 	this.update(this.clock.getDelta(), timestamp);
			// 	this.render();
			// }


			if(Potree.measureTimings){
				performance.mark("loop-end");
				performance.measure("loop", "loop-start", "loop-end");
			}
			
			this.resolveTimings(timestamp);

			Potree.framenumber++;

			if(this.stats){
				this.stats.end();
			}
		}

		postError(content, params = {}){
			let message = this.postMessage(content, params);

			message.element.addClass("potree_message_error");

			return message;
		}

		postMessage(content, params = {}){
			let message = new Message(content);

			let animationDuration = 100;

			message.element.css("display", "none");
			message.elClose.click( () => {
				message.element.slideToggle(animationDuration);

				let index = this.messages.indexOf(message);
				if(index >= 0){
					this.messages.splice(index, 1);
				}
			});

			this.elMessages.prepend(message.element);

			message.element.slideToggle(animationDuration);

			this.messages.push(message);

			if(params.duration !== undefined){
				let fadeDuration = 500;
				let slideOutDuration = 200;
				setTimeout(() => {
					message.element.animate({
						opacity: 0	
					}, fadeDuration);
					message.element.slideToggle(slideOutDuration);
				}, params.duration);
			}

			return message;
		}
	};

	class PointCloudZappchaGeometryNode{

		constructor(){
			this.id = PointCloudZappchaGeometryNode.IDCount++;
			this.bytesPerAxis = 4;
			this.bytesPerVertex = 17;
			this.dataLocation = 0;
			this.children = [];
			this.left = null;
			this.right = null;
			this.boundingBox = null;
			this.number = null;
			this.geometry = null;
			this.loaded = false;
			this.numPoints = 0;
			this.originalNumPoints = 0;
			this.filter = 0;
			this.dataSize = 0;
			this.level = 0;
			this.signedURL = null;
			this.version = 0;
			this.oneTimeDisposeHandlers = [];
			this.maxInt32Bits = 0x1000000; // 2^24
		}

		isGeometryNode(){
			return true;
		}

		isTreeNode(){
			return false;
		}

		isLoaded(){
			return this.loaded;
		}

		getBoundingSphere(){
			return this.boundingSphere;
		}

		getChildren(){
			return this.children;
		}

		getBoundingBox(){
			return this.boundingBox;
		}

		getLevel(){
			return this.level;
		}
		
		completeLoad(successful){
			this.loaded = successful;
			this.loading = false;
			Potree.numNodesLoading--;
		}
		
		load(){
			if (Potree.numNodesLoading >= Potree.maxNodesLoading || this.loaded || this.loading) {
				return;
			}
			
			this.loading = true;
			Potree.numNodesLoading++;
			
			let node = this;
			let buffSize = node.numPoints * node.bytesPerVertex;
			if(this.version===18){
				buffSize = node.dataSize;
			}
			
			fetch(node.signedURL,{
				headers: {
					'range': 'bytes='+node.dataLocation+"-"+(node.dataLocation+BigInt(buffSize)),
					'Cache-Control': 'no-cache,no-store',
				},
			}).then(function(response){
				response.arrayBuffer().then((buffer) => {
					const workerPath = Potree.scriptPath + '/workers/ZappchaWorker.js';
					const worker = Potree.workerPool.getWorker(workerPath);
					
					worker.onmessage = (e) => {
						let position = new Float32Array(e.data.position);
						let color = new Uint8Array(e.data.color);
						let intensities = new Float32Array(e.data.intensities);
						let classifications = new Uint8Array(e.data.classifications);
						let indices = e.data.indices;

						node.geometry = new THREE.BufferGeometry();

						node.geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
						node.geometry.setAttribute('rgba', new THREE.BufferAttribute(color, 4, true));
						node.geometry.setAttribute('intensity', new THREE.BufferAttribute(intensities, 1));
						node.geometry.setAttribute('classification', new THREE.BufferAttribute(classifications, 1));
						node.geometry.setAttribute('indices', new THREE.BufferAttribute(new Uint8Array(indices), 4, true));
						
						Potree.workerPool.returnWorker(workerPath, worker);
						
						node.completeLoad(true);
					};
				
					let message = {
						version : node.version,
						filter : node.filter,
						bytesPerAxis: node.bytesPerAxis,
						bytesPerVertex: node.bytesPerVertex,
						buffer: buffer,
						numPoints: node.numPoints,
						originalNumPoints: node.originalNumPoints,
						minx: node.boundingBox.min.x,
						miny: node.boundingBox.min.y,
						minz: node.boundingBox.min.z,
						maxx: node.boundingBox.max.x,
						maxy: node.boundingBox.max.y,
						maxz: node.boundingBox.max.z
					};
					
					worker.postMessage(message);
				});
			}).catch(function(err){
				console.log("Something went wrong!", err);
				this.completeLoad(false);
			});
		}
		
		dispose(){
			if (this.geometry) {
				this.geometry.dispose();
				this.geometry = null;
				this.loaded = false;

				for (let i = 0; i < this.oneTimeDisposeHandlers.length; i++) {
					let handler = this.oneTimeDisposeHandlers[i];
					handler();
				}
				this.oneTimeDisposeHandlers = [];
			}
		}

		getNumPoints(){
			return this.numPoints;
		}
	};


	PointCloudZappchaGeometryNode.IDCount = 0;


	class PointCloudZappchaGeometry extends EventDispatcher{

		constructor(){
			super();
			this.numPoints = 0;
			this.version = 0;
			this.boundingBox = null;
			this.numNodes = 0;
			this.name = null;
			this.provider = null;
			this.url = null;
			this.root = null;
			this.levels = 0;
			this._spacing = null;
			this.signedURL = null;
			
			{
				const attributes = new PointAttributes();

				attributes.add(PointAttribute.POSITION_CARTESIAN);
				attributes.add(new PointAttribute("rgba", PointAttributeTypes.DATA_TYPE_UINT8, 4));
				attributes.add(new PointAttribute("intensity", PointAttributeTypes.DATA_TYPE_UINT16, 1));
				attributes.add(new PointAttribute("classification", PointAttributeTypes.DATA_TYPE_UINT8, 1));

				this.pointAttributes = attributes;
			}
		}
		
		static async load(key, callback) {
			let signedURL = key;
			let response = await fetch(signedURL, {
				headers: {
					'Range': 'bytes=0-123',
					'Cache-Control':'no-cache,no-store',
				},
			});
			
			let buffer = await response.arrayBuffer();

			try {
				let sourceView = new DataView(buffer);
				const geometry = new PointCloudZappchaGeometry();
				geometry.key = key;
				geometry.provider = "Zappcha";
				geometry.numNodes = sourceView.getInt32(8,true);
				geometry.numPoints = sourceView.getBigInt64(12,true);
				geometry.version =sourceView.getInt32(4,true);
				geometry.signedURL = signedURL;
				let minX = sourceView.getFloat64(20,true);
				let minY = sourceView.getFloat64(28,true);
				let minZ = sourceView.getFloat64(36,true);
				let maxX = sourceView.getFloat64(44,true);
				let maxY = sourceView.getFloat64(52,true);
				let maxZ = sourceView.getFloat64(60,true);
				geometry.boundingBox = new THREE.Box3(
					new THREE.Vector3(minX,minY,minZ),
					new THREE.Vector3(maxX,maxY,maxZ)
				);
				
				let offset = geometry.boundingBox.min.clone().multiplyScalar(-1);

				geometry.boundingBox.min.add(offset);
				geometry.boundingBox.max.add(offset);
				geometry.offset = offset;
				
				console.log(geometry.boundingBox);

				let center = geometry.boundingBox.getCenter(new THREE.Vector3());
				let radius = geometry.boundingBox.getSize(new THREE.Vector3()).length() / 2;
				geometry.boundingSphere = new THREE.Sphere(center, radius);
				
				console.log(minX+" "+minY+" "+minZ);
				
				geometry.loadHierarchy(key);
				callback(geometry);
			
			} catch (e) {
				console.error(e.message);
				callback(null);
			}

		}
		
		
		async loadHierarchy(key){
			let vpc = this;
			
			let nodeSize = 14;
			if(vpc.version===18)
				nodeSize = 21;
			
			var end = 124+(nodeSize * this.numNodes);
			let response = await fetch(vpc.signedURL, {
				headers: {
					'Range': 'bytes=124-'+end,
					'Cache-Control':'no-cache,no-store',
				},
			});
			let buffer = await response.arrayBuffer();
			let view = new DataView(buffer);
			
			let stack = [];
			let root = null;

			let levels = 0;

			// TODO Debug: let start = new Date().getTime();
			// read hierarchy
			for (let i = 0; i < vpc.numNodes; i++) {
				let mask = view.getUint8(i * nodeSize, true);
				let hasLeft = (mask & 1) > 0;
				let hasRight = (mask & 2) > 0;
				let splitX = (mask & 4) > 0;
				let splitY = (mask & 8) > 0;
				let splitZ = (mask & 16) > 0;
				let split = null;
				if (splitX) {
					split = 'X';
				} else if (splitY) {
					split = 'Y';
				} if (splitZ) {
					split = 'Z';
				}

				let node = new PointCloudZappchaGeometryNode();
				node.version = vpc.version;
				node.signedURL = vpc.signedURL;
				node.hasLeft = hasLeft;
				node.hasRight = hasRight;
				node.split = split;
				node.isLeaf = !hasLeft && !hasRight;
				node.number = i;
				node.level = stack.length;
				if(node.version===18){
					node.dataSize = view.getInt32(i * nodeSize + 1, true);
					node.filter = view.getInt32(i*nodeSize+5,true) / 10000.0;
					node.numPoints = view.getInt16(i * nodeSize + 9, true);
					node.originalNumPoints = view.getInt16(i * nodeSize + 11, true);
					node.dataLocation = view.getBigInt64(i * nodeSize + 13, true);
				}
				else {	
					node.numPoints = view.getInt16(i * nodeSize + 2, true);
					node.dataSize = view.getInt16(i * nodeSize + 4, true);
					node.bytesPerVertex = view.getInt8(i * nodeSize + 1, true);
					node.dataLocation = view.getBigInt64(i * nodeSize + 6, true);
				}
				if(node.dataLocation==-1)
					node.dataLocation=BigInt(0);
				node.bytesPerAxis = (node.bytesPerVertex-5)/3;
				
				levels = Math.max(levels, node.level);
				
				if (stack.length > 0) {
					let parent = stack[stack.length - 1];
					node.boundingBox = parent.boundingBox.clone();
					let parentBBSize = parent.boundingBox.getSize(new THREE.Vector3());

					if (parent.hasLeft && !parent.left) {
						parent.left = node;
						parent.children.push(node);
						if (parent.split === 'X') {
							node.boundingBox.max.x = node.boundingBox.min.x + parentBBSize.x / 2;
						} else if (parent.split === 'Y') {
							node.boundingBox.max.y = node.boundingBox.min.y + parentBBSize.y / 2;
						} else if (parent.split === 'Z') {
							node.boundingBox.max.z = node.boundingBox.min.z + parentBBSize.z / 2;
						}

						let center = node.boundingBox.getCenter(new THREE.Vector3());
						let radius = node.boundingBox.getSize(new THREE.Vector3()).length() / 2;
						node.boundingSphere = new THREE.Sphere(center, radius);
					} else {
						parent.right = node;
						parent.children.push(node);
						if (parent.split === 'X') {
							node.boundingBox.min.x = node.boundingBox.min.x + parentBBSize.x / 2;
						} else if (parent.split === 'Y') {
							node.boundingBox.min.y = node.boundingBox.min.y + parentBBSize.y / 2;
						} else if (parent.split === 'Z') {
							node.boundingBox.min.z = node.boundingBox.min.z + parentBBSize.z / 2;
						}

						let center = node.boundingBox.getCenter(new THREE.Vector3());
						let radius = node.boundingBox.getSize(new THREE.Vector3()).length() / 2;
						node.boundingSphere = new THREE.Sphere(center, radius);
					}
				} else {
					root = node;
					root.boundingBox = vpc.boundingBox.clone();
					let center = root.boundingBox.getCenter(new THREE.Vector3());
					let radius = root.boundingBox.getSize(new THREE.Vector3()).length() / 2;
					root.boundingSphere = new THREE.Sphere(center, radius);
				}

				let bbSize = node.boundingBox.getSize(new THREE.Vector3());
				node.spacing = ((bbSize.x + bbSize.y + bbSize.z) / 3) / 75;
				node.estimatedSpacing = node.spacing;

				stack.push(node);

				if (node.isLeaf) {
					let done = false;
					while (!done && stack.length > 0) {
						stack.pop();

						let top = stack[stack.length - 1];

						done = stack.length > 0 && top.hasRight && top.right == null;
					}
				}
			}
			// TODO Debug:
			// let end = new Date().getTime();
			// let parseDuration = end - start;
			// let msg = parseDuration;
			// document.getElementById("lblDebug").innerHTML = msg;

			vpc.root = root;
			vpc.levels = levels;
			console.log(vpc.root);
			
			vpc.dispatchEvent({type: 'hierarchy_loaded'});
		};

		get spacing(){
			if (this._spacing) {
				return this._spacing;
			} else if (this.root) {
				return this.root.spacing;
			} else {
				// TODO ???: null;
			}
		}

		set spacing(value){
			this._spacing = value;
		}

	};

	class PointCloudZappchaNode extends PointCloudTreeNode {
		constructor () {
			super();

			this.left = null;
			this.right = null;
			this.sceneNode = null;
			this.kdtree = null;
		}

		getNumPoints () {
			return this.geometryNode.numPoints;
		}

		isLoaded () {
			return true;
		}

		isTreeNode () {
			return true;
		}

		isGeometryNode () {
			return false;
		}

		getLevel () {
			return this.geometryNode.level;
		}

		getBoundingSphere () {
			return this.geometryNode.boundingSphere;
		}

		getBoundingBox () {
			return this.geometryNode.boundingBox;
		}

		toTreeNode (child) {
			let geometryNode = null;

			if (this.left === child) {
				geometryNode = this.left;
			} else if (this.right === child) {
				geometryNode = this.right;
			}

			if (!geometryNode.loaded) {
				return;
			}

			let node = new PointCloudZappchaNode();
			let sceneNode = THREE.PointCloud(geometryNode.geometry, this.kdtree.material);
			sceneNode.visible = false;

			node.kdtree = this.kdtree;
			node.geometryNode = geometryNode;
			node.sceneNode = sceneNode;
			node.parent = this;
			node.left = this.geometryNode.left;
			node.right = this.geometryNode.right;
		}

		getChildren () {
			let children = [];

			if (this.left) {
				children.push(this.left);
			}

			if (this.right) {
				children.push(this.right);
			}

			return children;
		}
	};

	class PointCloudZappcha extends PointCloudTree{
		constructor (geometry) {
			super();

			this.root = null;
			if (geometry.root) {
				this.root = geometry.root;
			} else {
				geometry.addEventListener('hierarchy_loaded', () => {
					this.root = geometry.root;
				});
			}

			this.visiblePointsTarget = 2 * 1000 * 1000;
			this.minimumNodePixelSize = 150;

			this.position.sub(geometry.offset);
			this.updateMatrix();

			this.numVisibleNodes = 0;
			this.numVisiblePoints = 0;

			this.boundingBoxNodes = [];
			this.loadQueue = [];
			this.visibleNodes = [];

			this.pcoGeometry = geometry;
			this.boundingBox = this.pcoGeometry.boundingBox;
			this.boundingSphere = this.pcoGeometry.boundingSphere;
			this.material = new PointCloudMaterial$1({vertexColors: THREE.VertexColors, size: 0.05, treeType: TreeType.KDTREE});
			this.material.sizeType = PointSizeType.ATTENUATED;
			this.material.size = 0.05;
			this.material.intensityRange=[0,255];
			
			{
				let attributeName = "rgba";
				if(this.pcoGeometry.pointAttributes.attributes.length > 1){
					attributeName = this.pcoGeometry.pointAttributes.attributes[1].name;
				}
				this.material.activeAttributeName = attributeName;
			}
			
			this.profileRequests = [];
			this.name = '';
		}

		getBoundingBoxWorld () {
			this.updateMatrixWorld(true);
			let box = this.boundingBox;
			let transform = this.matrixWorld;
			let tBox = Utils.computeTransformedBoundingBox(box, transform);

			return tBox;
		};

		setName (name) {
			if (this.name !== name) {
				this.name = name;
				this.dispatchEvent({type: 'name_changed', name: name, pointcloud: this});
			}
		}

		getName () {
			return this.name;
		}

		getLevel () {
			return this.level;
		}
		
		getAttribute(name){

			const attribute = this.pcoGeometry.pointAttributes.attributes.find(a => a.name === name);

			if(attribute){
				return attribute;
			}else {
				return null;
			}
		}

		getAttributes(){
			return this.pcoGeometry.pointAttributes;
		}

		toTreeNode (geometryNode, parent) {
			let node = new PointCloudZappchaNode();
			let sceneNode = new THREE.Points(geometryNode.geometry, this.material);

			sceneNode.frustumCulled = false;
			sceneNode.onBeforeRender = (_this, scene, camera, geometry, material, group) => {
				if (material.program) {
					_this.getContext().useProgram(material.program.program);

					if (material.program.getUniforms().map.level) {
						let level = geometryNode.getLevel();
						material.uniforms.level.value = level;
						material.program.getUniforms().map.level.setValue(_this.getContext(), level);
					}

					if (this.visibleNodeTextureOffsets && material.program.getUniforms().map.vnStart) {
						let vnStart = this.visibleNodeTextureOffsets.get(node);
						material.uniforms.vnStart.value = vnStart;
						material.program.getUniforms().map.vnStart.setValue(_this.getContext(), vnStart);
					}

					if (material.program.getUniforms().map.pcIndex) {
						let i = node.pcIndex ? node.pcIndex : this.visibleNodes.indexOf(node);
						material.uniforms.pcIndex.value = i;
						material.program.getUniforms().map.pcIndex.setValue(_this.getContext(), i);
					}
				}
			};

			node.geometryNode = geometryNode;
			node.sceneNode = sceneNode;
			node.pointcloud = this;
			node.left = geometryNode.left;
			node.right = geometryNode.right;

			if (!parent) {
				this.root = node;
				this.add(sceneNode);
			} else {
				parent.sceneNode.add(sceneNode);

				if (parent.left === geometryNode) {
					parent.left = node;
				} else if (parent.right === geometryNode) {
					parent.right = node;
				}
			}

			let disposeListener = function () {
				parent.sceneNode.remove(node.sceneNode);

				if (parent.left === node) {
					parent.left = geometryNode;
				} else if (parent.right === node) {
					parent.right = geometryNode;
				}
			};
			geometryNode.oneTimeDisposeHandlers.push(disposeListener);

			return node;
		}

		updateMaterial (material, visibleNodes, camera, renderer) {
			material.fov = camera.fov * (Math.PI / 180);
			material.screenWidth = renderer.domElement.clientWidth;
			material.screenHeight = renderer.domElement.clientHeight;
			material.spacing = this.pcoGeometry.spacing;
			material.near = camera.near;
			material.far = camera.far;

			// reduce shader source updates by setting maxLevel slightly higher than actually necessary
			if (this.maxLevel > material.levels) {
				material.levels = this.maxLevel + 2;
			}

			// material.uniforms.octreeSize.value = this.boundingBox.size().x;
			let bbSize = this.boundingBox.getSize(new THREE.Vector3());
			material.bbSize = [bbSize.x, bbSize.y, bbSize.z];
		}

		updateVisibleBounds () {

		}

		hideDescendants (object) {
			let stack = [];
			for (let i = 0; i < object.children.length; i++) {
				let child = object.children[i];
				if (child.visible) {
					stack.push(child);
				}
			}

			while (stack.length > 0) {
				let child = stack.shift();

				child.visible = false;
				if (child.boundingBoxNode) {
					child.boundingBoxNode.visible = false;
				}

				for (let i = 0; i < child.children.length; i++) {
					let childOfChild = child.children[i];
					if (childOfChild.visible) {
						stack.push(childOfChild);
					}
				}
			}
		}

		

		nodesOnRay (nodes, ray) {
			let nodesOnRay = [];

			let _ray = ray.clone();
			for (let i = 0; i < nodes.length; i++) {
				let node = nodes[i];
				let sphere = node.getBoundingSphere().clone().applyMatrix4(node.sceneNode.matrixWorld);
				// TODO Unused: let box = node.getBoundingBox().clone().applyMatrix4(node.sceneNode.matrixWorld);

				if (_ray.intersectsSphere(sphere)) {
					nodesOnRay.push(node);
				}
				// if(_ray.isIntersectionBox(box)){
				//	nodesOnRay.push(node);
				// }
			}

			return nodesOnRay;
		}

		
		computeVisibilityTextureData(nodes){

			if(exports.measureTimings) performance.mark("computeVisibilityTextureData-start");

			let data = new Uint8Array(nodes.length * 3);
			let visibleNodeTextureOffsets = new Map();

			// copy array
			nodes = nodes.slice();

			// sort by level and number
			let sort = function (a, b) {
				let la = a.geometryNode.level;
				let lb = b.geometryNode.level;
				let na = a.geometryNode.number;
				let nb = b.geometryNode.number;
				if (la !== lb) return la - lb;
				if (na < nb) return -1;
				if (na > nb) return 1;
				return 0;
			};
			nodes.sort(sort);

			let visibleNodeNames = [];
			for (let i = 0; i < nodes.length; i++) {
				visibleNodeNames.push(nodes[i].geometryNode.number);
			}

			for (let i = 0; i < nodes.length; i++) {
				let node = nodes[i];

				visibleNodeTextureOffsets.set(node, i);

				let b1 = 0;	// children
				let b2 = 0;	// offset to first child
				let b3 = 0;	// split

				if (node.geometryNode.left && visibleNodeNames.indexOf(node.geometryNode.left.number) > 0) {
					b1 += 1;
					b2 = visibleNodeNames.indexOf(node.geometryNode.left.number) - i;
				}
				if (node.geometryNode.right && visibleNodeNames.indexOf(node.geometryNode.right.number) > 0) {
					b1 += 2;
					b2 = (b2 === 0) ? visibleNodeNames.indexOf(node.geometryNode.right.number) - i : b2;
				}

				if (node.geometryNode.split === 'X') {
					b3 = 1;
				} else if (node.geometryNode.split === 'Y') {
					b3 = 2;
				} else if (node.geometryNode.split === 'Z') {
					b3 = 4;
				}

				data[i * 3 + 0] = b1;
				data[i * 3 + 1] = b2;
				data[i * 3 + 2] = b3;
			}

			if(exports.measureTimings){
				performance.mark("computeVisibilityTextureData-end");
				performance.measure("render.computeVisibilityTextureData", "computeVisibilityTextureData-start", "computeVisibilityTextureData-end");
			}

			return {
				data: data,
				offsets: visibleNodeTextureOffsets
			};
		}

		get progress () {
			if (this.pcoGeometry.root) {
				return exports.numNodesLoading > 0 ? 0 : 1;
			} else {
				return 0;
			}
		}
		
		/**
		 *
		 *
		 *
		 * params.pickWindowSize:	Look for points inside a pixel window of this size.
		 *							Use odd values: 1, 3, 5, ...
		 *
		 *
		 * TODO: only draw pixels that are actually read with readPixels().
		 *
		 */
		pick(viewer, camera, ray, params = {}){

			let renderer = viewer.renderer;
			let pRenderer = viewer.pRenderer;

			performance.mark("pick-start");

			let getVal = (a, b) => a !== undefined ? a : b;

			let pickWindowSize = getVal(params.pickWindowSize, 17);
			let pickOutsideClipRegion = getVal(params.pickOutsideClipRegion, false);

			pickWindowSize = 65;

			let size = renderer.getSize(new THREE.Vector2());

			let width = Math.ceil(getVal(params.width, size.width));
			let height = Math.ceil(getVal(params.height, size.height));

			let pointSizeType = getVal(params.pointSizeType, this.material.pointSizeType);
			let pointSize = getVal(params.pointSize, this.material.size);

			let nodes = this.nodesOnRay(this.visibleNodes, ray);

			if (nodes.length === 0) {
				return null;
			}

			if (!this.pickState) {
				let scene = new THREE.Scene();

				let material = new Potree.PointCloudMaterial();
				material.activeAttributeName = "indices";

				let renderTarget = new THREE.WebGLRenderTarget(
					1, 1,
					{ minFilter: THREE.LinearFilter,
						magFilter: THREE.NearestFilter,
						format: THREE.RGBAFormat }
				);

				this.pickState = {
					renderTarget: renderTarget,
					material: material,
					scene: scene
				};
			};

			let pickState = this.pickState;
			let pickMaterial = pickState.material;

			{ // update pick material
				pickMaterial.pointSizeType = pointSizeType;
				//pickMaterial.shape = this.material.shape;
				pickMaterial.shape = Potree.PointShape.PARABOLOID;

				pickMaterial.uniforms.uFilterReturnNumberRange.value = this.material.uniforms.uFilterReturnNumberRange.value;
				pickMaterial.uniforms.uFilterNumberOfReturnsRange.value = this.material.uniforms.uFilterNumberOfReturnsRange.value;
				pickMaterial.uniforms.uFilterGPSTimeClipRange.value = this.material.uniforms.uFilterGPSTimeClipRange.value;
				pickMaterial.uniforms.uFilterPointSourceIDClipRange.value = this.material.uniforms.uFilterPointSourceIDClipRange.value;

				pickMaterial.activeAttributeName = "indices";

				pickMaterial.size = pointSize;
				pickMaterial.uniforms.minSize.value = this.material.uniforms.minSize.value;
				pickMaterial.uniforms.maxSize.value = this.material.uniforms.maxSize.value;
				pickMaterial.classification = this.material.classification;
				pickMaterial.recomputeClassification();

				if(params.pickClipped){
					pickMaterial.clipBoxes = this.material.clipBoxes;
					pickMaterial.uniforms.clipBoxes = this.material.uniforms.clipBoxes;
					if(this.material.clipTask === Potree.ClipTask.HIGHLIGHT){
						pickMaterial.clipTask = Potree.ClipTask.NONE;
					}else {
						pickMaterial.clipTask = this.material.clipTask;
					}
					pickMaterial.clipMethod = this.material.clipMethod;
				}else {
					pickMaterial.clipBoxes = [];
				}

				this.updateMaterial(pickMaterial, nodes, camera, renderer);
			}

			pickState.renderTarget.setSize(width, height);

			let pixelPos = new THREE.Vector2(params.x, params.y);

			let gl = renderer.getContext();
			gl.enable(gl.SCISSOR_TEST);
			gl.scissor(
				parseInt(pixelPos.x - (pickWindowSize - 1) / 2),
				parseInt(pixelPos.y - (pickWindowSize - 1) / 2),
				parseInt(pickWindowSize), parseInt(pickWindowSize));


			renderer.state.buffers.depth.setTest(pickMaterial.depthTest);
			renderer.state.buffers.depth.setMask(pickMaterial.depthWrite);
			renderer.state.setBlending(THREE.NoBlending);

			{ // RENDER
				renderer.setRenderTarget(pickState.renderTarget);
				gl.clearColor(0, 0, 0, 0);
				renderer.clear(true, true, true);

				let tmp = this.material;
				this.material = pickMaterial;

				pRenderer.renderOctree(this, nodes, camera, pickState.renderTarget);

				this.material = tmp;
			}

			let clamp = (number, min, max) => Math.min(Math.max(min, number), max);

			let x = parseInt(clamp(pixelPos.x - (pickWindowSize - 1) / 2, 0, width));
			let y = parseInt(clamp(pixelPos.y - (pickWindowSize - 1) / 2, 0, height));
			let w = parseInt(Math.min(x + pickWindowSize, width) - x);
			let h = parseInt(Math.min(y + pickWindowSize, height) - y);

			let pixelCount = w * h;
			let buffer = new Uint8Array(4 * pixelCount);

			gl.readPixels(x, y, pickWindowSize, pickWindowSize, gl.RGBA, gl.UNSIGNED_BYTE, buffer);

			renderer.setRenderTarget(null);
			renderer.state.reset();
			renderer.setScissorTest(false);
			gl.disable(gl.SCISSOR_TEST);

			let pixels = buffer;
			let ibuffer = new Uint32Array(buffer.buffer);

			// find closest hit inside pixelWindow boundaries
			let min = Number.MAX_VALUE;
			let hits = [];
			for (let u = 0; u < pickWindowSize; u++) {
				for (let v = 0; v < pickWindowSize; v++) {
					let offset = (u + v * pickWindowSize);
					let distance = Math.pow(u - (pickWindowSize - 1) / 2, 2) + Math.pow(v - (pickWindowSize - 1) / 2, 2);

					let pcIndex = pixels[4 * offset + 3];
					pixels[4 * offset + 3] = 0;
					let pIndex = ibuffer[offset];

					if(!(pcIndex === 0 && pIndex === 0) && (pcIndex !== undefined) && (pIndex !== undefined)){
						let hit = {
							pIndex: pIndex,
							pcIndex: pcIndex,
							distanceToCenter: distance
						};

						if(params.all){
							hits.push(hit);
						}else {
							if(hits.length > 0){
								if(distance < hits[0].distanceToCenter){
									hits[0] = hit;
								}
							}else {
								hits.push(hit);
							}
						}


					}
				}
			}

			//DEBUG: show panel with pick image
			// {
			// 	let img = Utils.pixelsArrayToImage(buffer, w, h);
			// 	let screenshot = img.src;
			
			// 	if(!this.debugDIV){
			// 		this.debugDIV = $(`
			// 			<div id="pickDebug"
			// 			style="position: absolute;
			// 			right: 400px; width: 300px;
			// 			bottom: 44px; width: 300px;
			// 			z-index: 1000;
			// 			"></div>`);
			// 		$(document.body).append(this.debugDIV);
			// 	}
			
			// 	this.debugDIV.empty();
			// 	this.debugDIV.append($(`<img src="${screenshot}"
			// 		style="transform: scaleY(-1); width: 300px"/>`));
			// 	//$(this.debugWindow.document).append($(`<img src="${screenshot}"/>`));
			// 	//this.debugWindow.document.write('<img src="'+screenshot+'"/>');
			// }


			for(let hit of hits){
				let point = {};

				if (!nodes[hit.pcIndex]) {
					return null;
				}

				let node = nodes[hit.pcIndex];
				let pc = node.sceneNode;
				let geometry = node.geometryNode.geometry;

				for(let attributeName in geometry.attributes){
					let attribute = geometry.attributes[attributeName];

					if (attributeName === 'position') {
						let x = attribute.array[3 * hit.pIndex + 0];
						let y = attribute.array[3 * hit.pIndex + 1];
						let z = attribute.array[3 * hit.pIndex + 2];

						let position = new THREE.Vector3(x, y, z);
						position.applyMatrix4(pc.matrixWorld);

						point[attributeName] = position;
					} else if (attributeName === 'indices') {

					} else {

						let values = attribute.array.slice(attribute.itemSize * hit.pIndex, attribute.itemSize * (hit.pIndex + 1)) ;

						if(attribute.potree){
							const {scale, offset} = attribute.potree;
							values = values.map(v => v / scale + offset);
						}

						point[attributeName] = values;

						//debugger;
						//if (values.itemSize === 1) {
						//	point[attribute.name] = values.array[hit.pIndex];
						//} else {
						//	let value = [];
						//	for (let j = 0; j < values.itemSize; j++) {
						//		value.push(values.array[values.itemSize * hit.pIndex + j]);
						//	}
						//	point[attribute.name] = value;
						//}
					}

				}

				hit.point = point;
			}

			performance.mark("pick-end");
			performance.measure("pick", "pick-start", "pick-end");

			if(params.all){
				return hits.map(hit => hit.point);
			}else {
				if(hits.length === 0){
					return null;
				}else {
					return hits[0].point;
					//let sorted = hits.sort( (a, b) => a.distanceToCenter - b.distanceToCenter);

					//return sorted[0].point;
				}
			}

		};
	};

	OrthographicCamera.prototype.zoomTo = function( node, factor = 1){

		if ( !node.geometry && !node.boundingBox) {
			return;
		}

		// TODO

		//let minWS = new THREE.Vector4(node.boundingBox.min.x, node.boundingBox.min.y, node.boundingBox.min.z, 1);
		//let minVS = minWS.applyMatrix4(this.matrixWorldInverse);

		//let right = node.boundingBox.max.x;
		//let bottom	= node.boundingBox.min.y;
		//let top = node.boundingBox.max.y;

		this.updateProjectionMatrix();	
	};

	PerspectiveCamera.prototype.zoomTo = function (node, factor) {
		if (!node.geometry && !node.boundingSphere && !node.boundingBox) {
			return;
		}

		if (node.geometry && node.geometry.boundingSphere === null) {
			node.geometry.computeBoundingSphere();
		}

		node.updateMatrixWorld();

		let bs;

		if (node.boundingSphere) {
			bs = node.boundingSphere;
		} else if (node.geometry && node.geometry.boundingSphere) {
			bs = node.geometry.boundingSphere;
		} else {
			bs = node.boundingBox.getBoundingSphere(new Sphere());
		}

		let _factor = factor || 1;

		bs = bs.clone().applyMatrix4(node.matrixWorld);
		let radius = bs.radius;
		let fovr = this.fov * Math.PI / 180;

		if (this.aspect < 1) {
			fovr = fovr * this.aspect;
		}

		let distanceFactor = Math.abs(radius / Math.sin(fovr / 2)) * _factor;

		let offset = this.getWorldDirection(new Vector3()).multiplyScalar(-distanceFactor);
		this.position.copy(bs.center.clone().add(offset));
	};

	Ray.prototype.distanceToPlaneWithNegative = function (plane) {
		let denominator = plane.normal.dot(this.direction);
		if (denominator === 0) {
			// line is coplanar, return origin
			if (plane.distanceToPoint(this.origin) === 0) {
				return 0;
			}

			// Null is preferable to undefined since undefined means.... it is undefined
			return null;
		}
		let t = -(this.origin.dot(plane.normal) + plane.constant) / denominator;

		return t;
	};

	const workerPool = new WorkerPool();

	const version = {
		major: 1,
		minor: 8,
		suffix: '.0'
	};

	let lru = new LRU();

	console.log('Potree ' + version.major + '.' + version.minor + version.suffix);

	let pointBudget = 1 * 1000 * 1000;
	let framenumber = 0;
	let numNodesLoading = 0;
	let maxNodesLoading = 64;

	const debug = {};

	exports.scriptPath = "";

	if (document.currentScript && document.currentScript.src) {
		exports.scriptPath = new URL(document.currentScript.src + '/..').href;
		if (exports.scriptPath.slice(-1) === '/') {
			exports.scriptPath = exports.scriptPath.slice(0, -1);
		}
	} else if(({ url: (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('potree.js', document.baseURI).href)) })){
		exports.scriptPath = new URL((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('potree.js', document.baseURI).href)) + "/..").href;
		if (exports.scriptPath.slice(-1) === '/') {
			exports.scriptPath = exports.scriptPath.slice(0, -1);
		}
	}else {
		console.error('Potree was unable to find its script path using document.currentScript. Is Potree included with a script tag? Does your browser support this function?');
	}

	let resourcePath = exports.scriptPath + '/resources';


	function loadPointCloud$1(path, name, callback){
		let loaded = function(e){
			e.pointcloud.name = name;
			callback(e);
		};

		let promise = new Promise( resolve => {

			// load pointcloud
			PointCloudZappchaGeometry.load(path, function (geometry) {
						if (!geometry) {
							console.error(new Error(`failed to load point cloud from URL: ${key}`));
						} else {
							let pointcloud = new PointCloudZappcha(geometry);
							resolve({type: 'pointcloud_loaded', pointcloud: pointcloud});
						}
					});
			/*if (!path){
				// TODO: callback? comment? Hello? Bueller? Anyone?
			} else if (path.indexOf('ept.json') > 0) {
				EptLoader.load(path, function(geometry) {
					if (!geometry) {
						console.error(new Error(`failed to load point cloud from URL: ${path}`));
					}
					else {
						let pointcloud = new PointCloudOctree(geometry);
						//loaded(pointcloud);
						resolve({type: 'pointcloud_loaded', pointcloud: pointcloud});
					}
				});
			} else if (path.indexOf('cloud.js') > 0) {
				POCLoader.load(path, function (geometry) {
					if (!geometry) {
						//callback({type: 'loading_failed'});
						console.error(new Error(`failed to load point cloud from URL: ${path}`));
					} else {
						let pointcloud = new PointCloudOctree(geometry);
						// loaded(pointcloud);
						resolve({type: 'pointcloud_loaded', pointcloud: pointcloud});
					}
				});
			} else if (path.indexOf('metadata.json') > 0) {
				Potree.OctreeLoader.load(path).then(e => {
					let geometry = e.geometry;

					if(!geometry){
						console.error(new Error(`failed to load point cloud from URL: ${path}`));
					}else{
						let pointcloud = new PointCloudOctree(geometry);

						let aPosition = pointcloud.getAttribute("position");

						let material = pointcloud.material;
						material.elevationRange = [
							aPosition.range[0][2],
							aPosition.range[1][2],
						];

						// loaded(pointcloud);
						resolve({type: 'pointcloud_loaded', pointcloud: pointcloud});
					}
				});

				OctreeLoader.load(path, function (geometry) {
					if (!geometry) {
						//callback({type: 'loading_failed'});
						console.error(new Error(`failed to load point cloud from URL: ${path}`));
					} else {
						let pointcloud = new PointCloudOctree(geometry);
						// loaded(pointcloud);
						resolve({type: 'pointcloud_loaded', pointcloud: pointcloud});
					}
				});
			}  else if (path.indexOf('.vpc') > 0) {
					PointCloudZappchaGeometry.load(path, function (geometry) {
						if (!geometry) {
							console.error(new Error(`failed to load point cloud from URL: ${key}`));
						} else {
							let pointcloud = new PointCloudZappcha(geometry);
							resolve({type: 'pointcloud_loaded', pointcloud: pointcloud});
						}
					});
			} else {
				//callback({'type': 'loading_failed'});
				console.error(new Error(`failed to load point cloud from URL: ${path}`));
			}*/
		});

		if(callback){
			promise.then(pointcloud => {
				loaded(pointcloud);
			});
		}else {
			return promise;
		}
	};


	// add selectgroup
	(function($){
		$.fn.extend({
			selectgroup: function(args = {}){

				let elGroup = $(this);
				let rootID = elGroup.prop("id");
				let groupID = `${rootID}`;
				let groupTitle = (args.title !== undefined) ? args.title : "";

				let elButtons = [];
				elGroup.find("option").each((index, value) => {
					let buttonID = $(value).prop("id");
					let label = $(value).html();
					let optionValue = $(value).prop("value");

					let elButton = $(`
					<span style="flex-grow: 1; display: inherit">
					<label for="${buttonID}" class="ui-button" style="width: 100%; padding: .4em .1em">${label}</label>
					<input type="radio" name="${groupID}" id="${buttonID}" value="${optionValue}" style="display: none"/>
					</span>
				`);
					let elLabel = elButton.find("label");
					let elInput = elButton.find("input");

					elInput.change( () => {
						elGroup.find("label").removeClass("ui-state-active");
						elGroup.find("label").addClass("ui-state-default");
						if(elInput.is(":checked")){
							elLabel.addClass("ui-state-active");
						}else {
							//elLabel.addClass("ui-state-default");
						}
					});

					elButtons.push(elButton);
				});

				let elFieldset = $(`
				<fieldset style="border: none; margin: 0px; padding: 0px">
					<legend>${groupTitle}</legend>
					<span style="display: flex">

					</span>
				</fieldset>
			`);

				let elButtonContainer = elFieldset.find("span");
				for(let elButton of elButtons){
					elButtonContainer.append(elButton);
				}

				elButtonContainer.find("label").each( (index, value) => {
					$(value).css("margin", "0px");
					$(value).css("border-radius", "0px");
					$(value).css("border", "1px solid black");
					$(value).css("border-left", "none");
				});
				elButtonContainer.find("label:first").each( (index, value) => {
					$(value).css("border-radius", "4px 0px 0px 4px");

				});
				elButtonContainer.find("label:last").each( (index, value) => {
					$(value).css("border-radius", "0px 4px 4px 0px");
					$(value).css("border-left", "none");
				});

				elGroup.empty();
				elGroup.append(elFieldset);



			}
		});
	})(jQuery);

	exports.Action = Action;
	exports.AnimationPath = AnimationPath;
	exports.Annotation = Annotation;
	exports.Box3Helper = Box3Helper$1;
	exports.BoxVolume = BoxVolume;
	exports.CameraAnimation = CameraAnimation;
	exports.CameraMode = CameraMode;
	exports.ClassificationScheme = ClassificationScheme;
	exports.ClipMethod = ClipMethod;
	exports.ClipTask = ClipTask;
	exports.ClipVolume = ClipVolume;
	exports.ClippingTool = ClippingTool;
	exports.Compass = Compass;
	exports.DeviceOrientationControls = DeviceOrientationControls;
	exports.EarthControls = EarthControls;
	exports.ElevationGradientRepeat = ElevationGradientRepeat;
	exports.Enum = Enum;
	exports.EnumItem = EnumItem;
	exports.EptBinaryLoader = EptBinaryLoader;
	exports.EptKey = EptKey;
	exports.EptLaszipLoader = EptLaszipLoader;
	exports.EptLazBatcher = EptLazBatcher;
	exports.EptLoader = EptLoader;
	exports.EptZstandardLoader = EptZstandardLoader;
	exports.EventDispatcher = EventDispatcher;
	exports.EyeDomeLightingMaterial = EyeDomeLightingMaterial;
	exports.Features = Features;
	exports.FirstPersonControls = FirstPersonControls;
	exports.GeoPackageLoader = GeoPackageLoader;
	exports.Geopackage = Geopackage$1;
	exports.Gradients = Gradients;
	exports.HierarchicalSlider = HierarchicalSlider;
	exports.Images360 = Images360;
	exports.Images360Loader = Images360Loader;
	exports.KeyCodes = KeyCodes;
	exports.LRU = LRU;
	exports.LRUItem = LRUItem;
	exports.LengthUnits = LengthUnits;
	exports.MOUSE = MOUSE$1;
	exports.Measure = Measure;
	exports.MeasuringTool = MeasuringTool;
	exports.Message = Message;
	exports.NodeLoader = NodeLoader;
	exports.NormalizationEDLMaterial = NormalizationEDLMaterial;
	exports.NormalizationMaterial = NormalizationMaterial;
	exports.OctreeLoader = OctreeLoader;
	exports.OrbitControls = OrbitControls;
	exports.OrientedImage = OrientedImage;
	exports.OrientedImageLoader = OrientedImageLoader;
	exports.OrientedImages = OrientedImages;
	exports.POCLoader = POCLoader;
	exports.PathAnimation = PathAnimation;
	exports.PointAttribute = PointAttribute;
	exports.PointAttributeTypes = PointAttributeTypes;
	exports.PointAttributes = PointAttributes;
	exports.PointCloudEptGeometry = PointCloudEptGeometry;
	exports.PointCloudEptGeometryNode = PointCloudEptGeometryNode;
	exports.PointCloudMaterial = PointCloudMaterial$1;
	exports.PointCloudOctree = PointCloudOctree;
	exports.PointCloudOctreeGeometry = PointCloudOctreeGeometry;
	exports.PointCloudOctreeGeometryNode = PointCloudOctreeGeometryNode;
	exports.PointCloudOctreeNode = PointCloudOctreeNode;
	exports.PointCloudSM = PointCloudSM;
	exports.PointCloudTree = PointCloudTree;
	exports.PointCloudTreeNode = PointCloudTreeNode;
	exports.PointCloudZappcha = PointCloudZappcha;
	exports.PointCloudZappchaGeometry = PointCloudZappchaGeometry;
	exports.PointCloudZappchaGeometryNode = PointCloudZappchaGeometryNode;
	exports.PointCloudZappchaNode = PointCloudZappchaNode;
	exports.PointShape = PointShape;
	exports.PointSizeType = PointSizeType;
	exports.Points = Points$1;
	exports.PolygonClipVolume = PolygonClipVolume;
	exports.Profile = Profile;
	exports.ProfileData = ProfileData;
	exports.ProfileRequest = ProfileRequest;
	exports.ProfileTool = ProfileTool;
	exports.Renderer = Renderer;
	exports.Scene = Scene$1;
	exports.ScreenBoxSelectTool = ScreenBoxSelectTool;
	exports.ShapefileLoader = ShapefileLoader;
	exports.SphereVolume = SphereVolume;
	exports.SpotLightHelper = SpotLightHelper$1;
	exports.TextSprite = TextSprite;
	exports.TransformationTool = TransformationTool;
	exports.TreeType = TreeType;
	exports.Utils = Utils;
	exports.VRControls = VRControls;
	exports.Version = Version;
	exports.Viewer = Viewer;
	exports.Volume = Volume;
	exports.VolumeTool = VolumeTool;
	exports.WorkerPool = WorkerPool;
	exports.XHRFactory = XHRFactory;
	exports.debug = debug;
	exports.framenumber = framenumber;
	exports.loadPointCloud = loadPointCloud$1;
	exports.loadProject = loadProject;
	exports.lru = lru;
	exports.maxNodesLoading = maxNodesLoading;
	exports.numNodesLoading = numNodesLoading;
	exports.pointBudget = pointBudget;
	exports.resourcePath = resourcePath;
	exports.saveProject = saveProject;
	exports.updatePointClouds = updatePointClouds;
	exports.updateVisibility = updateVisibility;
	exports.updateVisibilityStructures = updateVisibilityStructures;
	exports.version = version;
	exports.workerPool = workerPool;

	Object.defineProperty(exports, '__esModule', { value: true });

})));
//# sourceMappingURL=potree.js.map
