( function () {

	class OutlinePass extends THREE.Pass {

		constructor( resolution, scene, camera, selectedObjects ) {

			super();
			this.renderScene = scene;
			this.renderCamera = camera;
			this.selectedObjects = selectedObjects !== undefined ? selectedObjects : [];
			this.visibleEdgeColor = new THREE.Color( 1, 1, 1 );
			this.hiddenEdgeColor = new THREE.Color( 0.1, 0.04, 0.02 );
			this.edgeGlow = 0.0;
			this.usePatternTexture = false;
			this.edgeThickness = 1.0;
			this.edgeStrength = 3.0;
			this.downSampleRatio = 2;
			this.pulsePeriod = 0;
			this._visibilityCache = new Map();
			this.resolution = resolution !== undefined ? new THREE.Vector2( resolution.x, resolution.y ) : new THREE.Vector2( 256, 256 );
			const pars = {
				minFilter: THREE.LinearFilter,
				magFilter: THREE.LinearFilter,
				format: THREE.RGBAFormat
			};
			const resx = Math.round( this.resolution.x / this.downSampleRatio );
			const resy = Math.round( this.resolution.y / this.downSampleRatio );
			this.maskBufferMaterial = new THREE.MeshBasicMaterial( {
				color: 0xffffff
			} );
			this.maskBufferMaterial.side = THREE.DoubleSide;
			this.renderTargetMaskBuffer = new THREE.WebGLRenderTarget( this.resolution.x, this.resolution.y, pars );
			this.renderTargetMaskBuffer.texture.name = 'OutlinePass.mask';
			this.renderTargetMaskBuffer.texture.generateMipmaps = false;
			this.depthMaterial = new THREE.MeshDepthMaterial();
			this.depthMaterial.side = THREE.DoubleSide;
			this.depthMaterial.depthPacking = THREE.RGBADepthPacking;
			this.depthMaterial.blending = THREE.NoBlending;
			this.prepareMaskMaterial = this.getPrepareMaskMaterial();
			this.prepareMaskMaterial.side = THREE.DoubleSide;
			this.prepareMaskMaterial.fragmentShader = replaceDepthToViewZ( this.prepareMaskMaterial.fragmentShader, this.renderCamera );
			this.renderTargetDepthBuffer = new THREE.WebGLRenderTarget( this.resolution.x, this.resolution.y, pars );
			this.renderTargetDepthBuffer.texture.name = 'OutlinePass.depth';
			this.renderTargetDepthBuffer.texture.generateMipmaps = false;
			this.renderTargetMaskDownSampleBuffer = new THREE.WebGLRenderTarget( resx, resy, pars );
			this.renderTargetMaskDownSampleBuffer.texture.name = 'OutlinePass.depthDownSample';
			this.renderTargetMaskDownSampleBuffer.texture.generateMipmaps = false;
			this.renderTargetBlurBuffer1 = new THREE.WebGLRenderTarget( resx, resy, pars );
			this.renderTargetBlurBuffer1.texture.name = 'OutlinePass.blur1';
			this.renderTargetBlurBuffer1.texture.generateMipmaps = false;
			this.renderTargetBlurBuffer2 = new THREE.WebGLRenderTarget( Math.round( resx / 2 ), Math.round( resy / 2 ), pars );
			this.renderTargetBlurBuffer2.texture.name = 'OutlinePass.blur2';
			this.renderTargetBlurBuffer2.texture.generateMipmaps = false;
			this.edgeDetectionMaterial = this.getEdgeDetectionMaterial();
			this.renderTargetEdgeBuffer1 = new THREE.WebGLRenderTarget( resx, resy, pars );
			this.renderTargetEdgeBuffer1.texture.name = 'OutlinePass.edge1';
			this.renderTargetEdgeBuffer1.texture.generateMipmaps = false;
			this.renderTargetEdgeBuffer2 = new THREE.WebGLRenderTarget( Math.round( resx / 2 ), Math.round( resy / 2 ), pars );
			this.renderTargetEdgeBuffer2.texture.name = 'OutlinePass.edge2';
			this.renderTargetEdgeBuffer2.texture.generateMipmaps = false;
			const MAX_EDGE_THICKNESS = 4;
			const MAX_EDGE_GLOW = 4;
			this.separableBlurMaterial1 = this.getSeperableBlurMaterial( MAX_EDGE_THICKNESS );
			this.separableBlurMaterial1.uniforms[ 'texSize' ].value.set( resx, resy );
			this.separableBlurMaterial1.uniforms[ 'kernelRadius' ].value = 1;
			this.separableBlurMaterial2 = this.getSeperableBlurMaterial( MAX_EDGE_GLOW );
			this.separableBlurMaterial2.uniforms[ 'texSize' ].value.set( Math.round( resx / 2 ), Math.round( resy / 2 ) );
			this.separableBlurMaterial2.uniforms[ 'kernelRadius' ].value = MAX_EDGE_GLOW; // Overlay material

			this.overlayMaterial = this.getOverlayMaterial(); // copy material

			if ( THREE.CopyShader === undefined ) console.error( 'THREE.OutlinePass relies on THREE.CopyShader' );
			const copyShader = THREE.CopyShader;
			this.copyUniforms = THREE.UniformsUtils.clone( copyShader.uniforms );
			this.copyUniforms[ 'opacity' ].value = 1.0;
			this.materialCopy = new THREE.ShaderMaterial( {
				uniforms: this.copyUniforms,
				vertexShader: copyShader.vertexShader,
				fragmentShader: copyShader.fragmentShader,
				blending: THREE.NoBlending,
				depthTest: false,
				depthWrite: false,
				transparent: true
			} );
			this.enabled = true;
			this.needsSwap = false;
			this._oldClearColor = new THREE.Color();
			this.oldClearAlpha = 1;
			this.fsQuad = new THREE.FullScreenQuad( null );
			this.tempPulseColor1 = new THREE.Color();
			this.tempPulseColor2 = new THREE.Color();
			this.textureMatrix = new THREE.Matrix4();

			function replaceDepthToViewZ( string, camera ) {

				var type = camera.isPerspectiveCamera ? 'perspective' : 'orthographic';
				return string.replace( /DEPTH_TO_VIEW_Z/g, type + 'DepthToViewZ' );

			}

		}

		dispose() {

			this.renderTargetMaskBuffer.dispose();
			this.renderTargetDepthBuffer.dispose();
			this.renderTargetMaskDownSampleBuffer.dispose();
			this.renderTargetBlurBuffer1.dispose();
			this.renderTargetBlurBuffer2.dispose();
			this.renderTargetEdgeBuffer1.dispose();
			this.renderTargetEdgeBuffer2.dispose();

		}

		setSize( width, height ) {

			this.renderTargetMaskBuffer.setSize( width, height );
			this.renderTargetDepthBuffer.setSize( width, height );
			let resx = Math.round( width / this.downSampleRatio );
			let resy = Math.round( height / this.downSampleRatio );
			this.renderTargetMaskDownSampleBuffer.setSize( resx, resy );
			this.renderTargetBlurBuffer1.setSize( resx, resy );
			this.renderTargetEdgeBuffer1.setSize( resx, resy );
			this.separableBlurMaterial1.uniforms[ 'texSize' ].value.set( resx, resy );
			resx = Math.round( resx / 2 );
			resy = Math.round( resy / 2 );
			this.renderTargetBlurBuffer2.setSize( resx, resy );
			this.renderTargetEdgeBuffer2.setSize( resx, resy );
			this.separableBlurMaterial2.uniforms[ 'texSize' ].value.set( resx, resy );

		}

		changeVisibilityOfSelectedObjects( bVisible ) {

			const cache = this._visibilityCache;

			function gatherSelectedMeshesCallBack( object ) {

				if ( object.isMesh ) {

					if ( bVisible === true ) {

						object.visible = cache.get( object );

					} else {

						cache.set( object, object.visible );
						object.visible = bVisible;

					}

				}

			}

			for ( let i = 0; i < this.selectedObjects.length; i ++ ) {

				const selectedObject = this.selectedObjects[ i ];
				selectedObject.traverse( gatherSelectedMeshesCallBack );

			}

		}

		changeVisibilityOfNonSelectedObjects( bVisible ) {

			const cache = this._visibilityCache;
			const selectedMeshes = [];

			function gatherSelectedMeshesCallBack( object ) {

				if ( object.isMesh ) selectedMeshes.push( object );

			}

			for ( let i = 0; i < this.selectedObjects.length; i ++ ) {

				const selectedObject = this.selectedObjects[ i ];
				selectedObject.traverse( gatherSelectedMeshesCallBack );

			}

			function VisibilityChangeCallBack( object ) {

				if ( object.isMesh || object.isSprite ) {

					// only meshes and sprites are supported by OutlinePass
					let bFound = false;

					for ( let i = 0; i < selectedMeshes.length; i ++ ) {

						const selectedObjectId = selectedMeshes[ i ].id;

						if ( selectedObjectId === object.id ) {

							bFound = true;
							break;

						}

					}

					if ( bFound === false ) {

						const visibility = object.visible;

						if ( bVisible === false || cache.get( object ) === true ) {

							object.visible = bVisible;

						}

						cache.set( object, visibility );

					}

				} else if ( object.isPoints || object.isLine ) {

					// the visibilty of points and lines is always set to false in order to
					// not affect the outline computation
					if ( bVisible === true ) {

						object.visible = cache.get( object ); // restore

					} else {

						cache.set( object, object.visible );
						object.visible = bVisible;

					}

				}

			}

			this.renderScene.traverse( VisibilityChangeCallBack );

		}

		updateTextureMatrix() {

			this.textureMatrix.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 );
			this.textureMatrix.multiply( this.renderCamera.projectionMatrix );
			this.textureMatrix.multiply( this.renderCamera.matrixWorldInverse );

		}

		render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {

			if ( this.selectedObjects.length > 0 ) {

				renderer.getClearColor( this._oldClearColor );
				this.oldClearAlpha = renderer.getClearAlpha();
				const oldAutoClear = renderer.autoClear;
				renderer.autoClear = false;
				if ( maskActive ) renderer.state.buffers.stencil.setTest( false );
				renderer.setClearColor( 0xffffff, 1 ); // Make selected objects invisible

				this.changeVisibilityOfSelectedObjects( false );
				const currentBackground = this.renderScene.background;
				this.renderScene.background = null; // 1. Draw Non Selected objects in the depth buffer

				this.renderScene.overrideMaterial = this.depthMaterial;
				renderer.setRenderTarget( this.renderTargetDepthBuffer );
				renderer.clear();
				renderer.render( this.renderScene, this.renderCamera ); // Make selected objects visible

				this.changeVisibilityOfSelectedObjects( true );

				this._visibilityCache.clear(); // Update Texture Matrix for Depth compare


				this.updateTextureMatrix(); // Make non selected objects invisible, and draw only the selected objects, by comparing the depth buffer of non selected objects

				this.changeVisibilityOfNonSelectedObjects( false );
				this.renderScene.overrideMaterial = this.prepareMaskMaterial;
				this.prepareMaskMaterial.uniforms[ 'cameraNearFar' ].value.set( this.renderCamera.near, this.renderCamera.far );
				this.prepareMaskMaterial.uniforms[ 'depthTexture' ].value = this.renderTargetDepthBuffer.texture;
				this.prepareMaskMaterial.uniforms[ 'textureMatrix' ].value = this.textureMatrix;
				renderer.setRenderTarget( this.renderTargetMaskBuffer );
				renderer.clear();
				renderer.render( this.renderScene, this.renderCamera );
				this.renderScene.overrideMaterial = null;
				this.changeVisibilityOfNonSelectedObjects( true );

				this._visibilityCache.clear();

				this.renderScene.background = currentBackground; // 2. Downsample to Half resolution

				this.fsQuad.material = this.materialCopy;
				this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetMaskBuffer.texture;
				renderer.setRenderTarget( this.renderTargetMaskDownSampleBuffer );
				renderer.clear();
				this.fsQuad.render( renderer );
				this.tempPulseColor1.copy( this.visibleEdgeColor );
				this.tempPulseColor2.copy( this.hiddenEdgeColor );

				if ( this.pulsePeriod > 0 ) {

					const scalar = ( 1 + 0.25 ) / 2 + Math.cos( performance.now() * 0.01 / this.pulsePeriod ) * ( 1.0 - 0.25 ) / 2;
					this.tempPulseColor1.multiplyScalar( scalar );
					this.tempPulseColor2.multiplyScalar( scalar );

				} // 3. Apply Edge Detection THREE.Pass


				this.fsQuad.material = this.edgeDetectionMaterial;
				this.edgeDetectionMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskDownSampleBuffer.texture;
				this.edgeDetectionMaterial.uniforms[ 'texSize' ].value.set( this.renderTargetMaskDownSampleBuffer.width, this.renderTargetMaskDownSampleBuffer.height );
				this.edgeDetectionMaterial.uniforms[ 'visibleEdgeColor' ].value = this.tempPulseColor1;
				this.edgeDetectionMaterial.uniforms[ 'hiddenEdgeColor' ].value = this.tempPulseColor2;
				renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
				renderer.clear();
				this.fsQuad.render( renderer ); // 4. Apply Blur on Half res

				this.fsQuad.material = this.separableBlurMaterial1;
				this.separableBlurMaterial1.uniforms[ 'colorTexture' ].value = this.renderTargetEdgeBuffer1.texture;
				this.separableBlurMaterial1.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionX;
				this.separableBlurMaterial1.uniforms[ 'kernelRadius' ].value = this.edgeThickness;
				renderer.setRenderTarget( this.renderTargetBlurBuffer1 );
				renderer.clear();
				this.fsQuad.render( renderer );
				this.separableBlurMaterial1.uniforms[ 'colorTexture' ].value = this.renderTargetBlurBuffer1.texture;
				this.separableBlurMaterial1.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionY;
				renderer.setRenderTarget( this.renderTargetEdgeBuffer1 );
				renderer.clear();
				this.fsQuad.render( renderer ); // Apply Blur on quarter res

				this.fsQuad.material = this.separableBlurMaterial2;
				this.separableBlurMaterial2.uniforms[ 'colorTexture' ].value = this.renderTargetEdgeBuffer1.texture;
				this.separableBlurMaterial2.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionX;
				renderer.setRenderTarget( this.renderTargetBlurBuffer2 );
				renderer.clear();
				this.fsQuad.render( renderer );
				this.separableBlurMaterial2.uniforms[ 'colorTexture' ].value = this.renderTargetBlurBuffer2.texture;
				this.separableBlurMaterial2.uniforms[ 'direction' ].value = OutlinePass.BlurDirectionY;
				renderer.setRenderTarget( this.renderTargetEdgeBuffer2 );
				renderer.clear();
				this.fsQuad.render( renderer ); // Blend it additively over the input texture

				this.fsQuad.material = this.overlayMaterial;
				this.overlayMaterial.uniforms[ 'maskTexture' ].value = this.renderTargetMaskBuffer.texture;
				this.overlayMaterial.uniforms[ 'edgeTexture1' ].value = this.renderTargetEdgeBuffer1.texture;
				this.overlayMaterial.uniforms[ 'edgeTexture2' ].value = this.renderTargetEdgeBuffer2.texture;
				this.overlayMaterial.uniforms[ 'patternTexture' ].value = this.patternTexture;
				this.overlayMaterial.uniforms[ 'edgeStrength' ].value = this.edgeStrength;
				this.overlayMaterial.uniforms[ 'edgeGlow' ].value = this.edgeGlow;
				this.overlayMaterial.uniforms[ 'usePatternTexture' ].value = this.usePatternTexture;
				if ( maskActive ) renderer.state.buffers.stencil.setTest( true );
				renderer.setRenderTarget( readBuffer );
				this.fsQuad.render( renderer );
				renderer.setClearColor( this._oldClearColor, this.oldClearAlpha );
				renderer.autoClear = oldAutoClear;

			}

			if ( this.renderToScreen ) {

				this.fsQuad.material = this.materialCopy;
				this.copyUniforms[ 'tDiffuse' ].value = readBuffer.texture;
				renderer.setRenderTarget( null );
				this.fsQuad.render( renderer );

			}

		}

		getPrepareMaskMaterial() {

			return new THREE.ShaderMaterial( {
				uniforms: {
					'depthTexture': {
						value: null
					},
					'cameraNearFar': {
						value: new THREE.Vector2( 0.5, 0.5 )
					},
					'textureMatrix': {
						value: null
					}
				},
				vertexShader: `#include <morphtarget_pars_vertex>
				#include <skinning_pars_vertex>

				varying vec4 projTexCoord;
				varying vec4 vPosition;
				uniform mat4 textureMatrix;

				void main() {

					#include <skinbase_vertex>
					#include <begin_vertex>
					#include <morphtarget_vertex>
					#include <skinning_vertex>
					#include <project_vertex>

					vPosition = mvPosition;
					vec4 worldPosition = modelMatrix * vec4( transformed, 1.0 );
					projTexCoord = textureMatrix * worldPosition;

				}`,
				fragmentShader: `#include <packing>
				varying vec4 vPosition;
				varying vec4 projTexCoord;
				uniform sampler2D depthTexture;
				uniform vec2 cameraNearFar;

				void main() {

					float depth = unpackRGBAToDepth(texture2DProj( depthTexture, projTexCoord ));
					float viewZ = - DEPTH_TO_VIEW_Z( depth, cameraNearFar.x, cameraNearFar.y );
					float depthTest = (-vPosition.z > viewZ) ? 1.0 : 0.0;
					gl_FragColor = vec4(0.0, depthTest, 1.0, 1.0);

				}`
			} );

		}

		getEdgeDetectionMaterial() {

			return new THREE.ShaderMaterial( {
				uniforms: {
					'maskTexture': {
						value: null
					},
					'texSize': {
						value: new THREE.Vector2( 0.5, 0.5 )
					},
					'visibleEdgeColor': {
						value: new THREE.Vector3( 1.0, 1.0, 1.0 )
					},
					'hiddenEdgeColor': {
						value: new THREE.Vector3( 1.0, 1.0, 1.0 )
					}
				},
				vertexShader: `varying vec2 vUv;

				void main() {
					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
				}`,
				fragmentShader: `varying vec2 vUv;

				uniform sampler2D maskTexture;
				uniform vec2 texSize;
				uniform vec3 visibleEdgeColor;
				uniform vec3 hiddenEdgeColor;

				void main() {
					vec2 invSize = 1.0 / texSize;
					vec4 uvOffset = vec4(1.0, 0.0, 0.0, 1.0) * vec4(invSize, invSize);
					vec4 c1 = texture2D( maskTexture, vUv + uvOffset.xy);
					vec4 c2 = texture2D( maskTexture, vUv - uvOffset.xy);
					vec4 c3 = texture2D( maskTexture, vUv + uvOffset.yw);
					vec4 c4 = texture2D( maskTexture, vUv - uvOffset.yw);
					float diff1 = (c1.r - c2.r)*0.5;
					float diff2 = (c3.r - c4.r)*0.5;
					float d = length( vec2(diff1, diff2) );
					float a1 = min(c1.g, c2.g);
					float a2 = min(c3.g, c4.g);
					float visibilityFactor = min(a1, a2);
					vec3 edgeColor = 1.0 - visibilityFactor > 0.001 ? visibleEdgeColor : hiddenEdgeColor;
					gl_FragColor = vec4(edgeColor, 1.0) * vec4(d);
				}`
			} );

		}

		getSeperableBlurMaterial( maxRadius ) {

			return new THREE.ShaderMaterial( {
				defines: {
					'MAX_RADIUS': maxRadius
				},
				uniforms: {
					'colorTexture': {
						value: null
					},
					'texSize': {
						value: new THREE.Vector2( 0.5, 0.5 )
					},
					'direction': {
						value: new THREE.Vector2( 0.5, 0.5 )
					},
					'kernelRadius': {
						value: 1.0
					}
				},
				vertexShader: `varying vec2 vUv;

				void main() {
					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
				}`,
				fragmentShader: `#include <common>
				varying vec2 vUv;
				uniform sampler2D colorTexture;
				uniform vec2 texSize;
				uniform vec2 direction;
				uniform float kernelRadius;

				float gaussianPdf(in float x, in float sigma) {
					return 0.39894 * exp( -0.5 * x * x/( sigma * sigma))/sigma;
				}

				void main() {
					vec2 invSize = 1.0 / texSize;
					float weightSum = gaussianPdf(0.0, kernelRadius);
					vec4 diffuseSum = texture2D( colorTexture, vUv) * weightSum;
					vec2 delta = direction * invSize * kernelRadius/float(MAX_RADIUS);
					vec2 uvOffset = delta;
					for( int i = 1; i <= MAX_RADIUS; i ++ ) {
						float w = gaussianPdf(uvOffset.x, kernelRadius);
						vec4 sample1 = texture2D( colorTexture, vUv + uvOffset);
						vec4 sample2 = texture2D( colorTexture, vUv - uvOffset);
						diffuseSum += ((sample1 + sample2) * w);
						weightSum += (2.0 * w);
						uvOffset += delta;
					}
					gl_FragColor = diffuseSum/weightSum;
				}`
			} );

		}

		getOverlayMaterial() {

			return new THREE.ShaderMaterial( {
				uniforms: {
					'maskTexture': {
						value: null
					},
					'edgeTexture1': {
						value: null
					},
					'edgeTexture2': {
						value: null
					},
					'patternTexture': {
						value: null
					},
					'edgeStrength': {
						value: 1.0
					},
					'edgeGlow': {
						value: 1.0
					},
					'usePatternTexture': {
						value: 0.0
					}
				},
				vertexShader: `varying vec2 vUv;

				void main() {
					vUv = uv;
					gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
				}`,
				fragmentShader: `varying vec2 vUv;

				uniform sampler2D maskTexture;
				uniform sampler2D edgeTexture1;
				uniform sampler2D edgeTexture2;
				uniform sampler2D patternTexture;
				uniform float edgeStrength;
				uniform float edgeGlow;
				uniform bool usePatternTexture;

				void main() {
					vec4 edgeValue1 = texture2D(edgeTexture1, vUv);
					vec4 edgeValue2 = texture2D(edgeTexture2, vUv);
					vec4 maskColor = texture2D(maskTexture, vUv);
					vec4 patternColor = texture2D(patternTexture, 6.0 * vUv);
					float visibilityFactor = 1.0 - maskColor.g > 0.0 ? 1.0 : 0.5;
					vec4 edgeValue = edgeValue1 + edgeValue2 * edgeGlow;
					vec4 finalColor = edgeStrength * maskColor.r * edgeValue;
					if(usePatternTexture)
						finalColor += + visibilityFactor * (1.0 - maskColor.r) * (1.0 - patternColor.r);
					gl_FragColor = finalColor;
				}`,
				blending: THREE.AdditiveBlending,
				depthTest: false,
				depthWrite: false,
				transparent: true
			} );

		}

	}

	OutlinePass.BlurDirectionX = new THREE.Vector2( 1.0, 0.0 );
	OutlinePass.BlurDirectionY = new THREE.Vector2( 0.0, 1.0 );

	THREE.OutlinePass = OutlinePass;

} )();
