Prechádzať zdrojové kódy

Merge pull request #11458 from Ludobaka/sao

Scalable Ambient Occlusion (SAO) post processing effect (3rd Try)
Mr.doob 8 rokov pred
rodič
commit
1bab8d04ba

+ 1 - 0
examples/files.js

@@ -224,6 +224,7 @@ var files = {
 		"webgl_postprocessing_procedural",
 		"webgl_postprocessing_smaa",
 		"webgl_postprocessing_ssao",
+		"webgl_postprocessing_sao",
 		"webgl_postprocessing_taa",
 		"webgl_postprocessing_unreal_bloom",
 		"webgl_raycast_texture",

+ 380 - 0
examples/js/postprocessing/SAOPass.js

@@ -0,0 +1,380 @@
+/**
+ * @author ludobaka / ludobaka.github.io
+ * SAO implementation inspired from bhouston previous SAO work
+ */
+
+THREE.SAOPass = function ( scene, camera, depthTexture, useNormals, resolution ) {
+
+	THREE.Pass.call( this );
+
+	this.scene = scene;
+	this.camera = camera;
+
+	this.clear = true;
+	this.needsSwap = false;
+
+	this.supportsDepthTextureExtension = ( depthTexture !== undefined ) ? depthTexture : false;
+	this.supportsNormalTexture = ( useNormals !== undefined ) ? useNormals : false;
+
+	this.oldClearColor = new THREE.Color();
+	this.oldClearAlpha = 1;
+
+	this.params = {
+		output: 0,
+		saoBias: 0.5,
+		saoIntensity: 0.18,
+		saoScale: 1,
+		saoKernelRadius: 100,
+		saoMinResolution: 0,
+		saoBlur: true,
+		saoBlurRadius: 8,
+		saoBlurStdDev: 4,
+		saoBlurDepthCutoff: 0.01
+	};
+
+	this.resolution = ( resolution !== undefined ) ? new THREE.Vector2( resolution.x, resolution.y ) : new THREE.Vector2( 256, 256 );
+
+	this.saoRenderTarget = new THREE.WebGLRenderTarget( this.resolution.x, this.resolution.y, {
+		minFilter: THREE.LinearFilter,
+		magFilter: THREE.LinearFilter,
+		format: THREE.RGBAFormat
+	} );
+	this.blurIntermediateRenderTarget = this.saoRenderTarget.clone();
+	this.beautyRenderTarget = this.saoRenderTarget.clone();
+
+	this.normalRenderTarget = new THREE.WebGLRenderTarget( this.resolution.x, this.resolution.y, {
+		minFilter: THREE.NearestFilter,
+		magFilter: THREE.NearestFilter,
+		format: THREE.RGBAFormat
+	} );
+	this.depthRenderTarget = this.normalRenderTarget.clone();
+
+	if ( this.supportsDepthTextureExtension ) {
+
+		var depthTexture = new THREE.DepthTexture();
+		depthTexture.type = THREE.UnsignedShortType;
+		depthTexture.minFilter = THREE.NearestFilter;
+		depthTexture.maxFilter = THREE.NearestFilter;
+
+		this.beautyRenderTarget.depthTexture = depthTexture;
+		this.beautyRenderTarget.depthBuffer = true;
+
+	}
+
+	this.depthMaterial = new THREE.MeshDepthMaterial();
+	this.depthMaterial.depthPacking = THREE.RGBADepthPacking;
+	this.depthMaterial.blending = THREE.NoBlending;
+
+	this.normalMaterial = new THREE.MeshNormalMaterial();
+	this.normalMaterial.blending = THREE.NoBlending;
+
+	if ( THREE.SAOShader === undefined ) {
+
+		console.error( 'THREE.SAOPass relies on THREE.SAOShader' );
+
+	}
+
+	this.saoMaterial = new THREE.ShaderMaterial( THREE.SAOShader );
+	this.saoMaterial.extensions.derivatives = true;
+	this.saoMaterial.extensions.drawBuffers = true;
+	this.saoMaterial.defines[ 'DEPTH_PACKING' ] = this.supportsDepthTextureExtension ? 0 : 1;
+	this.saoMaterial.defines[ 'NORMAL_TEXTURE' ] = this.supportsNormalTexture ? 1 : 0;
+	this.saoMaterial.uniforms[ 'tDepth' ].value = ( this.supportsDepthTextureExtension ) ? depthTexture : this.depthRenderTarget.texture;
+	this.saoMaterial.uniforms[ 'tNormal' ].value = this.normalRenderTarget.texture;
+	this.saoMaterial.uniforms[ 'size' ].value.set( this.resolution.x, this.resolution.y );
+	this.saoMaterial.uniforms[ 'cameraNear' ].value = this.camera.near;
+	this.saoMaterial.uniforms[ 'cameraFar' ].value = this.camera.far;
+	this.saoMaterial.uniforms[ 'cameraInverseProjectionMatrix' ].value.getInverse( this.camera.projectionMatrix );
+	this.saoMaterial.uniforms[ 'cameraProjectionMatrix' ].value = this.camera.projectionMatrix;
+	this.saoMaterial.blending = THREE.NoBlending;
+
+	if ( THREE.DepthLimitedBlurShader === undefined ) {
+
+		console.error( 'THREE.SAOPass relies on THREE.DepthLimitedBlurShader' );
+
+	}
+
+	this.vBlurMaterial = new THREE.ShaderMaterial( {
+		uniforms: THREE.UniformsUtils.clone( THREE.DepthLimitedBlurShader.uniforms ),
+		defines: THREE.DepthLimitedBlurShader.defines,
+		vertexShader: THREE.DepthLimitedBlurShader.vertexShader,
+		fragmentShader: THREE.DepthLimitedBlurShader.fragmentShader
+	} );
+	this.vBlurMaterial.defines[ 'DEPTH_PACKING' ] = this.supportsDepthTextureExtension ? 0 : 1;
+	this.vBlurMaterial.uniforms[ 'tDiffuse' ].value = this.saoRenderTarget.texture;
+	this.vBlurMaterial.uniforms[ 'tDepth' ].value = ( this.supportsDepthTextureExtension ) ? depthTexture : this.depthRenderTarget.texture;
+	this.vBlurMaterial.uniforms[ 'cameraNear' ].value = this.camera.near;
+	this.vBlurMaterial.uniforms[ 'cameraFar' ].value = this.camera.far;
+	this.vBlurMaterial.uniforms[ 'size' ].value.set( this.resolution.x, this.resolution.y );
+	this.vBlurMaterial.blending = THREE.NoBlending;
+
+	this.hBlurMaterial = new THREE.ShaderMaterial( {
+		uniforms: THREE.UniformsUtils.clone( THREE.DepthLimitedBlurShader.uniforms ),
+		defines: THREE.DepthLimitedBlurShader.defines,
+		vertexShader: THREE.DepthLimitedBlurShader.vertexShader,
+		fragmentShader: THREE.DepthLimitedBlurShader.fragmentShader
+	} );
+	this.hBlurMaterial.defines[ 'DEPTH_PACKING' ] = this.supportsDepthTextureExtension ? 0 : 1;
+	this.hBlurMaterial.uniforms[ 'tDiffuse' ].value = this.blurIntermediateRenderTarget.texture;
+	this.hBlurMaterial.uniforms[ 'tDepth' ].value = ( this.supportsDepthTextureExtension ) ? depthTexture : this.depthRenderTarget.texture;
+	this.hBlurMaterial.uniforms[ 'cameraNear' ].value = this.camera.near;
+	this.hBlurMaterial.uniforms[ 'cameraFar' ].value = this.camera.far;
+	this.hBlurMaterial.uniforms[ 'size' ].value.set( this.resolution.x, this.resolution.y );
+	this.hBlurMaterial.blending = THREE.NoBlending;
+
+	if ( THREE.CopyShader === undefined ) {
+
+		console.error( 'THREE.SAOPass relies on THREE.CopyShader' );
+
+	}
+
+	this.materialCopy = new THREE.ShaderMaterial( {
+		uniforms: THREE.UniformsUtils.clone( THREE.CopyShader.uniforms ),
+		vertexShader: THREE.CopyShader.vertexShader,
+		fragmentShader: THREE.CopyShader.fragmentShader,
+		blending: THREE.NoBlending
+	} );
+	this.materialCopy.transparent = true;
+	this.materialCopy.depthTest = false;
+	this.materialCopy.depthWrite = false;
+	this.materialCopy.blending = THREE.CustomBlending;
+	this.materialCopy.blendSrc = THREE.DstColorFactor;
+	this.materialCopy.blendDst = THREE.ZeroFactor;
+	this.materialCopy.blendEquation = THREE.AddEquation;
+	this.materialCopy.blendSrcAlpha = THREE.DstAlphaFactor;
+	this.materialCopy.blendDstAlpha = THREE.ZeroFactor;
+	this.materialCopy.blendEquationAlpha = THREE.AddEquation;
+
+	if ( THREE.CopyShader === undefined ) {
+
+		console.error( 'THREE.SAOPass relies on THREE.UnpackDepthRGBAShader' );
+
+	}
+
+	this.depthCopy = new THREE.ShaderMaterial( {
+		uniforms: THREE.UniformsUtils.clone( THREE.UnpackDepthRGBAShader.uniforms ),
+		vertexShader: THREE.UnpackDepthRGBAShader.vertexShader,
+		fragmentShader: THREE.UnpackDepthRGBAShader.fragmentShader,
+		blending: THREE.NoBlending
+	} );
+
+	this.quadCamera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+	this.quadScene = new THREE.Scene();
+	this.quad = new THREE.Mesh( new THREE.PlaneGeometry( 2, 2 ), null );
+	this.quadScene.add( this.quad );
+
+};
+
+THREE.SAOPass.OUTPUT = {
+	'Beauty': 1,
+	'Default': 0,
+	'SAO': 2,
+	'Depth': 3,
+	'Normal': 4
+};
+
+THREE.SAOPass.prototype = Object.assign( Object.create( THREE.Pass.prototype ), {
+	constructor: THREE.SAOPass,
+
+	render: function ( renderer, writeBuffer, readBuffer, delta, maskActive ) {
+
+		// Rendering readBuffer first when rendering to screen
+		if ( this.renderToScreen ) {
+
+			this.materialCopy.blending = THREE.NoBlending;
+			this.materialCopy.uniforms[ 'tDiffuse' ].value = readBuffer.texture;
+			this.materialCopy.needsUpdate = true;
+			this.renderPass( renderer, this.materialCopy, null );
+
+		}
+
+		if ( this.params.output == 1 ) {
+
+			return;
+
+		}
+
+		this.oldClearColor.copy( renderer.getClearColor() );
+		this.oldClearAlpha = renderer.getClearAlpha();
+		var oldAutoClear = renderer.autoClear;
+		renderer.autoClear = false;
+
+		renderer.clearTarget( this.depthRenderTarget );
+
+		this.saoMaterial.uniforms[ 'bias' ].value = this.params.saoBias;
+		this.saoMaterial.uniforms[ 'intensity' ].value = this.params.saoIntensity;
+		this.saoMaterial.uniforms[ 'scale' ].value = this.params.saoScale;
+		this.saoMaterial.uniforms[ 'kernelRadius' ].value = this.params.saoKernelRadius;
+		this.saoMaterial.uniforms[ 'minResolution' ].value = this.params.saoMinResolution;
+		// this.saoMaterial.uniforms['randomSeed'].value = Math.random();
+
+		var depthCutoff = this.params.saoBlurDepthCutoff * ( this.camera.far - this.camera.near );
+		this.vBlurMaterial.uniforms[ 'depthCutoff' ].value = depthCutoff;
+		this.hBlurMaterial.uniforms[ 'depthCutoff' ].value = depthCutoff;
+
+		this.params.saoBlurRadius = Math.floor( this.params.saoBlurRadius );
+		if ( ( this.prevStdDev !== this.params.saoBlurStdDev ) || ( this.prevNumSamples !== this.params.saoBlurRadius ) ) {
+
+			THREE.BlurShaderUtils.configure( this.vBlurMaterial, this.params.saoBlurRadius, this.params.saoBlurStdDev, new THREE.Vector2( 0, 1 ) );
+			THREE.BlurShaderUtils.configure( this.hBlurMaterial, this.params.saoBlurRadius, this.params.saoBlurStdDev, new THREE.Vector2( 1, 0 ) );
+			this.prevStdDev = this.params.saoBlurStdDev;
+			this.prevNumSamples = this.params.saoBlurRadius;
+
+		}
+
+		// Rendering scene to depth texture
+		renderer.setClearColor( 0x000000 );
+		renderer.render( this.scene, this.camera, this.beautyRenderTarget, true );
+
+		// Re-render scene if depth texture extension is not supported
+		if ( ! this.supportsDepthTextureExtension ) {
+
+			// Clear rule : far clipping plane in both RGBA and Basic encoding
+			this.renderOverride( renderer, this.depthMaterial, this.depthRenderTarget, 0xffffff, 1.0 );
+
+		}
+
+		if ( this.supportsNormalTexture ) {
+
+			// Clear rule : default normal is facing the camera
+			this.renderOverride( renderer, this.normalMaterial, this.normalRenderTarget, 0x7777ff, 1.0 );
+
+		}
+
+		// Rendering SAO texture
+		this.renderPass( renderer, this.saoMaterial, this.saoRenderTarget, 0xffffff, 1.0 );
+
+		// Blurring SAO texture
+		if ( this.params.saoBlur ) {
+
+			this.renderPass( renderer, this.vBlurMaterial, this.blurIntermediateRenderTarget, 0xffffff, 1.0 );
+			this.renderPass( renderer, this.hBlurMaterial, this.saoRenderTarget, 0xffffff, 1.0 );
+
+		}
+
+		var outputMaterial = this.materialCopy;
+		// Setting up SAO rendering
+		if ( this.params.output == 3 ) {
+
+			if ( this.supportsDepthTextureExtension ) {
+
+				this.materialCopy.uniforms[ 'tDiffuse' ].value = this.beautyRenderTarget.depthTexture;
+				this.materialCopy.needsUpdate = true;
+
+			} else {
+
+				this.depthCopy.uniforms[ 'tDiffuse' ].value = this.depthRenderTarget.texture;
+				this.depthCopy.needsUpdate = true;
+				outputMaterial = this.depthCopy;
+
+			}
+
+		} else if ( this.params.output == 4 ) {
+
+			this.materialCopy.uniforms[ 'tDiffuse' ].value = this.normalRenderTarget.texture;
+			this.materialCopy.needsUpdate = true;
+
+		} else {
+
+			this.materialCopy.uniforms[ 'tDiffuse' ].value = this.saoRenderTarget.texture;
+			this.materialCopy.needsUpdate = true;
+
+		}
+
+		// Blending depends on output, only want a CustomBlending when showing SAO
+		if ( this.params.output == 0 ) {
+
+			outputMaterial.blending = THREE.CustomBlending;
+
+		} else {
+
+			outputMaterial.blending = THREE.NoBlending;
+
+		}
+
+		// Rendering SAOPass result on top of previous pass
+		this.renderPass( renderer, outputMaterial, this.renderToScreen ? null : readBuffer );
+
+		renderer.setClearColor( this.oldClearColor, this.oldClearAlpha );
+		renderer.autoClear = oldAutoClear;
+
+	},
+
+	renderPass: function ( renderer, passMaterial, renderTarget, clearColor, clearAlpha ) {
+
+		// save original state
+		var originalClearColor = renderer.getClearColor();
+		var originalClearAlpha = renderer.getClearAlpha();
+		var originalAutoClear = renderer.autoClear;
+
+		// setup pass state
+		renderer.autoClear = false;
+		var clearNeeded = ( clearColor !== undefined ) && ( clearColor !== null );
+		if ( clearNeeded ) {
+
+			renderer.setClearColor( clearColor );
+			renderer.setClearAlpha( clearAlpha || 0.0 );
+
+		}
+
+		this.quad.material = passMaterial;
+		renderer.render( this.quadScene, this.quadCamera, renderTarget, clearNeeded );
+
+		// restore original state
+		renderer.autoClear = originalAutoClear;
+		renderer.setClearColor( originalClearColor );
+		renderer.setClearAlpha( originalClearAlpha );
+
+	},
+
+	renderOverride: function ( renderer, overrideMaterial, renderTarget, clearColor, clearAlpha ) {
+
+		var originalClearColor = renderer.getClearColor();
+		var originalClearAlpha = renderer.getClearAlpha();
+		var originalAutoClear = renderer.autoClear;
+
+		renderer.autoClear = false;
+
+		clearColor = overrideMaterial.clearColor || clearColor;
+		clearAlpha = overrideMaterial.clearAlpha || clearAlpha;
+		var clearNeeded = ( clearColor !== undefined ) && ( clearColor !== null );
+		if ( clearNeeded ) {
+
+			renderer.setClearColor( clearColor );
+			renderer.setClearAlpha( clearAlpha || 0.0 );
+
+		}
+
+		this.scene.overrideMaterial = overrideMaterial;
+		renderer.render( this.scene, this.camera, renderTarget, clearNeeded );
+		this.scene.overrideMaterial = null;
+
+		// restore original state
+		renderer.autoClear = originalAutoClear;
+		renderer.setClearColor( originalClearColor );
+		renderer.setClearAlpha( originalClearAlpha );
+
+	},
+
+	setSize: function ( width, height ) {
+
+		this.beautyRenderTarget.setSize( width, height );
+		this.saoRenderTarget.setSize( width, height );
+		this.blurIntermediateRenderTarget.setSize( width, height );
+		this.normalRenderTarget.setSize( width, height );
+		this.depthRenderTarget.setSize( width, height );
+
+		this.saoMaterial.uniforms[ 'size' ].value.set( width, height );
+		this.saoMaterial.uniforms[ 'cameraInverseProjectionMatrix' ].value.getInverse( this.camera.projectionMatrix );
+		this.saoMaterial.uniforms[ 'cameraProjectionMatrix' ].value = this.camera.projectionMatrix;
+		this.saoMaterial.needsUpdate = true;
+
+		this.vBlurMaterial.uniforms[ 'size' ].value.set( width, height );
+		this.vBlurMaterial.needsUpdate = true;
+
+		this.hBlurMaterial.uniforms[ 'size' ].value.set( width, height );
+		this.hBlurMaterial.needsUpdate = true;
+
+	}
+
+} );

+ 155 - 0
examples/js/shaders/DepthLimitedBlurShader.js

@@ -0,0 +1,155 @@
+THREE.DepthLimitedBlurShader = {
+	defines: {
+		'KERNEL_RADIUS': 4,
+		'DEPTH_PACKING': 1,
+		'PERSPECTIVE_CAMERA': 1
+	},
+	uniforms: {
+		'tDiffuse': { type: 't', value: null },
+		'size': { type: 'v2', value: new THREE.Vector2( 512, 512 ) },
+		'sampleUvOffsets': { type: 'v2v', value: [ new THREE.Vector2( 0, 0 ) ] },
+		'sampleWeights': { type: '1fv', value: [ 1.0 ] },
+		'tDepth': { type: 't', value: null },
+		'cameraNear': { type: 'f', value: 10 },
+		'cameraFar': { type: 'f', value: 1000 },
+		'depthCutoff': { type: 'f', value: 10 },
+	},
+	vertexShader: [
+		"#include <common>",
+
+		"uniform vec2 size;",
+
+		"varying vec2 vUv;",
+		"varying vec2 vInvSize;",
+
+		"void main() {",
+		"	vUv = uv;",
+		"	vInvSize = 1.0 / size;",
+
+		"	gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
+		"}"
+
+	].join( "\n" ),
+	fragmentShader: [
+		"#include <common>",
+		"#include <packing>",
+
+		"uniform sampler2D tDiffuse;",
+		"uniform sampler2D tDepth;",
+
+		"uniform float cameraNear;",
+		"uniform float cameraFar;",
+		"uniform float depthCutoff;",
+
+		"uniform vec2 sampleUvOffsets[ KERNEL_RADIUS + 1 ];",
+		"uniform float sampleWeights[ KERNEL_RADIUS + 1 ];",
+
+		"varying vec2 vUv;",
+		"varying vec2 vInvSize;",
+
+		"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 orthoDepthToViewZ( depth, cameraNear, cameraFar );",
+		"	#endif",
+		"}",
+
+		"void main() {",
+		"	float depth = getDepth( vUv );",
+		"	if( depth >= ( 1.0 - EPSILON ) ) {",
+		"		discard;",
+		"	}",
+
+		"	float centerViewZ = -getViewZ( depth );",
+		"	bool rBreak = false, lBreak = false;",
+
+		"	float weightSum = sampleWeights[0];",
+		"	vec4 diffuseSum = texture2D( tDiffuse, vUv ) * weightSum;",
+
+		"	for( int i = 1; i <= KERNEL_RADIUS; i ++ ) {",
+
+		"		float sampleWeight = sampleWeights[i];",
+		"		vec2 sampleUvOffset = sampleUvOffsets[i] * vInvSize;",
+
+		"		vec2 sampleUv = vUv + sampleUvOffset;",
+		"		float viewZ = -getViewZ( getDepth( sampleUv ) );",
+
+		"		if( abs( viewZ - centerViewZ ) > depthCutoff ) rBreak = true;",
+
+		"		if( ! rBreak ) {",
+		"			diffuseSum += texture2D( tDiffuse, sampleUv ) * sampleWeight;",
+		"			weightSum += sampleWeight;",
+		"		}",
+
+		"		sampleUv = vUv - sampleUvOffset;",
+		"		viewZ = -getViewZ( getDepth( sampleUv ) );",
+
+		"		if( abs( viewZ - centerViewZ ) > depthCutoff ) lBreak = true;",
+
+		"		if( ! lBreak ) {",
+		"			diffuseSum += texture2D( tDiffuse, sampleUv ) * sampleWeight;",
+		"			weightSum += sampleWeight;",
+		"		}",
+
+		"	}",
+
+		"	gl_FragColor = diffuseSum / weightSum;",
+		"}"
+	].join( "\n" )
+};
+
+THREE.BlurShaderUtils = {
+
+	createSampleWeights: function ( kernelRadius, stdDev ) {
+
+		var gaussian = function ( x, stdDev ) {
+
+			return Math.exp( - ( x * x ) / ( 2.0 * ( stdDev * stdDev ) ) ) / ( Math.sqrt( 2.0 * Math.PI ) * stdDev );
+
+		};
+
+		var weights = [];
+
+		for ( var i = 0; i <= kernelRadius; i ++ ) {
+
+			weights.push( gaussian( i, stdDev ) );
+
+		}
+
+		return weights;
+
+	},
+
+	createSampleOffsets: function ( kernelRadius, uvIncrement ) {
+
+		var offsets = [];
+
+		for ( var i = 0; i <= kernelRadius; i ++ ) {
+
+			offsets.push( uvIncrement.clone().multiplyScalar( i ) );
+
+		}
+
+		return offsets;
+
+	},
+
+	configure: function ( material, kernelRadius, stdDev, uvIncrement ) {
+
+		material.defines[ 'KERNEL_RADIUS' ] = kernelRadius;
+		material.uniforms[ 'sampleUvOffsets' ].value = THREE.BlurShaderUtils.createSampleOffsets( kernelRadius, uvIncrement );
+		material.uniforms[ 'sampleWeights' ].value = THREE.BlurShaderUtils.createSampleWeights( kernelRadius, stdDev );
+		material.needsUpdate = true;
+
+	}
+
+};

+ 177 - 0
examples/js/shaders/SAOShader.js

@@ -0,0 +1,177 @@
+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 orthoDepthToViewZ( 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" )
+};

+ 215 - 0
examples/webgl_postprocessing_sao.html

@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+
+<html lang="en">
+	<head>
+		<title>three.js webgl - post processing - Scalable Ambient Occlusion (SAO)</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<style>
+			body {
+				background-color: #000000;
+				margin: 0px;
+				overflow: hidden;
+				font-family:Monospace;
+				font-size:13px;
+				text-align:center;
+				font-weight: bold;
+			}
+
+			a {
+				color:#00ff78;
+			}
+
+			#info {
+				color: #fff;
+				position: absolute;
+				top: 0px;
+				width: 100%;
+				padding: 5px;
+			}
+			.dg.ac {
+				z-index: 1 !important; /* FIX DAT.GUI */
+			}
+		</style>
+	</head>
+	<body>
+		<script src="../build/three.js"></script>
+
+        <script src="js/postprocessing/EffectComposer.js"></script>
+		<script src="js/postprocessing/RenderPass.js"></script>
+		<script src="js/postprocessing/ShaderPass.js"></script>
+		<script src="js/postprocessing/SAOPass.js"></script>
+
+		<script src="js/shaders/CopyShader.js"></script>
+		<script src="js/shaders/SAOShader.js"></script>
+        <script src="js/shaders/DepthLimitedBlurShader.js"></script>
+        <script src="js/shaders/UnpackDepthRGBAShader.js"></script>
+
+		<script src="js/Detector.js"></script>
+		<script src="js/libs/stats.min.js"></script>
+		<script src='js/libs/dat.gui.min.js'></script>
+
+		<div id="info">
+			<a href="http://threejs.org" target="_blank">three.js</a> - Scalable Ambient Occlusion (SAO) shader by <a href="http://clara.io">Ben Houston</a> / Post-processing pass by <a href="http://ludobaka.github.io">Ludobaka</a>
+		</div>
+
+		<script>
+
+			if ( ! Detector.webgl ) Detector.addGetWebGLMessage();
+
+			var container, stats;
+			var camera, scene, renderer;
+			var depthMaterial, saoMaterial, saoModulateMaterial, normalMaterial, vBlurMaterial, hBlurMaterial, copyMaterial;
+			var depthRenderTarget, normalRenderTarget, saoRenderTarget, beautyRenderTarget, blurIntermediateRenderTarget;
+            var composer, renderPass, saoPass, copyPass;
+			var group;
+			var params = {
+				output: 0,
+				saoBias: 0.5,
+				saoIntensity: 0.25,
+				saoScale: 1,
+				saoKernelRadius: 100,
+				saoMinResolution: 0,
+				saoBlur: true,
+				saoBlurRadius: 12,
+				saoBlurStdDev: 6,
+				saoBlurDepthCutoff: 0.01
+			}
+			var supportsDepthTextureExtension = false;
+			var isWebGL2 = false;
+
+			init();
+			animate();
+
+			function init() {
+
+				container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				var width = window.innerWidth || 1;
+				var height = window.innerHeight || 1;
+				var devicePixelRatio = window.devicePixelRatio || 1;
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setClearColor( 0xa0a0a0 );
+				renderer.setPixelRatio( devicePixelRatio );
+				renderer.setSize( width, height );
+				document.body.appendChild( renderer.domElement );
+
+				camera = new THREE.PerspectiveCamera( 65, width / height, 3, 10 );
+				camera.position.z = 7;
+
+				scene = new THREE.Scene();
+
+				group = new THREE.Object3D();
+				scene.add( group );
+
+				var light = new THREE.PointLight( 0xddffdd, 0.8 );
+				light.position.z = 70;
+				light.position.y = -70;
+				light.position.x = -70;
+				scene.add( light );
+
+				var light2 = new THREE.PointLight( 0xffdddd, 0.8 );
+				light2.position.z = 70;
+				light2.position.x = -70;
+				light2.position.y = 70;
+				scene.add( light2 );
+
+				var light3 = new THREE.PointLight( 0xddddff, 0.8 );
+				light3.position.z = 70;
+				light3.position.x = 70;
+				light3.position.y = -70;
+				scene.add( light3 );
+
+				var light3 = new THREE.AmbientLight( 0xffffff, 0.05 );
+				scene.add( light3 );
+
+				var geometry = new THREE.SphereBufferGeometry( 3, 48, 24 );
+				for ( var i = 0; i < 120; i ++ ) {
+
+					var material = new THREE.MeshStandardMaterial();
+					material.roughness = 0.5 * Math.random() + 0.25;
+					material.metalness = 0;
+					material.color.setHSL( Math.random(), 1.0, 0.3 );
+
+					var mesh = new THREE.Mesh( geometry, material );
+					mesh.position.x = Math.random() * 4 - 2;
+					mesh.position.y = Math.random() * 4 - 2;
+					mesh.position.z = Math.random() * 4 - 2;
+					mesh.rotation.x = Math.random();
+					mesh.rotation.y = Math.random();
+					mesh.rotation.z = Math.random();
+
+					mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 0.2 + 0.05;
+					group.add( mesh );
+				}
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				composer = new THREE.EffectComposer(renderer);
+				renderPass = new THREE.RenderPass(scene, camera);
+				composer.addPass(renderPass);
+				saoPass = new THREE.SAOPass(scene, camera, false, true);
+				saoPass.renderToScreen = true;
+				composer.addPass(saoPass);
+
+				// Init gui
+				var gui = new dat.GUI();
+				gui.add( saoPass.params, "output", {
+					'Beauty': THREE.SAOPass.OUTPUT.Beauty,
+					'Beauty+SAO': THREE.SAOPass.OUTPUT.Default,
+					'SAO': THREE.SAOPass.OUTPUT.SAO,
+					'Depth': THREE.SAOPass.OUTPUT.Depth,
+					'Normal': THREE.SAOPass.OUTPUT.Normal
+				});
+				gui.add( saoPass.params, "saoBias", -1, 1 );
+				gui.add( saoPass.params, "saoIntensity", 0, 1 );
+				gui.add( saoPass.params, "saoScale", 0, 10 );
+				gui.add( saoPass.params, "saoKernelRadius", 1, 100 );
+				gui.add( saoPass.params, "saoMinResolution", 0, 1 );
+				gui.add( saoPass.params, "saoBlur" );
+				gui.add( saoPass.params, "saoBlurRadius", 0, 200 );
+				gui.add( saoPass.params, "saoBlurStdDev", 0.5, 150 );
+				gui.add( saoPass.params, "saoBlurDepthCutoff", 0.0, 0.1 );
+
+				window.addEventListener( 'resize', onWindowResize, false );
+			}
+
+			function onWindowResize() {
+
+				var width = window.innerWidth || 1;
+				var height = window.innerHeight || 1;
+				var devicePixelRatio = window.devicePixelRatio || 1;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+				renderer.setSize( width, height );
+
+                composer.setSize( width, height );
+
+
+			}
+
+			function animate() {
+				requestAnimationFrame( animate );
+
+				stats.begin();
+				render();
+				stats.end();
+			}
+
+			var prevStdDev, prevNumSamples;
+
+			function render() {
+				var timer = performance.now();
+				group.rotation.x = timer * 0.0002;
+				group.rotation.y = timer * 0.0001;
+
+				composer.render();
+			}
+
+		</script>
+	</body>
+</html>