Skip to content

Commit bb5600f

Browse files
bhoustonmrdoob
authored andcommitted
add unbiased mode to the MSAA render pass. (mrdoob#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.
1 parent 60ef1a6 commit bb5600f

File tree

7 files changed

+251
-24
lines changed

7 files changed

+251
-24
lines changed

examples/files.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ var files = {
189189
"webgl_postprocessing_godrays",
190190
"webgl_postprocessing_masking",
191191
"webgl_postprocessing_msaa",
192+
"webgl_postprocessing_msaa_unbiased",
192193
"webgl_postprocessing_nodes",
193194
"webgl_postprocessing_procedural",
194195
"webgl_postprocessing_smaa",

examples/js/postprocessing/ManualMSAARenderPass.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ THREE.ManualMSAARenderPass = function ( scene, camera ) {
1818
this.camera = camera;
1919

2020
this.sampleLevel = 4; // specified as n, where the number of samples is 2^n, so sampleLevel = 4, is 2^4 samples, 16.
21+
this.unbiased = true;
2122

2223
if ( THREE.CopyShader === undefined ) console.error( "THREE.ManualMSAARenderPass relies on THREE.CopyShader" );
2324

@@ -77,27 +78,39 @@ Object.assign( THREE.ManualMSAARenderPass.prototype, {
7778
var autoClear = renderer.autoClear;
7879
renderer.autoClear = false;
7980

80-
this.copyUniforms[ "opacity" ].value = 1.0 / jitterOffsets.length;
81+
var baseSampleWeight = 1.0 / jitterOffsets.length;
82+
var roundingRange = 1 / 32;
8183
this.copyUniforms[ "tDiffuse" ].value = this.sampleRenderTarget.texture;
8284

85+
var width = readBuffer.width, height = readBuffer.height;
86+
8387
// render the scene multiple times, each slightly jitter offset from the last and accumulate the results.
8488
for ( var i = 0; i < jitterOffsets.length; i ++ ) {
8589

86-
// only jitters perspective cameras. TODO: add support for jittering orthogonal cameras
8790
var jitterOffset = jitterOffsets[i];
88-
if ( camera.setViewOffset ) {
89-
camera.setViewOffset( readBuffer.width, readBuffer.height,
91+
if ( this.camera.setViewOffset ) {
92+
this.camera.setViewOffset( width, height,
9093
jitterOffset[ 0 ] * 0.0625, jitterOffset[ 1 ] * 0.0625, // 0.0625 = 1 / 16
91-
readBuffer.width, readBuffer.height );
94+
width, height );
95+
}
96+
97+
var sampleWeight = baseSampleWeight;
98+
if( this.unbiased ) {
99+
// the theory is that equal weights for each sample lead to an accumulation of rounding errors.
100+
// The following equation varies the sampleWeight per sample so that it is uniformly distributed
101+
// across a range of values whose rounding errors cancel each other out.
102+
var uniformCenteredDistribution = ( -0.5 + ( i + 0.5 ) / jitterOffsets.length );
103+
sampleWeight += roundingRange * uniformCenteredDistribution;
92104
}
93105

106+
this.copyUniforms[ "opacity" ].value = sampleWeight;
107+
94108
renderer.render( this.scene, this.camera, this.sampleRenderTarget, true );
95109
renderer.render( this.scene2, this.camera2, writeBuffer, (i === 0) );
96110

97111
}
98112

99-
// reset jitter to nothing. TODO: add support for orthogonal cameras
100-
if ( camera.setViewOffset ) camera.setViewOffset( undefined, undefined, undefined, undefined, undefined, undefined );
113+
if ( this.camera.clearViewOffset ) this.camera.clearViewOffset();
101114

102115
renderer.autoClear = autoClear;
103116

examples/js/postprocessing/TAARenderPass.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ Object.assign( THREE.TAARenderPass.prototype, {
4545

4646
var jitterOffsets = THREE.TAARenderPass.JitterVectors[ 5 ];
4747

48-
var camera = ( this.camera || this.scene.camera );
49-
5048
if ( ! this.sampleRenderTarget ) {
5149

5250
this.sampleRenderTarget = new THREE.WebGLRenderTarget( readBuffer.width, readBuffer.height, this.params );
@@ -82,10 +80,9 @@ Object.assign( THREE.TAARenderPass.prototype, {
8280
for ( var i = 0; i < numSamplesPerFrame; i ++ ) {
8381

8482
var j = this.accumulateIndex;
85-
// only jitters perspective cameras. TODO: add support for jittering orthogonal cameras
8683
var jitterOffset = jitterOffsets[j];
87-
if ( camera.setViewOffset ) {
88-
camera.setViewOffset( readBuffer.width, readBuffer.height,
84+
if ( this.camera.setViewOffset ) {
85+
this.camera.setViewOffset( readBuffer.width, readBuffer.height,
8986
jitterOffset[ 0 ] * 0.0625, jitterOffset[ 1 ] * 0.0625, // 0.0625 = 1 / 16
9087
readBuffer.width, readBuffer.height );
9188
}
@@ -97,9 +94,8 @@ Object.assign( THREE.TAARenderPass.prototype, {
9794
if( this.accumulateIndex >= jitterOffsets.length ) break;
9895
}
9996

100-
// reset jitter to nothing. TODO: add support for orthogonal cameras
101-
if ( camera.setViewOffset ) camera.setViewOffset( undefined, undefined, undefined, undefined, undefined, undefined );
102-
97+
if ( this.camera.clearViewOffset ) this.camera.clearViewOffset();
98+
10399
}
104100

105101
var accumulationWeight = this.accumulateIndex * sampleWeight;

examples/webgl_postprocessing_msaa.html

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
var camera, scene, renderer, composer, copyPass, msaaRenderPass;
5454
var gui, stats, texture;
5555

56-
var param = { MSAASampleLevel: 2 };
56+
var param = {
57+
sampleLevel: 2
58+
};
5759

5860
init();
5961
animate();
@@ -66,19 +68,13 @@
6668

6769
gui = new dat.GUI();
6870

69-
var example = gui.add( param, 'MSAASampleLevel', {
71+
gui.add( param, 'sampleLevel', {
7072
'Level 0: 1 Sample': 0,
7173
'Level 1: 2 Samples': 1,
7274
'Level 2: 4 Samples': 2,
7375
'Level 3: 8 Samples': 3,
7476
'Level 4: 16 Samples': 4,
7577
'Level 5: 32 Samples': 5
76-
} ).onFinishChange( function() {
77-
78-
if( msaaRenderPass ) {
79-
msaaRenderPass.sampleLevel = param.MSAASampleLevel;
80-
}
81-
8278
} );
8379

8480
gui.open();
@@ -128,7 +124,7 @@
128124
composer = new THREE.EffectComposer( renderer );
129125

130126
msaaRenderPass = new THREE.ManualMSAARenderPass( scene, camera );
131-
msaaRenderPass.sampleLevel = param.MSAASampleLevel;
127+
msaaRenderPass.unbiased = false;
132128
composer.addPass( msaaRenderPass );
133129

134130
copyPass = new THREE.ShaderPass( THREE.CopyShader );
@@ -171,6 +167,8 @@
171167

172168
}
173169

170+
msaaRenderPass.sampleLevel = param.sampleLevel;
171+
174172
composer.render();
175173
stats.end();
176174

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>three.js webgl - postprocessing manual msaa</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
7+
<style>
8+
body {
9+
margin: 0px;
10+
background-color: #000;
11+
overflow: hidden;
12+
font-family:Monospace;
13+
font-size:13px;
14+
margin: 0px;
15+
text-align:center;
16+
overflow: hidden;
17+
}
18+
19+
#info {
20+
color: #fff;
21+
position: absolute;
22+
top: 10px;
23+
width: 100%;
24+
text-align: center;
25+
display:block;
26+
}
27+
</style>
28+
</head>
29+
<body>
30+
<div id="info">
31+
<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/>
32+
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/>
33+
Turn off the "unbiased" feature to see the banding that results from accumulated rounding errors.
34+
</div>
35+
36+
<div id="container"></div>
37+
38+
<script src="../build/three.js"></script>
39+
<script src="js/libs/stats.min.js"></script>
40+
<script src="js/libs/dat.gui.min.js"></script>
41+
42+
<script src="js/shaders/CopyShader.js"></script>
43+
44+
<script src="js/postprocessing/EffectComposer.js"></script>
45+
<script src="js/postprocessing/ManualMSAARenderPass.js"></script>
46+
<script src="js/postprocessing/RenderPass.js"></script>
47+
<script src="js/postprocessing/MaskPass.js"></script>
48+
<script src="js/postprocessing/ShaderPass.js"></script>
49+
50+
51+
<script>
52+
53+
var camera, scene, renderer, composer, copyPass, msaaRenderPass;
54+
var gui, stats, texture;
55+
56+
var param = {
57+
sampleLevel: 4,
58+
unbiased: true
59+
};
60+
61+
init();
62+
animate();
63+
64+
clearGui();
65+
66+
function clearGui() {
67+
68+
if ( gui ) gui.destroy();
69+
70+
gui = new dat.GUI();
71+
72+
gui.add( param, "unbiased" );
73+
gui.add( param, 'sampleLevel', {
74+
'Level 0: 1 Sample': 0,
75+
'Level 1: 2 Samples': 1,
76+
'Level 2: 4 Samples': 2,
77+
'Level 3: 8 Samples': 3,
78+
'Level 4: 16 Samples': 4,
79+
'Level 5: 32 Samples': 5
80+
} );
81+
82+
gui.open();
83+
84+
}
85+
86+
function init() {
87+
88+
container = document.getElementById( "container" );
89+
90+
var width = window.innerWidth || 1;
91+
var height = window.innerHeight || 1;
92+
var devicePixelRatio = window.devicePixelRatio || 1;
93+
94+
renderer = new THREE.WebGLRenderer( { antialias: false } );
95+
renderer.setPixelRatio( devicePixelRatio );
96+
renderer.setSize( width, height );
97+
document.body.appendChild( renderer.domElement );
98+
99+
stats = new Stats();
100+
container.appendChild( stats.dom );
101+
102+
//
103+
104+
camera = new THREE.PerspectiveCamera( 65, width / height, 3, 10 );
105+
camera.position.z = 7;
106+
107+
scene = new THREE.Scene();
108+
109+
group = new THREE.Object3D();
110+
scene.add( group );
111+
112+
var light = new THREE.PointLight( 0xddffdd, 1.0 );
113+
light.position.z = 70;
114+
light.position.y = -70;
115+
light.position.x = -70;
116+
scene.add( light );
117+
118+
var light2 = new THREE.PointLight( 0xffdddd, 1.0 );
119+
light2.position.z = 70;
120+
light2.position.x = -70;
121+
light2.position.y = 70;
122+
scene.add( light2 );
123+
124+
var light3 = new THREE.PointLight( 0xddddff, 1.0 );
125+
light3.position.z = 70;
126+
light3.position.x = 70;
127+
light3.position.y = -70;
128+
scene.add( light3 );
129+
130+
var light3 = new THREE.AmbientLight( 0xffffff, 0.05 );
131+
scene.add( light3 );
132+
133+
var geometry = new THREE.SphereBufferGeometry( 3, 48, 24 );
134+
for ( var i = 0; i < 120; i ++ ) {
135+
136+
var material = new THREE.MeshStandardMaterial();
137+
material.roughness = 0.5 * Math.random() + 0.25;
138+
material.metalness = 0;
139+
material.color.setHSL( Math.random(), 1.0, 0.3 );
140+
141+
var mesh = new THREE.Mesh( geometry, material );
142+
mesh.position.x = Math.random() * 4 - 2;
143+
mesh.position.y = Math.random() * 4 - 2;
144+
mesh.position.z = Math.random() * 4 - 2;
145+
mesh.rotation.x = Math.random();
146+
mesh.rotation.y = Math.random();
147+
mesh.rotation.z = Math.random();
148+
149+
mesh.scale.x = mesh.scale.y = mesh.scale.z = Math.random() * 0.2 + 0.05;
150+
group.add( mesh );
151+
}
152+
153+
// postprocessing
154+
155+
composer = new THREE.EffectComposer( renderer );
156+
157+
msaaRenderPass = new THREE.ManualMSAARenderPass( scene, camera );
158+
composer.addPass( msaaRenderPass );
159+
160+
copyPass = new THREE.ShaderPass( THREE.CopyShader );
161+
copyPass.renderToScreen = true;
162+
composer.addPass( copyPass );
163+
164+
window.addEventListener( 'resize', onWindowResize, false );
165+
166+
}
167+
168+
function onWindowResize() {
169+
170+
var width = window.innerWidth;
171+
var height = window.innerHeight;
172+
173+
camera.aspect = width / height;
174+
camera.updateProjectionMatrix();
175+
176+
renderer.setSize( width, height );
177+
178+
var pixelRatio = renderer.getPixelRatio();
179+
var newWidth = Math.floor( width / pixelRatio ) || 1;
180+
var newHeight = Math.floor( height / pixelRatio ) || 1;
181+
composer.setSize( newWidth, newHeight );
182+
183+
}
184+
185+
function animate() {
186+
187+
requestAnimationFrame( animate );
188+
189+
stats.begin();
190+
191+
for ( var i = 0; i < scene.children.length; i ++ ) {
192+
193+
var child = scene.children[ i ];
194+
195+
child.rotation.x += 0.005;
196+
child.rotation.y += 0.01;
197+
198+
}
199+
200+
msaaRenderPass.sampleLevel = param.sampleLevel;
201+
msaaRenderPass.unbiased = param.unbiased;
202+
203+
composer.render();
204+
stats.end();
205+
206+
}
207+
208+
</script>
209+
<div>
210+
</body>
211+
</html>

examples/webgl_postprocessing_taa.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
composer = new THREE.EffectComposer( renderer );
145145

146146
taaRenderPass = new THREE.TAARenderPass( scene, camera );
147+
taaRenderPass.unbiased = false;
147148
composer.addPass( taaRenderPass );
148149

149150
renderPass = new THREE.RenderPass( scene, camera );

src/cameras/PerspectiveCamera.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@ THREE.PerspectiveCamera.prototype = Object.assign( Object.create( THREE.Camera.p
155155

156156
},
157157

158+
clearViewOffset: function() {
159+
160+
this.view = null;
161+
this.updateProjectionMatrix();
162+
163+
},
164+
158165
updateProjectionMatrix: function () {
159166

160167
var near = this.near,

0 commit comments

Comments
 (0)