/**
 * @author alteredq / http://alteredqualia.com/
 * @author MPanknin / http://www.redplant.de/
 * @author takahiro / https://github.com/takahirox
 *
 * WebGLDeferredRenderer supports two types of Deferred Renderings.
 * One is Classic Deferred Rendering and the other one is
 * Light Pre-Pass (Deferred Lighting).
 * Classic Deferred Rendering is default. You can use Light Pre-Pass
 * by calling .enableLightPrePass( true ) method.
 *
 * Dependencies
 *  - CopyShader
 *  - RenderPass
 *  - ShaderPass
 *  - EffectComposer
 *  - FXAAShader
 *
 * TODO
 *  - reuse existing glsl
 *  - shadow
 *  - optimization
 *  - MRT (when it's available on Three.js)
 *  - AmbientLight
 *  - HemisphereLight
 *  - PointLight (distance < 0)
 *  - morphNormals
 *  - BumpMap
 *  - ToneMap
 *  - envMap
 *  - wrapAround
 *  - addEffect
 */

import {
	AdditiveBlending,
	BackSide,
	Color,
	DepthStencilFormat,
	DepthTexture,
	FloatType,
	GreaterEqualDepth,
	LinearFilter,
	Matrix4,
	Mesh,
	NearestFilter,
	NoBlending,
	OrthographicCamera,
	PlaneBufferGeometry,
	RGBAFormat,
	RGBFormat,
	Scene,
	ShaderMaterial,
	SphereBufferGeometry,
	Uniform,
	UnsignedByteType,
	UnsignedInt248Type,
	Vector2,
	Vector3,
	Vector4,
	WebGLRenderTarget,
	WebGLRenderer
} from "../../../build/three.module.js";
import { EffectComposer } from "../postprocessing/EffectComposer.js";
import { ShaderPass } from "../postprocessing/ShaderPass.js";
import { RenderPass } from "../postprocessing/RenderPass.js";
import { FXAAShader } from "../shaders/FXAAShader.js";
import { CopyShader } from "../shaders/CopyShader.js";

var WebGLDeferredRenderer = function ( parameters ) {

	parameters = parameters || {};

	// private properties

	var _this = this;

	var _context;
	var _state;

	var _width, _height;

	// for Classic Deferred Rendering
	var _compColor;
	var _passColor, _passForward, _passCopy;

	// for Light Pre-Pass
	var _compReconstruction;
	var _passReconstruction;

	// for Common
	var _compNormalDepth, _compLight, _compFinal;
	var _passNormalDepth, _passLight, _passLightFullscreen, _passFinal, _passFXAA;

	var _depthTexture;

	var _currentCamera;

	var _lightScene, _lightFullscreenScene;

	var _antialias = false;
	var _hasTransparentObject = false;
	var _lightPrePass = false;
	var _cacheKeepAlive = false;

	var _tmpMaterial = new ShaderMaterial( { visible: false } );
	var _tmpVector3 = new Vector3();

	// scene/material/light cache for deferred rendering.
	// save them at the creation and release
	// if they're unused removeThresholdCount frames
	// unless _cacheKeepAlive is true.

	// scene.uuid -> lightScene, lightFullscreenScene
	var _lightScenesCache = {};
	var _lightFullscreenScenesCache = {};

	// object.material.uuid -> deferredMaterial or
	// object.material[ n ].uuid -> deferredMaterial
	var _normalDepthMaterialsCache = {};
	var _normalDepthShininessMaterialsCache = {};
	var _colorMaterialsCache = {};
	var _reconstructionMaterialsCache = {};

	// originalLight.uuid -> deferredLight
	var _deferredLightsCache = {};

	// deferredLight.uuid -> deferredLightMaterial
	var _classicDeferredLightMaterialsCache = {};
	var _lightPrePassMaterialsCache = {};

	var _removeThresholdCount = 60;

	// deferredMaterials.uuid -> object.material or
	// deferredMaterials.uuid -> object.material[ n ]
	// save before render and release after render.
	var _originalMaterialsTable = {};

	// object.uuid -> originalOnBeforeRender
	// save before render and release after render.
	var _originalOnBeforeRendersTable = {};

	// object.material.uuid -> object.material.visible or
	// object.material[ i ].uuid -> object.material[ i ].visible or
	// save before render and release after render.
	var _originalVisibleTable = {};

	// external properties

	this.renderer = undefined;
	this.domElement = undefined;

	this.forwardRendering = false; // for debug

	// private methods

	function init( parameters ) {

		_this.renderer = parameters.renderer !== undefined ? parameters.renderer : new WebGLRenderer();
		_this.domElement = _this.renderer.domElement;

		_context = _this.renderer.getContext();
		_state = _this.renderer.state;

		_width = parameters.width !== undefined ? parameters.width : _this.renderer.getSize( new Vector2() ).width;
		_height = parameters.height !== undefined ? parameters.height : _this.renderer.getSize( new Vector2() ).height;

		var antialias = parameters.antialias !== undefined ? parameters.antialias : false;

		if ( parameters.cacheKeepAlive !== undefined ) _cacheKeepAlive = parameters.cacheKeepAlive;

		initDepthTexture();

		initPassNormalDepth();
		initPassColor();
		initPassLight();
		initPassReconstruction();
		initPassFinal();

		_this.setSize( _width, _height );
		_this.setAntialias( antialias );
		_this.enableLightPrePass( false );

	}

	function initDepthTexture() {

		_depthTexture = new DepthTexture(
			_width,
			_height,
			UnsignedInt248Type,
			undefined,
			undefined,
			undefined,
			undefined,
			undefined,
			undefined,
			DepthStencilFormat
		);

	}

	function initPassNormalDepth() {

		_passNormalDepth = new RenderPass();
		_passNormalDepth.clear = true;

		var rt = new WebGLRenderTarget( _width, _height, {
			minFilter: NearestFilter,
			magFilter: NearestFilter,
			format: RGBAFormat,
			type: FloatType,
			stencilBuffer: true,
			depthTexture: _depthTexture
		} );

		rt.texture.generateMipamps = false;

		_compNormalDepth = new EffectComposer( _this.renderer, rt );
		_compNormalDepth.renderToScreen = false;
		_compNormalDepth.addPass( _passNormalDepth );

	}

	function initPassColor() {

		_passColor = new RenderPass();
		_passColor.clear = true;

		var rt = new WebGLRenderTarget( _width, _height, {
			minFilter: NearestFilter,
			magFilter: NearestFilter,
			format: RGBAFormat,
			type: FloatType,
			depthTexture: _depthTexture
		} );

		rt.texture.generateMipamps = false;

		_compColor = new EffectComposer( _this.renderer, rt );
		_compColor.renderToScreen = false;
		_compColor.addPass( _passColor );

	}

	function initPassLight() {

		_passLightFullscreen = new RenderPass();
		_passLightFullscreen.clear = true;
		_passLightFullscreen.camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );

		_passLight = new RenderPass();
		_passLight.clear = false;

		var rt = new WebGLRenderTarget( _width, _height, {
			minFilter: NearestFilter,
			magFilter: NearestFilter,
			format: RGBAFormat,
			type: FloatType,
			depthTexture: _depthTexture
		} );

		rt.texture.generateMipamps = false;

		_compLight = new EffectComposer( _this.renderer, rt );
		_compLight.renderToScreen = false;
		_compLight.addPass( _passLightFullscreen );
		_compLight.addPass( _passLight );

	}

	function initPassReconstruction() {

		_passReconstruction = new RenderPass();
		_passReconstruction.clear = true;

		var rt = new WebGLRenderTarget( _width, _height, {
			minFilter: NearestFilter,
			magFilter: NearestFilter,
			format: RGBAFormat,
			type: FloatType,
			depthTexture: _depthTexture
		} );

		rt.texture.generateMipamps = false;

		_compReconstruction = new EffectComposer( _this.renderer, rt );
		_compReconstruction.renderToScreen = false;
		_compReconstruction.addPass( _passReconstruction );

	}

	function initPassFinal() {

		_passFinal = new ShaderPass( ShaderDeferred[ 'final' ] );
		_passFinal.clear = true;
		_passFinal.uniforms.samplerResult.value = _compLight.renderTarget2.texture;
		_passFinal.material.blending = NoBlending;
		_passFinal.material.depthWrite = false;
		_passFinal.material.depthTest = false;

		_passForward = new RenderPass();
		_passForward.clear = false;

		_passCopy = new ShaderPass( CopyShader );

		_passFXAA = new ShaderPass( FXAAShader );

		var rt = new WebGLRenderTarget( _width, _height, {
			minFilter: NearestFilter,
			magFilter: LinearFilter,
			format: RGBFormat,
			type: UnsignedByteType,
			depthTexture: _depthTexture
		} );

		rt.texture.generateMipamps = false;

		_compFinal = new EffectComposer( _this.renderer, rt );
		_compFinal.addPass( _passFinal );
		_compFinal.addPass( _passForward );
		_compFinal.addPass( _passCopy );
		_compFinal.addPass( _passFXAA );

	}

	function initLightScene( scene ) {

		var lightSceneData = _lightScenesCache[ scene.uuid ];
		var lightFullscreenSceneData = _lightFullscreenScenesCache[ scene.uuid ];

		if ( lightSceneData === undefined ) {

			var s = new Scene();
			s.userData.lights = {};

			lightSceneData = createCacheData();
			lightSceneData.scene = s;

			_lightScenesCache[ scene.uuid ] = lightSceneData;

		}

		if ( lightFullscreenSceneData === undefined ) {

			var s = new Scene();
			s.userData.lights = {};

			var emissiveLight = createDeferredEmissiveLight();

			s.userData.emissiveLight = emissiveLight;
			s.add( emissiveLight );

			lightFullscreenSceneData = createCacheData();
			lightFullscreenSceneData.scene = s;

			_lightFullscreenScenesCache[ scene.uuid ] = lightFullscreenSceneData;

		}

		lightSceneData.used = true;
		lightFullscreenSceneData.used = true;

		var lightScene = lightSceneData.scene;
		var lightFullscreenScene = lightFullscreenSceneData.scene;

		// emissiveLight is only for Classic Deferred Rendering
		lightFullscreenScene.userData.emissiveLight.visible = ! _lightPrePass;

		_lightScene = lightScene;
		_lightFullscreenScene = lightFullscreenScene;

	}

	function getMaterialFromCacheOrCreate( originalMaterial, cache, createFunc, updateFunc ) {

		var data = cache[ originalMaterial.uuid ];

		if ( data === undefined ) {

			data = createCacheData();
			data.material = createFunc( originalMaterial );
			cache[ originalMaterial.uuid ] = data;

		}

		data.used = true;

		updateFunc( data.material, originalMaterial );

		_originalMaterialsTable[ data.material.uuid ] = originalMaterial;

		return data.material;

	}

	function overrideMaterialAndOnBeforeRender( object, getMaterialFunc, onBeforeRender ) {

		if ( object.material === undefined ) return;

		if ( Array.isArray( object.material ) ) {

			for ( var i = 0, il = object.material.length; i < il; i ++ ) {

				object.material[ i ] = getMaterialFunc( object.material[ i ] );

			}

		} else {

			object.material = getMaterialFunc( object.material );

		}

		object.onBeforeRender = onBeforeRender;

	}

	function restoreOriginalMaterial( object ) {

		if ( object.material === undefined ) return;

		if ( Array.isArray( object.material ) ) {

			for ( var i = 0, il = object.material.length; i < il; i ++ ) {

				object.material[ i ] = _originalMaterialsTable[ object.material[ i ].uuid ];

			}

		} else {

			object.material = _originalMaterialsTable[ object.material.uuid ];

		}

	}

	function setMaterialNormalDepth( object ) {

		overrideMaterialAndOnBeforeRender( object, getNormalDepthMaterial, updateDeferredNormalDepthUniforms );

	}

	function getNormalDepthMaterial( originalMaterial ) {

		return getMaterialFromCacheOrCreate(
			originalMaterial,
			( _lightPrePass ) ? _normalDepthShininessMaterialsCache : _normalDepthMaterialsCache,
			createDeferredNormalDepthMaterial,
			updateDeferredNormalDepthMaterial
		);

	}

	function createDeferredNormalDepthMaterial() {

		var shader = ( _lightPrePass ) ? ShaderDeferred[ 'normalDepthShininess' ] : ShaderDeferred[ 'normalDepth' ];

		return new ShaderMaterial( {
			uniforms: Object.assign( {}, shader.uniforms ),
			fragmentShader: shader.fragmentShader,
			vertexShader: shader.vertexShader,
			blending: NoBlending
		} );

	}

	function updateDeferredNormalDepthMaterial( material, originalMaterial ) {

		if ( originalMaterial.skinning !== undefined ) material.skinning = originalMaterial.skinning;
		if ( originalMaterial.morphTargets !== undefined ) material.morphTargets = originalMaterial.morphTargets;

		if ( originalMaterial.visible === true ) {

			material.visible = ! originalMaterial.transparent;

		} else {

			material.visible = false;

		}

	}

	function updateDeferredNormalDepthUniforms( renderer, scene, camera, geometry, material ) {

		if ( ! _lightPrePass ) return;

		var originalMaterial = _originalMaterialsTable[ material.uuid ];

		if ( originalMaterial === undefined || originalMaterial.shininess === undefined ) return;

		material.uniforms.shininess.value = originalMaterial.shininess;

	}

	function setMaterialColor( object ) {

		overrideMaterialAndOnBeforeRender( object, getColorMaterial, updateDeferredColorUniforms );

	}

	function getColorMaterial( originalMaterial ) {

		return getMaterialFromCacheOrCreate(
			originalMaterial,
			_colorMaterialsCache,
			createDeferredColorMaterial,
			updateDeferredColorMaterial
		);

	}

	function createDeferredColorMaterial( originalMaterial ) {

		var shader = ShaderDeferred[ 'color' ];

		var material = new ShaderMaterial( {
			uniforms: Object.assign( {}, shader.uniforms ),
			fragmentShader: shader.fragmentShader,
			vertexShader: shader.vertexShader,
			blending: NoBlending
		} );

		if ( originalMaterial.map !== undefined ) material.map = originalMaterial.map;

		return material;

	}

	function updateDeferredColorMaterial( material, originalMaterial ) {

		if ( originalMaterial.map !== undefined ) material.map = originalMaterial.map;
		if ( originalMaterial.skinning !== undefined ) material.skinning = originalMaterial.skinning;
		if ( originalMaterial.morphTargets !== undefined ) material.morphTargets = originalMaterial.morphTargets;

		if ( originalMaterial.visible === true ) {

			material.visible = ! originalMaterial.transparent;

		} else {

			material.visible = false;

		}

	}

	function updateDeferredColorUniforms( renderer, scene, camera, geometry, material ) {

		var originalMaterial = _originalMaterialsTable[ material.uuid ];
		var uniforms = material.uniforms;

		var diffuse, emissive;

		if ( originalMaterial.isMeshBasicMaterial === true ) {

			emissive = originalMaterial.color;

		} else {

			diffuse = originalMaterial.color;
			emissive = originalMaterial.emissive;

		}

		var specular = originalMaterial.specular;
		var shininess = originalMaterial.shininess;
		var map = originalMaterial.map;

		if ( diffuse !== undefined ) uniforms.diffuse.value.copy( diffuse );
		if ( emissive !== undefined ) uniforms.emissive.value.copy( emissive );
		if ( specular !== undefined ) uniforms.specular.value.copy( specular );
		if ( shininess !== undefined && uniforms.shininess !== undefined ) uniforms.shininess.value = shininess;
		if ( map !== undefined ) uniforms.map.value = map;

	}

	function setMaterialReconstruction( object ) {

		overrideMaterialAndOnBeforeRender( object, getReconstructionMaterial, updateDeferredReconstructionUniforms );

	}

	function getReconstructionMaterial( originalMaterial ) {

		if ( originalMaterial.transparent === true ) {

			_originalMaterialsTable[ originalMaterial.uuid ] = originalMaterial;
			return originalMaterial;

		}

		return getMaterialFromCacheOrCreate(
			originalMaterial,
			_reconstructionMaterialsCache,
			createDeferredReconstructionMaterial,
			updateDeferredReconstructionMaterial
		);

	}

	function createDeferredReconstructionMaterial( originalMaterial ) {

		var shader = ShaderDeferred[ 'reconstruction' ];

		var material = new ShaderMaterial( {
			uniforms: Object.assign( {}, shader.uniforms ),
			fragmentShader: shader.fragmentShader,
			vertexShader: shader.vertexShader,
			blending: NoBlending
		} );

		if ( originalMaterial.map !== undefined ) material.map = originalMaterial.map;

		return material;

	}

	function updateDeferredReconstructionMaterial( material, originalMaterial ) {

		updateDeferredColorMaterial( material, originalMaterial );

	}

	function updateDeferredReconstructionUniforms( renderer, scene, camera, geometry, material, group ) {

		if ( material.transparent === true ) {

			// 'this' is object here because this method is set as object.onBefore()
			var onBeforeRender = _originalOnBeforeRendersTable[ this.uuid ];

			if ( onBeforeRender ) {

				onBeforeRender.call( this, renderer, scene, camera, geometry, material, group );

			}

			return;

		}

		updateDeferredColorUniforms( renderer, scene, camera, geometry, material );

		material.uniforms.samplerLight.value = _compLight.renderTarget2.texture;

	}

	function setVisibleForForwardRendering( object ) {

		if ( object.material === undefined ) return;

		if ( Array.isArray( object.material ) ) {

			for ( var i = 0, il = object.material.length; i < il; i ++ ) {

				if ( _originalVisibleTable[ object.material[ i ].uuid ] === undefined ) {

					_originalVisibleTable[ object.material[ i ].uuid ] = object.material[ i ].visible;
					object.material[ i ].visible = object.material[ i ].transparent && object.material[ i ].visible;

				}

			}

		} else {

			if ( _originalVisibleTable[ object.material.uuid ] === undefined ) {

				_originalVisibleTable[ object.material.uuid ] = object.material.visible;
				object.material.visible = object.material.transparent && object.material.visible;

			}

		}

	}

	function restoreVisible( object ) {

		if ( object.material === undefined ) return;

		if ( Array.isArray( object.material ) ) {

			for ( var i = 0, il = object.material.length; i < il; i ++ ) {

				object.material[ i ].visible = _originalVisibleTable[ object.material[ i ].uuid ];

			}

		} else {

			object.material.visible = _originalVisibleTable[ object.material.uuid ];

		}

	}

	function createDeferredEmissiveLight() {

		var shader = ShaderDeferred[ 'emissiveLight' ];

		var material = new ShaderMaterial( {
			uniforms: Object.assign( {}, shader.uniforms ),
			vertexShader: shader.vertexShader,
			fragmentShader: shader.fragmentShader,
			blending: NoBlending,
			depthWrite: false
		} );

		var geometry = new PlaneBufferGeometry( 2, 2 );
		var mesh = new Mesh( geometry, material );

		mesh.onBeforeRender = function ( renderer, scene, camera, geometry, material ) {

			material.uniforms.samplerColor.value = _compColor.renderTarget2.texture;

		};

		return mesh;

	}

	function createDeferredLight( originalLight ) {

		if ( originalLight.isPointLight ) {

			return createDeferredPointLight( originalLight );

		} else if ( originalLight.isSpotLight ) {

			return createDeferredSpotLight( originalLight );

		} else if ( originalLight.isDirectionalLight ) {

			return createDeferredDirectionalLight( originalLight );

		}

		return null;

	}

	function createDeferredLightMaterial( originalLight ) {

		if ( originalLight.isPointLight ) {

			return createDeferredPointLightMaterial();

		} else if ( originalLight.isSpotLight ) {

			return createDeferredSpotLightMaterial();

		} else if ( originalLight.isDirectionalLight ) {

			return createDeferredDirectionalLightMaterial();

		}

		return null;

	}

	function getDeferredLightMaterial( light ) {

		var cache = ( _lightPrePass ) ? _lightPrePassMaterialsCache : _classicDeferredLightMaterialsCache;

		var data = cache[ light.uuid ];

		if ( data === undefined ) {

			data = createCacheData();
			data.material = createDeferredLightMaterial( light.userData.originalLight );
			cache[ light.uuid ] = data;

		}

		data.used = true;

		return data.material;

	}

	function updateDeferredLight( light ) {

		var originalLight = light.userData.originalLight;

		if ( originalLight.isPointLight ) {

			updateDeferredPointLight( light );

		}

	}

	function createDeferredLightMesh( light, geometry ) {

		var mesh = new Mesh( geometry, _tmpMaterial );

		mesh.userData.originalLight = light;

		return mesh;

	}

	function createDeferredLightShaderMaterial( shader ) {

		var material = new ShaderMaterial( {
			uniforms: Object.assign( {}, shader.uniforms ),
			vertexShader: shader.vertexShader,
			fragmentShader: shader.fragmentShader,
			transparent: true,
			blending: AdditiveBlending,
			depthWrite: false
		} );

		if ( _lightPrePass ) material.premultipliedAlpha = true;

		return material;

	}

	function updateDeferredLightCommonUniforms( uniforms ) {

		if ( _lightPrePass ) {

			uniforms.samplerNormalDepthShininess.value = _compNormalDepth.renderTarget2.texture;

		} else {

			uniforms.samplerNormalDepth.value = _compNormalDepth.renderTarget2.texture;
			uniforms.samplerColor.value = _compColor.renderTarget2.texture;

		}

	}

	function createDeferredPointLight( light ) {

		var mesh = createDeferredLightMesh( light, new SphereBufferGeometry( 1, 16, 8 ) );
		mesh.onBeforeRender = updateDeferredPointLightUniforms;
		return mesh;

	}

	/*
	 * optimization:
	 * Renders PointLight only back face with stencil test.
	 */
	function createDeferredPointLightMaterial() {

		var shader = ( _lightPrePass ) ? ShaderDeferred[ 'pointLightPre' ] : ShaderDeferred[ 'pointLight' ];

		var material = createDeferredLightShaderMaterial( shader );

		material.side = BackSide;
		material.depthFunc = GreaterEqualDepth;

		return material;

	}

	function updateDeferredPointLight( light ) {

		var originalLight = light.userData.originalLight;
		var distance = originalLight.distance;

		if ( distance > 0 ) {

			light.scale.set( 1, 1, 1 ).multiplyScalar( distance );
			light.position.setFromMatrixPosition( originalLight.matrixWorld );

		}

	}

	function updateDeferredPointLightUniforms( renderer, scene, camera, geometry, material ) {

		var light = this;

		var originalLight = light.userData.originalLight;
		var distance = originalLight.distance;
		var uniforms = material.uniforms;

		uniforms.lightColor.value.copy( originalLight.color );

		if ( distance > 0 ) {

			uniforms.lightRadius.value = distance;
			uniforms.lightIntensity.value = originalLight.intensity;
			uniforms.lightPositionVS.value.setFromMatrixPosition( originalLight.matrixWorld ).applyMatrix4( _currentCamera.matrixWorldInverse );

		} else {

			uniforms.lightRadius.value = Infinity;

		}

		updateDeferredLightCommonUniforms( uniforms );

	}

	function createDeferredSpotLight( light ) {

		var mesh = createDeferredLightMesh( light, new PlaneBufferGeometry( 2, 2 ) );
		mesh.onBeforeRender = updateDeferredSpotLightUniforms;
		return mesh;

	}

	function createDeferredSpotLightMaterial() {

		var shader = ( _lightPrePass ) ? ShaderDeferred[ 'spotLightPre' ] : ShaderDeferred[ 'spotLight' ];

		var material = createDeferredLightShaderMaterial( shader );

		material.depthTest = false;

		return material;

	}

	function updateDeferredSpotLightUniforms() {

		var light = this;

		var originalLight = light.userData.originalLight;
		var uniforms = light.material.uniforms;

		uniforms.lightAngle.value = originalLight.angle;
		uniforms.lightColor.value.copy( originalLight.color );
		uniforms.lightIntensity.value = originalLight.intensity;
		uniforms.lightPositionVS.value.setFromMatrixPosition( originalLight.matrixWorld ).applyMatrix4( _currentCamera.matrixWorldInverse );

		var vec = uniforms.lightDirectionVS.value;
		var vec2 = _tmpVector3;

		vec.setFromMatrixPosition( originalLight.matrixWorld );
		vec2.setFromMatrixPosition( originalLight.target.matrixWorld );
		vec.sub( vec2 ).normalize().transformDirection( _currentCamera.matrixWorldInverse );

		updateDeferredLightCommonUniforms( uniforms );

	}

	function createDeferredDirectionalLight( light ) {

		var mesh = createDeferredLightMesh( light, new PlaneBufferGeometry( 2, 2 ) );
		mesh.onBeforeRender = updateDeferredDirectionalLightUniforms;
		return mesh;

	}

	function createDeferredDirectionalLightMaterial() {

		var shader = ( _lightPrePass ) ? ShaderDeferred[ 'directionalLightPre' ] : ShaderDeferred[ 'directionalLight' ];

		var material = createDeferredLightShaderMaterial( shader );

		material.depthTest = false;

		return material;

	}

	function updateDeferredDirectionalLightUniforms() {

		var light = this;

		var originalLight = light.userData.originalLight;
		var uniforms = light.material.uniforms;

		uniforms.lightColor.value.copy( originalLight.color );
		uniforms.lightIntensity.value = originalLight.intensity;

		var vec = uniforms.lightDirectionVS.value;
		var vec2 = _tmpVector3;

		vec.setFromMatrixPosition( originalLight.matrixWorld );
		vec2.setFromMatrixPosition( originalLight.target.matrixWorld );
		vec.sub( vec2 ).normalize().transformDirection( _currentCamera.matrixWorldInverse );

		updateDeferredLightCommonUniforms( uniforms );

	}

	function saveOriginalOnBeforeRenderAndCheckTransparency( object ) {

		if ( object.material === undefined ) return;

		_originalOnBeforeRendersTable[ object.uuid ] = object.onBeforeRender;

		// _hasTransparentObject is used only for Classic Deferred Rendering
		if ( _hasTransparentObject || _lightPrePass ) return;

		if ( ! object.visible ) return;

		if ( Array.isArray( object.material ) ) {

			for ( var i = 0, il = object.material.length; i < il; i ++ ) {

				if ( object.material[ i ].visible === true && object.material[ i ].transparent === true ) {

					_hasTransparentObject = true;
					break;

				}

			}

		} else {

			if ( object.material.visible === true && object.material.transparent === true ) _hasTransparentObject = true;

		}

	}

	function restoreOriginalOnBeforeRender( object ) {

		if ( object.material === undefined ) return;

		object.onBeforeRender = _originalOnBeforeRendersTable[ object.uuid ];

	}

	function addDeferredLightsToLightScene( object ) {

		if ( object.isLight !== true ) return;

		var data = _deferredLightsCache[ object.uuid ];

		if ( data === undefined ) {

			data = createCacheData();
			data.light = createDeferredLight( object );
			_deferredLightsCache[ object.uuid ] = data;

		}

		data.used = true;

		var light = data.light;

		if ( light === null ) return;

		var scene = ( object.isPointLight === true ) ? _lightScene : _lightFullscreenScene;

		var lights = scene.userData.lights;

		if ( lights[ light.uuid ] === undefined ) {

			scene.add( light );

			lights[ light.uuid ] = {
				light: light,
				found: true
			};

		}

		lights[ light.uuid ].found = true;

	}

	function updateDeferredLightsInLightScene( scene ) {

		var lights = scene.userData.lights;
		var keys = Object.keys( lights );

		for ( var i = 0, il = keys.length; i < il; i ++ ) {

			var key = keys[ i ];

			if ( lights[ key ].found === false ) {

				scene.remove( lights[ key ].light );
				delete lights[ key ];

			} else {

				var light = lights[ key ].light;
				light.material = getDeferredLightMaterial( light );

				updateDeferredLight( light );
				lights[ key ].found = false;

			}

		}

	}

	function updateDeferredCommonUniforms( camera ) {

		var uniforms = ShaderDeferredCommon[ 'commonUniforms' ];

		uniforms.viewWidth.value = _width;
		uniforms.viewHeight.value = _height;

		uniforms.matProjInverse.value.getInverse( camera.projectionMatrix );

	}

	function enableFinalPasses() {

		if ( _lightPrePass ) {

			_passForward.enabled = false;
			_passCopy.enabled = false;

			if ( _antialias ) {

				_passFXAA.enabled = true;

			} else {

				_passFXAA.enabled = false;

			}

		} else {

			if ( _hasTransparentObject ) {

				if ( _antialias ) {

					_passForward.enabled = true;
					_passCopy.enabled = false;
					_passFXAA.enabled = true;

				} else {

					_passForward.enabled = true;
					_passCopy.enabled = true;
					_passFXAA.enabled = false;

				}

			} else {

				if ( _antialias ) {

					_passForward.enabled = false;
					_passCopy.enabled = false;
					_passFXAA.enabled = true;

				} else {

					_passForward.enabled = false;
					_passCopy.enabled = false;
					_passFXAA.enabled = false;

				}

			}

		}

	}

	function createCacheData() {

		return {
			used: true,
			keepAlive: _cacheKeepAlive,
			count: 0
		};

	}

	function cleanupCache( cache ) {

		var keys = Object.keys( cache );

		for ( var i = 0, il = keys.length; i < il; i ++ ) {

			var key = keys[ i ];

			if ( cache[ key ].used === false ) {

				cache[ key ].count ++;

				if ( cache[ key ].keepAlive === false && cache[ key ].count > _removeThresholdCount ) {

					delete cache[ key ];

				}

			} else {

				cache[ key ].used = false;
				cache[ key ].count = 0;

			}

		}

	}

	function cleanupTable( table ) {

		var keys = Object.keys( table );

		for ( var i = 0, il = keys.length; i < il; i ++ ) {

			var key = keys[ i ];

			table[ key ] = undefined;

		}

	}

	function cleanupCaches() {

		cleanupCache( _lightScenesCache );
		cleanupCache( _lightFullscreenScenesCache );
		cleanupCache( _normalDepthMaterialsCache );
		cleanupCache( _normalDepthShininessMaterialsCache );
		cleanupCache( _colorMaterialsCache );
		cleanupCache( _reconstructionMaterialsCache );
		cleanupCache( _classicDeferredLightMaterialsCache );
		cleanupCache( _lightPrePassMaterialsCache );
		cleanupCache( _deferredLightsCache );

		cleanupTable( _originalMaterialsTable );
		cleanupTable( _originalOnBeforeRendersTable );
		cleanupTable( _originalVisibleTable );

	}

	/*
	 * Classic Deferred Rendering
	 *
	 * 1) g-buffer normal + depth pass
	 *
	 * RGB: normal
	 *   A: depth
	 *
	 *
	 * Light Pre-Pass Rendering
	 *
	 * 1') g-buffer normal + depth pass + shininess
	 *
	 *  RG: normal
	 *   B: shininess
	 *   A: depth
	 */

	function renderNormalDepth( scene, camera ) {

		scene.traverse( setMaterialNormalDepth );

		_passNormalDepth.scene = scene;
		_passNormalDepth.camera = camera;

		_this.renderer.autoClearDepth = true;
		_this.renderer.autoClearStencil = true;

		_state.buffers.stencil.setTest( true );
		_state.buffers.stencil.setFunc( _context.ALWAYS, 1, 0xffffffff );
		_state.buffers.stencil.setOp( _context.REPLACE, _context.REPLACE, _context.REPLACE );

		_compNormalDepth.render();

		scene.traverse( restoreOriginalMaterial );

	}

	/*
	 * Classic Deferred Rendering
	 *
	 * 2) g-buffer color pass
	 *
	 * R: diffuse
	 * G: emissive
	 * B: specular
	 * A: shininess
	 */

	function renderColor( scene, camera ) {

		scene.traverse( setMaterialColor );

		_passColor.scene = scene;
		_passColor.camera = camera;

		_this.renderer.autoClearDepth = false;
		_this.renderer.autoClearStencil = false;

		_state.buffers.stencil.setFunc( _context.EQUAL, 1, 0xffffffff );
		_state.buffers.stencil.setOp( _context.KEEP, _context.KEEP, _context.KEEP );

		_compColor.render();

		scene.traverse( restoreOriginalMaterial );

	}

	/*
	 * Classic Deferred Rendering
	 *
	 * 3) light pass
	 */

	function renderLight( scene, camera ) {

		scene.traverse( addDeferredLightsToLightScene );

		updateDeferredLightsInLightScene( _lightScene );
		updateDeferredLightsInLightScene( _lightFullscreenScene );

		_passLight.scene = _lightScene;
		_passLight.camera = camera;

		_passLightFullscreen.scene = _lightFullscreenScene;

		_this.renderer.autoClearDepth = false;
		_this.renderer.autoClearStencil = false;

		_compLight.render();

		_state.buffers.stencil.setTest( false );

	}

	/*
	 * Light Pre-Pass Rendering
	 *
	 * 2') Light pre pass
	 */

	function renderLightPre( scene, camera ) {

		scene.traverse( addDeferredLightsToLightScene );

		updateDeferredLightsInLightScene( _lightScene );
		updateDeferredLightsInLightScene( _lightFullscreenScene );

		_passLight.scene = _lightScene;
		_passLight.camera = camera;

		_passLightFullscreen.scene = _lightFullscreenScene;

		_this.renderer.autoClearDepth = false;
		_this.renderer.autoClearStencil = false;

		_state.buffers.stencil.setFunc( _context.EQUAL, 1, 0xffffffff );
		_state.buffers.stencil.setOp( _context.KEEP, _context.KEEP, _context.KEEP );

		_compLight.render();

	}

	/*
	 * Light Pre-Pass Rendering
	 *
	 * 3') Reconstruction pass
	 *
	 * Transprency handling:
	 * Here renders transparent objects with normal forward rendering.
	 */

	function renderReconstruction( scene, camera ) {

		scene.traverse( setMaterialReconstruction );

		_passReconstruction.scene = scene;
		_passReconstruction.camera = camera;

		_this.renderer.autoClearDepth = false;
		_this.renderer.autoClearStencil = false;

		_compReconstruction.render();

		_state.buffers.stencil.setTest( false );

		scene.traverse( restoreOriginalMaterial );

	}

	/*
	 * Classic Deferred Rendering
	 *
	 * 4) Final pass
	 *
	 * transparency handling:
	 * If there's any transparent objects, here renders them on the deferred rendering result
	 * with normal forward rendering. This may be the easist way but heavy.
	 * We should consider any better ways someday.
	 *
	 *
	 * Light Pre-Pass Rendering
	 *
	 * 4') Final pass
	 *
	 *
	 * Common
	 *
	 * antialias handling:
	 * Here uses postprocessing FXAA for antialias.
	 *
	 */

	function renderFinal( scene, camera ) {

		if ( ! _lightPrePass && _hasTransparentObject ) {

			scene.traverse( setVisibleForForwardRendering );
			scene.traverse( restoreOriginalOnBeforeRender );

			_passForward.scene = scene;
			_passForward.camera = camera;

		}

		enableFinalPasses();

		_this.renderer.autoClearDepth = false;
		_this.renderer.autoClearStencil = false;

		_compFinal.render();

		if ( ! _lightPrePass && _hasTransparentObject ) {

			scene.traverse( restoreVisible );

		}

	}

	// external APIs

	this.setSize = function ( width, height ) {

		_width = width;
		_height = height;

		this.renderer.setSize( _width, _height );

		_compNormalDepth.setSize( _width, _height );
		_compColor.setSize( _width, _height );
		_compLight.setSize( _width, _height );
		_compReconstruction.setSize( _width, _height );
		_compFinal.setSize( _width, _height );

		_depthTexture.image.width = _width;
		_depthTexture.image.height = _height;
		_depthTexture.needsUpdate = true;

		_passFXAA.uniforms.resolution.value.set( 1 / _width, 1 / _height );

	};

	this.setAntialias = function ( enabled ) {

		_antialias = enabled;

	};

	this.enableLightPrePass = function ( enabled ) {

		_lightPrePass = enabled;

		_passFinal.uniforms.samplerResult.value = ( _lightPrePass ) ? _compReconstruction.renderTarget2.texture : _compLight.renderTarget2.texture;

	};

	this.render = function ( scene, camera ) {

		// for debug to compare with normal forward rendering

		if ( this.forwardRendering ) {

			this.renderer.render( scene, camera );
			return;

		}

		var currentSceneAutoUpdate = scene.autoUpdate;
		var currentAutoClearColor = this.renderer.autoClearColor;
		var currentAutoClearDepth = this.renderer.autoClearDepth;
		var currentAutoClearStencil = this.renderer.autoClearStencil;

		_currentCamera = camera;

		initLightScene( scene );

		scene.autoUpdate = false;
		scene.updateMatrixWorld();

		_hasTransparentObject = false;

		scene.traverse( saveOriginalOnBeforeRenderAndCheckTransparency );

		updateDeferredCommonUniforms( camera );

		renderNormalDepth( scene, camera );

		if ( _lightPrePass ) {

			renderLightPre( scene, camera );
			renderReconstruction( scene, camera );

		} else {

			renderColor( scene, camera );
			renderLight( scene, camera );

		}

		renderFinal( scene, camera );

		scene.traverse( restoreOriginalOnBeforeRender );

		cleanupCaches();

		scene.autoUpdate = currentSceneAutoUpdate;
		this.renderer.autoClearColor = currentAutoClearColor;
		this.renderer.autoClearDepth = currentAutoClearDepth;
		this.renderer.autoClearStencil = currentAutoClearStencil;

	};

	// initialize

	init( parameters );

};

var DeferredShaderChunk = {

	packVector3: [

		"float vec3_to_float( vec3 data ) {",

		"	const float unit = 255.0/256.0;",
		"	highp float compressed = fract( data.x * unit ) + floor( data.y * unit * 255.0 ) + floor( data.z * unit * 255.0 ) * 255.0;",
		"	return compressed;",

		"}"

	].join( "\n" ),

	unpackFloat: [

		"vec3 float_to_vec3( float data ) {",

		"	const float unit = 255.0;",
		"	vec3 uncompressed;",
		"	uncompressed.x = fract( data );",
		"	float zInt = floor( data / unit );",
		"	uncompressed.z = fract( zInt / unit );",
		"	uncompressed.y = fract( floor( data - ( zInt * unit ) ) / unit );",
		"	return uncompressed;",

		"}"

	].join( "\n" ),

	// Refer to http://aras-p.info/texts/CompactNormalStorage.html
	packNormal: [

		"vec2 normal_to_vec2( vec3 normal ) {",

		"	return normal.xy / sqrt( normal.z * 8.0 + 8.0 ) + 0.5;",

		"}"

	].join( "\n" ),

	unpackVector2: [

		"vec3 vec2_to_normal( vec2 data ) {",

		"	vec2 fenc = data * 4.0 - 2.0;",
		"	float f = dot( fenc, fenc );",
		"	float g = sqrt( 1.0 - f / 4.0 );",
		"	vec3 normal;",
		"	normal.xy = fenc * g;",
		"	normal.z = 1.0 - f / 2.0;",
		"	return normal;",

		"}"

	].join( "\n" ),

	computeTextureCoord: [

		"vec2 texCoord = gl_FragCoord.xy / vec2( viewWidth, viewHeight );"

	].join( "\n" ),

	packNormalDepth: [

		"vec4 packedNormalDepth;",
		"packedNormalDepth.xyz = normal * 0.5 + 0.5;",
		"packedNormalDepth.w = position.z / position.w;"

	].join( "\n" ),

	unpackNormalDepth: [

		"vec4 normalDepthMap = texture2D( samplerNormalDepth, texCoord );",
		"float depth = normalDepthMap.w;",

		"if ( depth == 0.0 ) discard;",

		"vec3 normal = normalDepthMap.xyz * 2.0 - 1.0;"

	].join( "\n" ),

	packNormalDepthShininess: [

		"vec4 packedNormalDepthShininess;",
		"packedNormalDepthShininess.xy = normal_to_vec2( normal );",
		"packedNormalDepthShininess.z = shininess;",
		"packedNormalDepthShininess.w = position.z / position.w;"

	].join( "\n" ),

	unpackNormalDepthShininess: [

		"vec4 normalDepthMap = texture2D( samplerNormalDepthShininess, texCoord );",
		"float depth = normalDepthMap.w;",

		"if ( depth == 0.0 ) discard;",

		"vec3 normal = vec2_to_normal( normalDepthMap.xy );",
		"float shininess = normalDepthMap.z;"

	].join( "\n" ),

	packColor: [

		"vec4 packedColor;",
		"packedColor.x = vec3_to_float( diffuseColor.rgb );",
		"packedColor.y = vec3_to_float( emissiveColor );",
		"packedColor.z = vec3_to_float( specularColor );",
		"packedColor.w = shininess;"

	].join( "\n" ),

	unpackColor: [

		"vec4 colorMap = texture2D( samplerColor, texCoord );",
		"vec3 diffuseColor = float_to_vec3( colorMap.x );",
		"vec3 emissiveColor = float_to_vec3( colorMap.y );",
		"vec3 specularColor = float_to_vec3( colorMap.z );",
		"float shininess = colorMap.w;"

	].join( "\n" ),

	packLight: [

		"vec4 packedLight;",
		"packedLight.xyz = lightIntensity * lightColor * max( dot( lightVector, normal ), 0.0 ) * attenuation;",
		"packedLight.w = lightIntensity * specular * max( dot( lightVector, normal ), 0.0 ) * attenuation;"

	].join( "\n" ),

	computeVertexPositionVS: [

		"vec2 xy = texCoord * 2.0 - 1.0;",
		"vec4 vertexPositionProjected = vec4( xy, depth, 1.0 );",
		"vec4 vertexPositionVS = matProjInverse * vertexPositionProjected;",
		"vertexPositionVS.xyz /= vertexPositionVS.w;",
		"vertexPositionVS.w = 1.0;"

	].join( "\n" ),

	// TODO: calculate schlick
	computeSpecular: [

		"vec3 halfVector = normalize( lightVector - normalize( vertexPositionVS.xyz ) );",
		"float dotNormalHalf = max( dot( normal, halfVector ), 0.0 );",
		"float specular = 0.31830988618 * ( shininess * 0.5 + 1.0 ) * pow( dotNormalHalf, shininess );"

	].join( "\n" ),

	combine: [

		"gl_FragColor = vec4( lightIntensity * lightColor * max( dot( lightVector, normal ), 0.0 ) * ( diffuseColor + specular * specularColor ) * attenuation, 1.0 );"

	].join( "\n" )

};

var ShaderDeferredCommon = {

	commonUniforms: {

		matProjInverse: new Uniform( new Matrix4() ),

		viewWidth: new Uniform( 800 ),
		viewHeight: new Uniform( 600 )

	}

};

var ShaderDeferred = {

	normalDepth: {

		uniforms: {},

		vertexShader: [

			"varying vec3 vNormal;",
			"varying vec4 vPosition;",

			"#include <morphtarget_pars_vertex>",
			"#include <skinning_pars_vertex>",

			"void main() {",

			"#include <begin_vertex>",
			"#include <beginnormal_vertex>",
			"#include <skinbase_vertex>",
			"#include <skinnormal_vertex>",
			"#include <defaultnormal_vertex>",
			"#include <morphtarget_vertex>",
			"#include <skinning_vertex>",
			"#include <project_vertex>",

			"	vNormal = normalize( transformedNormal );",
			"	vPosition = gl_Position;",

			"}"

		].join( "\n" ),

		fragmentShader: [

			"varying vec3 vNormal;",
			"varying vec4 vPosition;",

			"void main() {",

			"	vec3 normal = vNormal;",
			"	vec4 position = vPosition;",

			DeferredShaderChunk[ "packNormalDepth" ],

			"	gl_FragColor = packedNormalDepth;",

			"}"

		].join( "\n" )

	},

	color: {

		uniforms: {

			map: new Uniform( null ),
			offsetRepeat: new Uniform( new Vector4( 0, 0, 1, 1 ) ),

			diffuse: new Uniform( new Color( 0x000000 ) ),
			emissive: new Uniform( new Color( 0x000000 ) ),
			specular: new Uniform( new Color( 0x000000 ) ),
			shininess: new Uniform( 30.0 )

		},

		vertexShader: [

			"#include <uv_pars_vertex>",
			"#include <morphtarget_pars_vertex>",
			"#include <skinning_pars_vertex>",

			"void main() {",

			"#include <uv_vertex>",
			"#include <begin_vertex>",
			"#include <beginnormal_vertex>",
			"#include <skinbase_vertex>",
			"#include <skinnormal_vertex>",
			"#include <defaultnormal_vertex>",
			"#include <morphtarget_vertex>",
			"#include <skinning_vertex>",
			"#include <project_vertex>",

			"}"

		].join( "\n" ),

		fragmentShader: [

			"uniform vec3 diffuse;",
			"uniform vec3 emissive;",
			"uniform vec3 specular;",
			"uniform float shininess;",

			"#include <uv_pars_fragment>",
			"#include <map_pars_fragment>",
			DeferredShaderChunk[ "packVector3" ],

			"void main() {",

			"	vec4 diffuseColor = vec4( diffuse, 1.0 );",
			"	vec3 emissiveColor = emissive;",
			"	vec3 specularColor = specular;",

			"#include <map_fragment>",
			DeferredShaderChunk[ "packColor" ],

			"	gl_FragColor = packedColor;",

			"}"

		].join( "\n" )

	},

	emissiveLight: {

		uniforms: Object.assign(

			{

				samplerColor: new Uniform( null )

			},

			ShaderDeferredCommon[ 'commonUniforms' ]

		),

		vertexShader: [

			"void main() { ",

			"	gl_Position = vec4( sign( position.xy ), 0.0, 1.0 );",

			"}"

		].join( '\n' ),

		fragmentShader: [

			"uniform sampler2D samplerColor;",

			"uniform float viewHeight;",
			"uniform float viewWidth;",

			DeferredShaderChunk[ "unpackFloat" ],

			"void main() {",

			DeferredShaderChunk[ "computeTextureCoord" ],
			DeferredShaderChunk[ "unpackColor" ],

			"	gl_FragColor = vec4( emissiveColor, 1.0 );",

			"}"

		].join( '\n' )

	},

	pointLight: {

		uniforms: Object.assign(

			{

				samplerNormalDepth: new Uniform( null ),
				samplerColor: new Uniform( null ),

				lightColor: new Uniform( new Color( 0x000000 ) ),
				lightPositionVS: new Uniform( new Vector3( 0, 1, 0 ) ),
				lightIntensity: new Uniform( 1.0 ),
				lightRadius: new Uniform( 1.0 )

			},

			ShaderDeferredCommon[ 'commonUniforms' ]

		),

		vertexShader: [

			"void main() {",

			"	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",

			"}"

		].join( "\n" ),

		fragmentShader: [

			"uniform sampler2D samplerNormalDepth;",
			"uniform sampler2D samplerColor;",

			"uniform float viewHeight;",
			"uniform float viewWidth;",

			"uniform vec3 lightColor;",
			"uniform vec3 lightPositionVS;",
			"uniform float lightIntensity;",
			"uniform float lightRadius;",

			"uniform mat4 matProjInverse;",

			DeferredShaderChunk[ "unpackFloat" ],

			"void main() {",

			DeferredShaderChunk[ "computeTextureCoord" ],
			DeferredShaderChunk[ "unpackNormalDepth" ],
			DeferredShaderChunk[ "computeVertexPositionVS" ],

			"	vec3 lightVector = lightPositionVS - vertexPositionVS.xyz;",
			"	float distance = length( lightVector );",

			"	if ( distance > lightRadius ) discard;",

			"	lightVector = normalize( lightVector );",

			DeferredShaderChunk[ "unpackColor" ],
			DeferredShaderChunk[ "computeSpecular" ],

			"	//float cutoff = 0.3;",
			"	//float denom = distance / lightRadius + 1.0;",
			"	//float attenuation = 1.0 / ( denom * denom );",
			"	//attenuation = ( attenuation - cutoff ) / ( 1.0 - cutoff );",
			"	//attenuation = max( attenuation, 0.0 );",
			"	//attenuation *= attenuation;",

			"	//diffuseColor *= saturate( -distance / lightRadius + 1.0 );",
			"	//float attenuation = 1.0;",

			"	float attenuation = saturate( -distance / lightRadius + 1.0 );",

			DeferredShaderChunk[ "combine" ],

			"}"

		].join( "\n" )

	},

	spotLight: {

		uniforms: Object.assign(

			{

				samplerNormalDepth: new Uniform( null ),
				samplerColor: new Uniform( null ),

				lightColor: new Uniform( new Color( 0x000000 ) ),
				lightDirectionVS: new Uniform( new Vector3( 0, 1, 0 ) ),
				lightPositionVS: new Uniform( new Vector3( 0, 1, 0 ) ),
				lightAngle: new Uniform( 1.0 ),
				lightIntensity: new Uniform( 1.0 )

			},

			ShaderDeferredCommon[ 'commonUniforms' ]

		),

		vertexShader: [

			"void main() { ",

			"	gl_Position = vec4( sign( position.xy ), 0.0, 1.0 );",

			"}"

		].join( "\n" ),

		fragmentShader: [

			"uniform sampler2D samplerNormalDepth;",
			"uniform sampler2D samplerColor;",

			"uniform float viewHeight;",
			"uniform float viewWidth;",

			"uniform vec3 lightColor;",
			"uniform vec3 lightPositionVS;",
			"uniform vec3 lightDirectionVS;",
			"uniform float lightAngle;",
			"uniform float lightIntensity;",

			"uniform mat4 matProjInverse;",

			DeferredShaderChunk[ "unpackFloat" ],

			"void main() {",

			DeferredShaderChunk[ "computeTextureCoord" ],
			DeferredShaderChunk[ "unpackNormalDepth" ],
			DeferredShaderChunk[ "computeVertexPositionVS" ],
			DeferredShaderChunk[ "unpackColor" ],

			"	vec3 lightVector = normalize( lightPositionVS.xyz - vertexPositionVS.xyz );",

			"	float rho = dot( lightDirectionVS, lightVector );",
			"	float rhoMax = cos( lightAngle );",

			"	if ( rho <= rhoMax ) discard;",

			"	float theta = rhoMax + 0.0001;",
			"	float phi = rhoMax + 0.05;",
			"	float falloff = 4.0;",

			"	float spot = 0.0;",

			"	if ( rho >= phi ) {",

			"		spot = 1.0;",

			"	} else if ( rho <= theta ) {",

			"		spot = 0.0;",

			"	} else { ",

			"		spot = pow( ( rho - theta ) / ( phi - theta ), falloff );",

			"	}",

			"	diffuseColor *= spot;",

			DeferredShaderChunk[ "computeSpecular" ],

			"	const float attenuation = 1.0;",

			DeferredShaderChunk[ "combine" ],

			"}"

		].join( "\n" )

	},

	directionalLight: {

		uniforms: Object.assign(

			{

				samplerNormalDepth: new Uniform( null ),
				samplerColor: new Uniform( null ),

				lightColor: new Uniform( new Color( 0x000000 ) ),
				lightDirectionVS: new Uniform( new Vector3( 0, 1, 0 ) ),
				lightIntensity: new Uniform( 1.0 )
			},

			ShaderDeferredCommon[ 'commonUniforms' ]

		),

		vertexShader: [

			"void main() { ",

			"	gl_Position = vec4( sign( position.xy ), 0.0, 1.0 );",

			"}"

		].join( '\n' ),

		fragmentShader: [

			"uniform sampler2D samplerNormalDepth;",
			"uniform sampler2D samplerColor;",

			"uniform float viewHeight;",
			"uniform float viewWidth;",

			"uniform vec3 lightColor;",
			"uniform vec3 lightDirectionVS;",
			"uniform float lightIntensity;",

			"uniform mat4 matProjInverse;",

			DeferredShaderChunk[ "unpackFloat" ],

			"void main() {",

			DeferredShaderChunk[ "computeTextureCoord" ],
			DeferredShaderChunk[ "unpackNormalDepth" ],
			DeferredShaderChunk[ "computeVertexPositionVS" ],
			DeferredShaderChunk[ "unpackColor" ],

			"	vec3 lightVector = normalize( lightDirectionVS );",

			DeferredShaderChunk[ "computeSpecular" ],

			"	const float attenuation = 1.0;",

			DeferredShaderChunk[ "combine" ],

			"}"

		].join( '\n' )

	},

	normalDepthShininess: {

		uniforms: {

			shininess: new Uniform( 30.0 )

		},

		vertexShader: [

			"varying vec3 vNormal;",
			"varying vec4 vPosition;",

			"#include <morphtarget_pars_vertex>",
			"#include <skinning_pars_vertex>",

			"void main() {",

			"#include <begin_vertex>",
			"#include <beginnormal_vertex>",
			"#include <skinbase_vertex>",
			"#include <skinnormal_vertex>",
			"#include <defaultnormal_vertex>",
			"#include <morphtarget_vertex>",
			"#include <skinning_vertex>",
			"#include <project_vertex>",

			"	vNormal = normalize( transformedNormal );",
			"	vPosition = gl_Position;",

			"}"

		].join( "\n" ),

		fragmentShader: [

			"varying vec3 vNormal;",
			"varying vec4 vPosition;",

			"uniform float shininess;",

			DeferredShaderChunk[ "packNormal" ],

			"void main() {",

			"	vec3 normal = vNormal;",
			"	vec4 position = vPosition;",

			DeferredShaderChunk[ "packNormalDepthShininess" ],

			"	gl_FragColor = packedNormalDepthShininess;",

			"}"

		].join( "\n" )

	},

	pointLightPre: {

		uniforms: Object.assign(

			{

				samplerNormalDepthShininess: new Uniform( null ),

				lightColor: new Uniform( new Color( 0x000000 ) ),
				lightPositionVS: new Uniform( new Vector3( 0, 1, 0 ) ),
				lightIntensity: new Uniform( 1.0 ),
				lightRadius: new Uniform( 1.0 )
			},

			ShaderDeferredCommon[ 'commonUniforms' ]

		),


		vertexShader: [

			"void main() {",

			"	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",

			"}"

		].join( "\n" ),

		fragmentShader: [

			"uniform sampler2D samplerNormalDepthShininess;",

			"uniform float viewHeight;",
			"uniform float viewWidth;",

			"uniform vec3 lightColor;",
			"uniform vec3 lightPositionVS;",
			"uniform float lightIntensity;",
			"uniform float lightRadius;",

			"uniform mat4 matProjInverse;",

			DeferredShaderChunk[ "unpackFloat" ],
			DeferredShaderChunk[ "unpackVector2" ],

			"void main() {",

			DeferredShaderChunk[ "computeTextureCoord" ],
			DeferredShaderChunk[ "unpackNormalDepthShininess" ],
			DeferredShaderChunk[ "computeVertexPositionVS" ],

			"	vec3 lightVector = lightPositionVS - vertexPositionVS.xyz;",
			"	float distance = length( lightVector );",

			"	if ( distance > lightRadius ) discard;",

			"	lightVector = normalize( lightVector );",

			DeferredShaderChunk[ "computeSpecular" ],

			"	float attenuation = saturate( -distance / lightRadius + 1.0 );",

			DeferredShaderChunk[ "packLight" ],

			"	gl_FragColor = packedLight;",

			"}"

		].join( "\n" )

	},

	spotLightPre: {

		uniforms: Object.assign(

			{

				samplerNormalDepthShininess: new Uniform( null ),

				lightColor: new Uniform( new Color( 0x000000 ) ),
				lightDirectionVS: new Uniform( new Vector3( 0, 1, 0 ) ),
				lightPositionVS: new Uniform( new Vector3( 0, 1, 0 ) ),
				lightAngle: new Uniform( 1.0 ),
				lightIntensity: new Uniform( 1.0 )

			},

			ShaderDeferredCommon[ 'commonUniforms' ]

		),

		vertexShader: [

			"void main() { ",

			"	gl_Position = vec4( sign( position.xy ), 0.0, 1.0 );",

			"}"

		].join( "\n" ),

		fragmentShader: [

			"uniform sampler2D samplerNormalDepthShininess;",

			"uniform float viewHeight;",
			"uniform float viewWidth;",

			"uniform vec3 lightColor;",
			"uniform vec3 lightPositionVS;",
			"uniform vec3 lightDirectionVS;",
			"uniform float lightAngle;",
			"uniform float lightIntensity;",

			"uniform mat4 matProjInverse;",

			DeferredShaderChunk[ "unpackFloat" ],
			DeferredShaderChunk[ "unpackVector2" ],

			"void main() {",

			DeferredShaderChunk[ "computeTextureCoord" ],
			DeferredShaderChunk[ "unpackNormalDepthShininess" ],
			DeferredShaderChunk[ "computeVertexPositionVS" ],

			"	vec3 lightVector = normalize( lightPositionVS.xyz - vertexPositionVS.xyz );",

			"	float rho = dot( lightDirectionVS, lightVector );",
			"	float rhoMax = cos( lightAngle );",

			"	if ( rho <= rhoMax ) discard;",

			"	float theta = rhoMax + 0.0001;",
			"	float phi = rhoMax + 0.05;",
			"	float falloff = 4.0;",

			"	float spot = 0.0;",

			"	if ( rho >= phi ) {",

			"		spot = 1.0;",

			"	} else if ( rho <= theta ) {",

			"		spot = 0.0;",

			"	} else { ",

			"		spot = pow( ( rho - theta ) / ( phi - theta ), falloff );",

			"	}",

			DeferredShaderChunk[ "computeSpecular" ],

			"	const float attenuation = 1.0;",

			DeferredShaderChunk[ "packLight" ],

			"	gl_FragColor = spot * packedLight;",

			"}"

		].join( "\n" )

	},

	directionalLightPre: {

		uniforms: Object.assign(

			{

				samplerNormalDepthShininess: new Uniform( null ),

				lightColor: new Uniform( new Color( 0x000000 ) ),
				lightDirectionVS: new Uniform( new Vector3( 0, 1, 0 ) ),
				lightIntensity: new Uniform( 1.0 )

			},

			ShaderDeferredCommon[ 'commonUniforms' ]

		),

		vertexShader: [

			"void main() { ",

			"	gl_Position = vec4( sign( position.xy ), 0.0, 1.0 );",

			"}"

		].join( '\n' ),

		fragmentShader: [

			"uniform sampler2D samplerNormalDepthShininess;",

			"uniform float viewHeight;",
			"uniform float viewWidth;",

			"uniform vec3 lightColor;",
			"uniform vec3 lightDirectionVS;",
			"uniform float lightIntensity;",

			"uniform mat4 matProjInverse;",

			DeferredShaderChunk[ "unpackFloat" ],
			DeferredShaderChunk[ "unpackVector2" ],

			"void main() {",

			DeferredShaderChunk[ "computeTextureCoord" ],
			DeferredShaderChunk[ "unpackNormalDepthShininess" ],
			DeferredShaderChunk[ "computeVertexPositionVS" ],

			"	vec3 lightVector = normalize( lightDirectionVS );",

			DeferredShaderChunk[ "computeSpecular" ],

			"	const float attenuation = 1.0;",

			DeferredShaderChunk[ "packLight" ],

			"	gl_FragColor = packedLight;",

			"}"

		].join( '\n' )

	},

	reconstruction: {

		uniforms: Object.assign(

			{

				samplerLight: new Uniform( null ),

				map: new Uniform( null ),
				offsetRepeat: new Uniform( new Vector4( 0, 0, 1, 1 ) ),

				diffuse: new Uniform( new Color( 0x000000 ) ),
				emissive: new Uniform( new Color( 0x000000 ) ),
				specular: new Uniform( new Color( 0x000000 ) ),
				shininess: new Uniform( 30.0 )

			},

			ShaderDeferredCommon[ 'commonUniforms' ]

		),

		vertexShader: [

			"#include <uv_pars_vertex>",
			"#include <morphtarget_pars_vertex>",
			"#include <skinning_pars_vertex>",

			"void main() {",

			"#include <uv_vertex>",
			"#include <begin_vertex>",
			"#include <beginnormal_vertex>",
			"#include <skinbase_vertex>",
			"#include <skinnormal_vertex>",
			"#include <defaultnormal_vertex>",
			"#include <morphtarget_vertex>",
			"#include <skinning_vertex>",
			"#include <project_vertex>",

			"}"

		].join( "\n" ),

		fragmentShader: [

			"uniform sampler2D samplerLight;",

			"uniform vec3 diffuse;",
			"uniform vec3 emissive;",
			"uniform vec3 specular;",
			"uniform float shininess;",

			"uniform float viewHeight;",
			"uniform float viewWidth;",

			"#include <uv_pars_fragment>",
			"#include <map_pars_fragment>",

			DeferredShaderChunk[ "unpackFloat" ],

			"void main() {",

			"	vec4 diffuseColor = vec4( diffuse, 1.0 );",
			"	vec3 emissiveColor = emissive;",
			"	vec3 specularColor = specular;",

			DeferredShaderChunk[ "computeTextureCoord" ],

			"	vec4 light = texture2D( samplerLight, texCoord );",

			"#include <map_fragment>",

			"	vec3 diffuseFinal = diffuseColor.rgb * light.rgb;",
			"	vec3 emissiveFinal = emissiveColor;",
			"	vec3 specularFinal = specularColor * light.rgb * light.a;",

			"	gl_FragColor = vec4( diffuseFinal + emissiveFinal + specularFinal, 1.0 );",

			"}"

		].join( "\n" )

	},

	// TODO: implement tone mapping
	final: {

		uniforms: {

			samplerResult: new Uniform( null )

		},

		vertexShader: [

			"varying vec2 texCoord;",

			"void main() {",

			"	vec4 pos = vec4( sign( position.xy ), 0.0, 1.0 );",
			"	texCoord = pos.xy * vec2( 0.5 ) + 0.5;",
			"	gl_Position = pos;",

			"}"

		].join( "\n" ),

		fragmentShader: [

			"varying vec2 texCoord;",
			"uniform sampler2D samplerResult;",

			"void main() {",

			"	gl_FragColor = texture2D( samplerResult, texCoord );",

			"}"

		].join( "\n" )

	}

};

export { WebGLDeferredRenderer, DeferredShaderChunk, ShaderDeferredCommon, ShaderDeferred };
