THREE.SAOShader = {
	defines: {
		'NUM_SAMPLES': 7,
		'NUM_RINGS': 4,
		'NORMAL_TEXTURE': 0,
		'DIFFUSE_TEXTURE': 0,
		'DEPTH_PACKING': 1,
		'PERSPECTIVE_CAMERA': 1
	},
	uniforms: {

		'tDepth': { type: 't', value: null },
		'tDiffuse': { type: 't', value: null },
		'tNormal': { type: 't', value: null },
		'size': { type: 'v2', value: new THREE.Vector2( 512, 512 ) },

		'cameraNear': { type: 'f', value: 1 },
		'cameraFar': { type: 'f', value: 100 },
		'cameraProjectionMatrix': { type: 'm4', value: new THREE.Matrix4() },
		'cameraInverseProjectionMatrix': { type: 'm4', value: new THREE.Matrix4() },

		'scale': { type: 'f', value: 1.0 },
		'intensity': { type: 'f', value: 0.1 },
		'bias': { type: 'f', value: 0.5 },

		'minResolution': { type: 'f', value: 0.0 },
		'kernelRadius': { type: 'f', value: 100.0 },
		'randomSeed': { type: 'f', value: 0.0 }
	},
	vertexShader: [
		"varying vec2 vUv;",

		"void main() {",
		"	vUv = uv;",
		"	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
		"}"

	].join( "\n" ),
	fragmentShader: [
		"#include <common>",

		"varying vec2 vUv;",

		"#if DIFFUSE_TEXTURE == 1",
		"uniform sampler2D tDiffuse;",
		"#endif",

		"uniform sampler2D tDepth;",

		"#if NORMAL_TEXTURE == 1",
		"uniform sampler2D tNormal;",
		"#endif",

		"uniform float cameraNear;",
		"uniform float cameraFar;",
		"uniform mat4 cameraProjectionMatrix;",
		"uniform mat4 cameraInverseProjectionMatrix;",

		"uniform float scale;",
		"uniform float intensity;",
		"uniform float bias;",
		"uniform float kernelRadius;",
		"uniform float minResolution;",
		"uniform vec2 size;",
		"uniform float randomSeed;",

		"// RGBA depth",

		"#include <packing>",

		"vec4 getDefaultColor( const in vec2 screenPosition ) {",
		"	#if DIFFUSE_TEXTURE == 1",
		"	return texture2D( tDiffuse, vUv );",
		"	#else",
		"	return vec4( 1.0 );",
		"	#endif",
		"}",

		"float getDepth( const in vec2 screenPosition ) {",
		"	#if DEPTH_PACKING == 1",
		"	return unpackRGBAToDepth( texture2D( tDepth, screenPosition ) );",
		"	#else",
		"	return texture2D( tDepth, screenPosition ).x;",
		"	#endif",
		"}",

		"float getViewZ( const in float depth ) {",
		"	#if PERSPECTIVE_CAMERA == 1",
		"	return perspectiveDepthToViewZ( depth, cameraNear, cameraFar );",
		"	#else",
		"	return orthographicDepthToViewZ( depth, cameraNear, cameraFar );",
		"	#endif",
		"}",

		"vec3 getViewPosition( const in vec2 screenPosition, const in float depth, const in float viewZ ) {",
		"	float clipW = cameraProjectionMatrix[2][3] * viewZ + cameraProjectionMatrix[3][3];",
		"	vec4 clipPosition = vec4( ( vec3( screenPosition, depth ) - 0.5 ) * 2.0, 1.0 );",
		"	clipPosition *= clipW; // unprojection.",

		"	return ( cameraInverseProjectionMatrix * clipPosition ).xyz;",
		"}",

		"vec3 getViewNormal( const in vec3 viewPosition, const in vec2 screenPosition ) {",
		"	#if NORMAL_TEXTURE == 1",
		"	return unpackRGBToNormal( texture2D( tNormal, screenPosition ).xyz );",
		"	#else",
		"	return normalize( cross( dFdx( viewPosition ), dFdy( viewPosition ) ) );",
		"	#endif",
		"}",

		"float scaleDividedByCameraFar;",
		"float minResolutionMultipliedByCameraFar;",

		"float getOcclusion( const in vec3 centerViewPosition, const in vec3 centerViewNormal, const in vec3 sampleViewPosition ) {",
		"	vec3 viewDelta = sampleViewPosition - centerViewPosition;",
		"	float viewDistance = length( viewDelta );",
		"	float scaledScreenDistance = scaleDividedByCameraFar * viewDistance;",

		"	return max(0.0, (dot(centerViewNormal, viewDelta) - minResolutionMultipliedByCameraFar) / scaledScreenDistance - bias) / (1.0 + pow2( scaledScreenDistance ) );",
		"}",

		"// moving costly divides into consts",
		"const float ANGLE_STEP = PI2 * float( NUM_RINGS ) / float( NUM_SAMPLES );",
		"const float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );",

		"float getAmbientOcclusion( const in vec3 centerViewPosition ) {",
		"	// precompute some variables require in getOcclusion.",
		"	scaleDividedByCameraFar = scale / cameraFar;",
		"	minResolutionMultipliedByCameraFar = minResolution * cameraFar;",
		"	vec3 centerViewNormal = getViewNormal( centerViewPosition, vUv );",

		"	// jsfiddle that shows sample pattern: https://jsfiddle.net/a16ff1p7/",
		"	float angle = rand( vUv + randomSeed ) * PI2;",
		"	vec2 radius = vec2( kernelRadius * INV_NUM_SAMPLES ) / size;",
		"	vec2 radiusStep = radius;",

		"	float occlusionSum = 0.0;",
		"	float weightSum = 0.0;",

		"	for( int i = 0; i < NUM_SAMPLES; i ++ ) {",
		"		vec2 sampleUv = vUv + vec2( cos( angle ), sin( angle ) ) * radius;",
		"		radius += radiusStep;",
		"		angle += ANGLE_STEP;",

		"		float sampleDepth = getDepth( sampleUv );",
		"		if( sampleDepth >= ( 1.0 - EPSILON ) ) {",
		"			continue;",
		"		}",

		"		float sampleViewZ = getViewZ( sampleDepth );",
		"		vec3 sampleViewPosition = getViewPosition( sampleUv, sampleDepth, sampleViewZ );",
		"		occlusionSum += getOcclusion( centerViewPosition, centerViewNormal, sampleViewPosition );",
		"		weightSum += 1.0;",
		"	}",

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

		"	return occlusionSum * ( intensity / weightSum );",
		"}",


		"void main() {",
		"	float centerDepth = getDepth( vUv );",
		"	if( centerDepth >= ( 1.0 - EPSILON ) ) {",
		"		discard;",
		"	}",

		"	float centerViewZ = getViewZ( centerDepth );",
		"	vec3 viewPosition = getViewPosition( vUv, centerDepth, centerViewZ );",

		"	float ambientOcclusion = getAmbientOcclusion( viewPosition );",

		"	gl_FragColor = getDefaultColor( vUv );",
		"	gl_FragColor.xyz *=  1.0 - ambientOcclusion;",
		"}"
	].join( "\n" )
};
