Explorar el Código

add unbiased mode to the MSAA render pass. (#8930)

* add unbiased mode to the MSAA render pass.

* clearer code.

* better comment.

* cleanup code.  fix bug of using "camera", instead of "this.camera"

* fix more places were "camera" is used instad of "this.camera" -- it worked because example declared "camera" as a global.

* reduce light intensity.

* add PerspectiveCamera.clearViewOffset() per @mrdoob's recommendation.
Ben Houston (Clara.io) hace 9 años
padre
commit
bb5600fd89

+ 1 - 0
examples/files.js

@@ -189,6 +189,7 @@ var files = {
 		"webgl_postprocessing_godrays",
 		"webgl_postprocessing_masking",
 		"webgl_postprocessing_msaa",
+		"webgl_postprocessing_msaa_unbiased",
 		"webgl_postprocessing_nodes",
 		"webgl_postprocessing_procedural",
 		"webgl_postprocessing_smaa",

+ 20 - 7
examples/js/postprocessing/ManualMSAARenderPass.js

@@ -18,6 +18,7 @@ THREE.ManualMSAARenderPass = function ( scene, camera ) {
 	this.camera = camera;
 
 	this.sampleLevel = 4; // specified as n, where the number of samples is 2^n, so sampleLevel = 4, is 2^4 samples, 16.
+	this.unbiased = true;
 
 	if ( THREE.CopyShader === undefined ) console.error( "THREE.ManualMSAARenderPass relies on THREE.CopyShader" );
 
@@ -77,27 +78,39 @@ Object.assign( THREE.ManualMSAARenderPass.prototype, {
 		var autoClear = renderer.autoClear;
 		renderer.autoClear = false;
 
-		this.copyUniforms[ "opacity" ].value = 1.0 / jitterOffsets.length;
+		var baseSampleWeight = 1.0 / jitterOffsets.length;
+		var roundingRange = 1 / 32;
 		this.copyUniforms[ "tDiffuse" ].value = this.sampleRenderTarget.texture;
 
+		var width = readBuffer.width, height = readBuffer.height;
+
 		// render the scene multiple times, each slightly jitter offset from the last and accumulate the results.
 		for ( var i = 0; i < jitterOffsets.length; i ++ ) {
 
-			// only jitters perspective cameras.	TODO: add support for jittering orthogonal cameras
 			var jitterOffset = jitterOffsets[i];
-			if ( camera.setViewOffset ) {
-				camera.setViewOffset( readBuffer.width, readBuffer.height,
+			if ( this.camera.setViewOffset ) {
+				this.camera.setViewOffset( width, height,
 					jitterOffset[ 0 ] * 0.0625, jitterOffset[ 1 ] * 0.0625,   // 0.0625 = 1 / 16
-					readBuffer.width, readBuffer.height );
+					width, height );
+			}
+
+			var sampleWeight = baseSampleWeight;
+			if( this.unbiased ) {
+				// the theory is that equal weights for each sample lead to an accumulation of rounding errors.
+				// The following equation varies the sampleWeight per sample so that it is uniformly distributed
+				// across a range of values whose rounding errors cancel each other out.
+				var uniformCenteredDistribution = ( -0.5 + ( i + 0.5 ) / jitterOffsets.length );
+				sampleWeight += roundingRange * uniformCenteredDistribution;
 			}
 
+			this.copyUniforms[ "opacity" ].value = sampleWeight;
+
 			renderer.render( this.scene, this.camera, this.sampleRenderTarget, true );
 			renderer.render( this.scene2, this.camera2, writeBuffer, (i === 0) );
 
 		}
 
-		// reset jitter to nothing.	TODO: add support for orthogonal cameras
-		if ( camera.setViewOffset ) camera.setViewOffset( undefined, undefined, undefined, undefined, undefined, undefined );
+		if ( this.camera.clearViewOffset ) this.camera.clearViewOffset();
 
 		renderer.autoClear = autoClear;
 

+ 4 - 8
examples/js/postprocessing/TAARenderPass.js

@@ -45,8 +45,6 @@ Object.assign( THREE.TAARenderPass.prototype, {
 
 		var jitterOffsets = THREE.TAARenderPass.JitterVectors[ 5 ];
 
-		var camera = ( this.camera || this.scene.camera );
-
 		if ( ! this.sampleRenderTarget ) {
 
 			this.sampleRenderTarget = new THREE.WebGLRenderTarget( readBuffer.width, readBuffer.height, this.params );
@@ -82,10 +80,9 @@ Object.assign( THREE.TAARenderPass.prototype, {
 			for ( var i = 0; i < numSamplesPerFrame; i ++ ) {
 
 				var j = this.accumulateIndex;
-				// only jitters perspective cameras.	TODO: add support for jittering orthogonal cameras
 				var jitterOffset = jitterOffsets[j];
-				if ( camera.setViewOffset ) {
-					camera.setViewOffset( readBuffer.width, readBuffer.height,
+				if ( this.camera.setViewOffset ) {
+					this.camera.setViewOffset( readBuffer.width, readBuffer.height,
 						jitterOffset[ 0 ] * 0.0625, jitterOffset[ 1 ] * 0.0625,   // 0.0625 = 1 / 16
 						readBuffer.width, readBuffer.height );
 				}
@@ -97,9 +94,8 @@ Object.assign( THREE.TAARenderPass.prototype, {
 				if( this.accumulateIndex >= jitterOffsets.length ) break;
 			}
 
-			// reset jitter to nothing.	TODO: add support for orthogonal cameras
-			if ( camera.setViewOffset ) camera.setViewOffset( undefined, undefined, undefined, undefined, undefined, undefined );
-
+			if ( this.camera.clearViewOffset ) this.camera.clearViewOffset();
+			
 		}
 
 		var accumulationWeight = this.accumulateIndex * sampleWeight;

+ 7 - 9
examples/webgl_postprocessing_msaa.html

@@ -53,7 +53,9 @@
 			var camera, scene, renderer, composer, copyPass, msaaRenderPass;
 			var gui, stats, texture;
 
-			var param = { MSAASampleLevel: 2 };
+			var param = {
+				sampleLevel: 2
+			};
 
 			init();
 			animate();
@@ -66,19 +68,13 @@
 
 				gui = new dat.GUI();
 
-				var example = gui.add( param, 'MSAASampleLevel', {
+				gui.add( param, 'sampleLevel', {
 					'Level 0: 1 Sample': 0,
 					'Level 1: 2 Samples': 1,
 					'Level 2: 4 Samples': 2,
 					'Level 3: 8 Samples': 3,
 					'Level 4: 16 Samples': 4,
 					'Level 5: 32 Samples': 5
-				} ).onFinishChange( function() {
-
-					if( msaaRenderPass ) {
-						msaaRenderPass.sampleLevel = param.MSAASampleLevel;
-					}
-
 				} );
 
 				gui.open();
@@ -128,7 +124,7 @@
 				composer = new THREE.EffectComposer( renderer );
 
 				msaaRenderPass = new THREE.ManualMSAARenderPass( scene, camera );
-				msaaRenderPass.sampleLevel = param.MSAASampleLevel;
+				msaaRenderPass.unbiased = false;
 				composer.addPass( msaaRenderPass );
 
 				copyPass = new THREE.ShaderPass( THREE.CopyShader );
@@ -171,6 +167,8 @@
 
 				}
 
+				msaaRenderPass.sampleLevel = param.sampleLevel;
+
 				composer.render();
 				stats.end();
 

+ 211 - 0
examples/webgl_postprocessing_msaa_unbiased.html

@@ -0,0 +1,211 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - postprocessing manual msaa</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 {
+				margin: 0px;
+				background-color: #000;
+				overflow: hidden;
+				font-family:Monospace;
+				font-size:13px;
+				margin: 0px;
+				text-align:center;
+				overflow: hidden;
+			}
+
+			#info {
+				color: #fff;
+				position: absolute;
+				top: 10px;
+				width: 100%;
+				text-align: center;
+				display:block;
+			}
+		</style>
+	</head>
+	<body>
+		<div id="info">
+			<a href="http://threejs.org" target="_blank">three.js</a> - Unbiased Manual Multi-Sample Anti-Aliasing (MSAA) pass by <a href="https://clara.io" target="_blank">Ben Houston</a><br/><br/>
+			This example shows how to unbias the rounding errors accumulated using high number of MSAA samples on a 8-bit per channel buffer.<br/><br/>
+			Turn off the "unbiased" feature to see the banding that results from accumulated rounding errors.
+		</div>
+
+		<div id="container"></div>
+
+		<script src="../build/three.js"></script>
+		<script src="js/libs/stats.min.js"></script>
+		<script src="js/libs/dat.gui.min.js"></script>
+
+		<script src="js/shaders/CopyShader.js"></script>
+
+		<script src="js/postprocessing/EffectComposer.js"></script>
+		<script src="js/postprocessing/ManualMSAARenderPass.js"></script>
+		<script src="js/postprocessing/RenderPass.js"></script>
+		<script src="js/postprocessing/MaskPass.js"></script>
+		<script src="js/postprocessing/ShaderPass.js"></script>
+
+
+		<script>
+
+			var camera, scene, renderer, composer, copyPass, msaaRenderPass;
+			var gui, stats, texture;
+
+			var param = {
+				sampleLevel: 4,
+				unbiased: true
+			};
+
+			init();
+			animate();
+
+			clearGui();
+
+			function clearGui() {
+
+				if ( gui ) gui.destroy();
+
+				gui = new dat.GUI();
+
+				gui.add( param, "unbiased" );
+				gui.add( param, 'sampleLevel', {
+					'Level 0: 1 Sample': 0,
+					'Level 1: 2 Samples': 1,
+					'Level 2: 4 Samples': 2,
+					'Level 3: 8 Samples': 3,
+					'Level 4: 16 Samples': 4,
+					'Level 5: 32 Samples': 5
+				} );
+
+				gui.open();
+
+			}
+
+			function init() {
+
+				container = document.getElementById( "container" );
+
+				var width = window.innerWidth || 1;
+				var height = window.innerHeight || 1;
+				var devicePixelRatio = window.devicePixelRatio || 1;
+
+				renderer = new THREE.WebGLRenderer( { antialias: false } );
+				renderer.setPixelRatio( devicePixelRatio );
+				renderer.setSize( width, height );
+				document.body.appendChild( renderer.domElement );
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				//
+
+				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, 1.0 );
+				light.position.z = 70;
+				light.position.y = -70;
+				light.position.x = -70;
+				scene.add( light );
+
+				var light2 = new THREE.PointLight( 0xffdddd, 1.0 );
+				light2.position.z = 70;
+				light2.position.x = -70;
+				light2.position.y = 70;
+				scene.add( light2 );
+
+				var light3 = new THREE.PointLight( 0xddddff, 1.0 );
+				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 );
+				}
+
+				// postprocessing
+
+				composer = new THREE.EffectComposer( renderer );
+
+				msaaRenderPass = new THREE.ManualMSAARenderPass( scene, camera );
+				composer.addPass( msaaRenderPass );
+
+				copyPass = new THREE.ShaderPass( THREE.CopyShader );
+		    copyPass.renderToScreen = true;
+				composer.addPass( copyPass );
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			function onWindowResize() {
+
+				var width = window.innerWidth;
+				var height = window.innerHeight;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( width, height );
+
+				var pixelRatio = renderer.getPixelRatio();
+				var newWidth  = Math.floor( width / pixelRatio ) || 1;
+				var newHeight = Math.floor( height / pixelRatio ) || 1;
+				composer.setSize( newWidth, newHeight );
+
+			}
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				stats.begin();
+
+				for ( var i = 0; i < scene.children.length; i ++ ) {
+
+					var child = scene.children[ i ];
+
+					child.rotation.x += 0.005;
+					child.rotation.y += 0.01;
+
+				}
+
+				msaaRenderPass.sampleLevel = param.sampleLevel;
+				msaaRenderPass.unbiased = param.unbiased;
+
+				composer.render();
+				stats.end();
+
+			}
+
+		</script>
+		<div>
+	</body>
+</html>

+ 1 - 0
examples/webgl_postprocessing_taa.html

@@ -144,6 +144,7 @@
 				composer = new THREE.EffectComposer( renderer );
 
 				taaRenderPass = new THREE.TAARenderPass( scene, camera );
+				taaRenderPass.unbiased = false;
 				composer.addPass( taaRenderPass );
 
 				renderPass = new THREE.RenderPass( scene, camera );

+ 7 - 0
src/cameras/PerspectiveCamera.js

@@ -155,6 +155,13 @@ THREE.PerspectiveCamera.prototype = Object.assign( Object.create( THREE.Camera.p
 
 	},
 
+	clearViewOffset: function() {
+
+		this.view = null;
+		this.updateProjectionMatrix();
+
+	},
+
 	updateProjectionMatrix: function () {
 
 		var near = this.near,