You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
17 KiB
294 lines
17 KiB
<!DOCTYPE html><html lang="en"><head>
|
|
<meta charset="utf-8">
|
|
<title>Post Processing</title>
|
|
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:site" content="@threejs">
|
|
<meta name="twitter:title" content="Three.js – Post Processing">
|
|
<meta property="og:image" content="https://threejs.org/files/share.png">
|
|
<link rel="shortcut icon" href="/files/favicon_white.ico" media="(prefers-color-scheme: dark)">
|
|
<link rel="shortcut icon" href="/files/favicon.ico" media="(prefers-color-scheme: light)">
|
|
|
|
<link rel="stylesheet" href="/manual/resources/lesson.css">
|
|
<link rel="stylesheet" href="/manual/resources/lang.css">
|
|
<!-- Import maps polyfill -->
|
|
<!-- Remove this when import maps will be widely supported -->
|
|
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "../../build/three.module.js"
|
|
}
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="lesson-title">
|
|
<h1>Post Processing</h1>
|
|
</div>
|
|
<div class="lesson">
|
|
<div class="lesson-main">
|
|
<p><em>Post processing</em> generally refers to applying some kind of effect or filter to
|
|
a 2D image. In the case of THREE.js we have a scene with a bunch of meshes in
|
|
it. We render that scene into a 2D image. Normally that image is rendered
|
|
directly into the canvas and displayed in the browser but instead we can <a href="rendertargets.html">render
|
|
it to a render target</a> and then apply some <em>post
|
|
processing</em> effects to the result before drawing it to the canvas. It's called
|
|
post processing because it happens after (post) the main scene processing.</p>
|
|
<p>Examples of post processing are Instagram like filters,
|
|
Photoshop filters, etc...</p>
|
|
<p>THREE.js has some example classes to help setup a post processing pipeline. The
|
|
way it works is you create an <code class="notranslate" translate="no">EffectComposer</code> and to it you add multiple <code class="notranslate" translate="no">Pass</code>
|
|
objects. You then call <code class="notranslate" translate="no">EffectComposer.render</code> and it renders your scene to a
|
|
<a href="rendertargets.html">render target</a> and then applies each <code class="notranslate" translate="no">Pass</code>.</p>
|
|
<p>Each <code class="notranslate" translate="no">Pass</code> can be some post processing effect like adding a vignette, blurring,
|
|
applying a bloom, applying film grain, adjusting the hue, saturation, contrast,
|
|
etc... and finally rendering the result to the canvas.</p>
|
|
<p>It's a little bit important to understand how <code class="notranslate" translate="no">EffectComposer</code> functions. It
|
|
creates two <a href="rendertargets.html">render targets</a>. Let's call them
|
|
<strong>rtA</strong> and <strong>rtB</strong>.</p>
|
|
<p>Then, you call <code class="notranslate" translate="no">EffectComposer.addPass</code> to add each pass in the order you want
|
|
to apply them. The passes are then applied <em>something like</em> this.</p>
|
|
<div class="threejs_center"><img src="../resources/images/threejs-postprocessing.svg" style="width: 600px"></div>
|
|
|
|
<p>First the scene you passed into <code class="notranslate" translate="no">RenderPass</code> is rendered to <strong>rtA</strong>, then
|
|
<strong>rtA</strong> is passed to the next pass, whatever it is. That pass uses <strong>rtA</strong> as
|
|
input to do whatever it does and writes the results to <strong>rtB</strong>. <strong>rtB</strong> is then
|
|
passed to the next pass which uses <strong>rtB</strong> as input and writes back to <strong>rtA</strong>.
|
|
This continues through all the passes. </p>
|
|
<p>Each <code class="notranslate" translate="no">Pass</code> has 4 basic options</p>
|
|
<h2 id="-enabled-"><code class="notranslate" translate="no">enabled</code></h2>
|
|
<p>Whether or not to use this pass</p>
|
|
<h2 id="-needsswap-"><code class="notranslate" translate="no">needsSwap</code></h2>
|
|
<p>Whether or not to swap <code class="notranslate" translate="no">rtA</code> and <code class="notranslate" translate="no">rtB</code> after finishing this pass</p>
|
|
<h2 id="-clear-"><code class="notranslate" translate="no">clear</code></h2>
|
|
<p>Whether or not to clear before rendering this pass</p>
|
|
<h2 id="-rendertoscreen-"><code class="notranslate" translate="no">renderToScreen</code></h2>
|
|
<p>Whether or not to render to the canvas instead the current destination render
|
|
target. Usually you need to set this to true on the last pass you add to your
|
|
<code class="notranslate" translate="no">EffectComposer</code>.</p>
|
|
<p>Let's put together a basic example. We'll start with the example from <a href="responsive.html">the
|
|
article on responsiveness</a>.</p>
|
|
<p>To that first we create an <code class="notranslate" translate="no">EffectComposer</code>.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const composer = new EffectComposer(renderer);
|
|
</pre>
|
|
<p>Then as the first pass we add a <code class="notranslate" translate="no">RenderPass</code> that will render our scene with our
|
|
camera into the first render target.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">composer.addPass(new RenderPass(scene, camera));
|
|
</pre>
|
|
<p>Next we add a <code class="notranslate" translate="no">BloomPass</code>. A <code class="notranslate" translate="no">BloomPass</code> renders its input to a generally
|
|
smaller render target and blurs the result. It then adds that blurred result on
|
|
top of the original input. This makes the scene <em>bloom</em></p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const bloomPass = new BloomPass(
|
|
1, // strength
|
|
25, // kernel size
|
|
4, // sigma ?
|
|
256, // blur render target resolution
|
|
);
|
|
composer.addPass(bloomPass);
|
|
</pre>
|
|
<p>Finally we had a <code class="notranslate" translate="no">FilmPass</code> that draws noise and scanlines on top of its input.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const filmPass = new FilmPass(
|
|
0.35, // noise intensity
|
|
0.025, // scanline intensity
|
|
648, // scanline count
|
|
false, // grayscale
|
|
);
|
|
filmPass.renderToScreen = true;
|
|
composer.addPass(filmPass);
|
|
</pre>
|
|
<p>Since the <code class="notranslate" translate="no">filmPass</code> is the last pass we set its <code class="notranslate" translate="no">renderToScreen</code> property to
|
|
true to tell it to render to the canvas. Without setting this it would instead
|
|
render to the next render target.</p>
|
|
<p>To use these classes we need to import a bunch of scripts.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js';
|
|
import {RenderPass} from 'three/addons/postprocessing/RenderPass.js';
|
|
import {BloomPass} from 'three/addons/postprocessing/BloomPass.js';
|
|
import {FilmPass} from 'three/addons/postprocessing/FilmPass.js';
|
|
</pre>
|
|
<p>For pretty much any post processing <code class="notranslate" translate="no">EffectComposer.js</code>, and <code class="notranslate" translate="no">RenderPass.js</code>
|
|
are required.</p>
|
|
<p>The last things we need to do are to use <code class="notranslate" translate="no">EffectComposer.render</code> instead of
|
|
<a href="/docs/#api/en/renderers/WebGLRenderer.render"><code class="notranslate" translate="no">WebGLRenderer.render</code></a> <em>and</em> to tell the <code class="notranslate" translate="no">EffectComposer</code> to match the size of
|
|
the canvas.</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function render(now) {
|
|
- time *= 0.001;
|
|
+let then = 0;
|
|
+function render(now) {
|
|
+ now *= 0.001; // convert to seconds
|
|
+ const deltaTime = now - then;
|
|
+ then = now;
|
|
|
|
if (resizeRendererToDisplaySize(renderer)) {
|
|
const canvas = renderer.domElement;
|
|
camera.aspect = canvas.clientWidth / canvas.clientHeight;
|
|
camera.updateProjectionMatrix();
|
|
+ composer.setSize(canvas.width, canvas.height);
|
|
}
|
|
|
|
cubes.forEach((cube, ndx) => {
|
|
const speed = 1 + ndx * .1;
|
|
- const rot = time * speed;
|
|
+ const rot = now * speed;
|
|
cube.rotation.x = rot;
|
|
cube.rotation.y = rot;
|
|
});
|
|
|
|
- renderer.render(scene, camera);
|
|
+ composer.render(deltaTime);
|
|
|
|
requestAnimationFrame(render);
|
|
}
|
|
</pre>
|
|
<p><code class="notranslate" translate="no">EffectComposer.render</code> takes a <code class="notranslate" translate="no">deltaTime</code> which is the time in seconds since
|
|
the last frame was rendered. It passes this to the various effects in case any
|
|
of them are animated. In this case the <code class="notranslate" translate="no">FilmPass</code> is animated.</p>
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/postprocessing.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/postprocessing.html" target="_blank">click here to open in a separate window</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>To change effect parameters at runtime usually requires setting uniform values.
|
|
Let's add a gui to adjust some of the parameters. Figuring out which values you
|
|
can easily adjust and how to adjust them requires digging through the code for
|
|
that effect.</p>
|
|
<p>Looking inside
|
|
<a href="https://github.com/mrdoob/three.js/blob/master/examples/js/postprocessing/BloomPass.js"><code class="notranslate" translate="no">BloomPass.js</code></a>
|
|
I found this line:</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">this.copyUniforms[ "opacity" ].value = strength;
|
|
</pre>
|
|
<p>So we can set the strength by setting</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">bloomPass.copyUniforms.opacity.value = someValue;
|
|
</pre>
|
|
<p>Similarly looking in
|
|
<a href="https://github.com/mrdoob/three.js/blob/master/examples/js/postprocessing/FilmPass.js"><code class="notranslate" translate="no">FilmPass.js</code></a>
|
|
I found these lines:</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">if ( grayscale !== undefined ) this.uniforms.grayscale.value = grayscale;
|
|
if ( noiseIntensity !== undefined ) this.uniforms.nIntensity.value = noiseIntensity;
|
|
if ( scanlinesIntensity !== undefined ) this.uniforms.sIntensity.value = scanlinesIntensity;
|
|
if ( scanlinesCount !== undefined ) this.uniforms.sCount.value = scanlinesCount;
|
|
</pre>
|
|
<p>So which makes it pretty clear how to set them.</p>
|
|
<p>Let's make a quick GUI to set those values</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
|
|
</pre>
|
|
<p>and</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
|
|
{
|
|
const folder = gui.addFolder('BloomPass');
|
|
folder.add(bloomPass.copyUniforms.opacity, 'value', 0, 2).name('strength');
|
|
folder.open();
|
|
}
|
|
{
|
|
const folder = gui.addFolder('FilmPass');
|
|
folder.add(filmPass.uniforms.grayscale, 'value').name('grayscale');
|
|
folder.add(filmPass.uniforms.nIntensity, 'value', 0, 1).name('noise intensity');
|
|
folder.add(filmPass.uniforms.sIntensity, 'value', 0, 1).name('scanline intensity');
|
|
folder.add(filmPass.uniforms.sCount, 'value', 0, 1000).name('scanline count');
|
|
folder.open();
|
|
}
|
|
</pre>
|
|
<p>and now we can adjust those settings</p>
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/postprocessing-gui.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/postprocessing-gui.html" target="_blank">click here to open in a separate window</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>That was a small step to making our own effect.</p>
|
|
<p>Post processing effects use shaders. Shaders are written in a language called
|
|
<a href="https://www.khronos.org/files/opengles_shading_language.pdf">GLSL (Graphics Library Shading Language)</a>. Going
|
|
over the entire language is way too large a topic for these articles. A few
|
|
resources to get start from would be maybe <a href="https://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html">this article</a>
|
|
and maybe <a href="https://thebookofshaders.com/">the Book of Shaders</a>.</p>
|
|
<p>I think an example to get you started would be helpful though so let's make a
|
|
simple GLSL post processing shader. We'll make one that lets us multiply the
|
|
image by a color.</p>
|
|
<p>For post processing THREE.js provides a useful helper called the <code class="notranslate" translate="no">ShaderPass</code>.
|
|
It takes an object with info defining a vertex shader, a fragment shader, and
|
|
the default inputs. It will handling setting up which texture to read from to
|
|
get the previous pass's results and where to render to, either one of the
|
|
<code class="notranslate" translate="no">EffectComposer</code>s render target or the canvas.</p>
|
|
<p>Here's a simple post processing shader that multiplies the previous pass's
|
|
result by a color. </p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const colorShader = {
|
|
uniforms: {
|
|
tDiffuse: { value: null },
|
|
color: { value: new THREE.Color(0x88CCFF) },
|
|
},
|
|
vertexShader: `
|
|
varying vec2 vUv;
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
varying vec2 vUv;
|
|
uniform sampler2D tDiffuse;
|
|
uniform vec3 color;
|
|
void main() {
|
|
vec4 previousPassColor = texture2D(tDiffuse, vUv);
|
|
gl_FragColor = vec4(
|
|
previousPassColor.rgb * color,
|
|
previousPassColor.a);
|
|
}
|
|
`,
|
|
};
|
|
</pre>
|
|
<p>Above <code class="notranslate" translate="no">tDiffuse</code> is the name that <code class="notranslate" translate="no">ShaderPass</code> uses to pass in the previous
|
|
pass's result texture so we pretty much always need that. We then declare
|
|
<code class="notranslate" translate="no">color</code> as a THREE.js <a href="/docs/#api/en/math/Color"><code class="notranslate" translate="no">Color</code></a>.</p>
|
|
<p>Next we need a vertex shader. For post processing the vertex shader shown here
|
|
is pretty much standard and rarely needs to be changed. Without going into too
|
|
many details (see articles linked above) the variables <code class="notranslate" translate="no">uv</code>, <code class="notranslate" translate="no">projectionMatrix</code>,
|
|
<code class="notranslate" translate="no">modelViewMatrix</code> and <code class="notranslate" translate="no">position</code> are all magically added by THREE.js.</p>
|
|
<p>Finally we create a fragment shader. In it we get a pixel color from the
|
|
previous pass with this line</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">vec4 previousPassColor = texture2D(tDiffuse, vUv);
|
|
</pre>
|
|
<p>we multiply it by our color and set <code class="notranslate" translate="no">gl_FragColor</code> to the result</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">gl_FragColor = vec4(
|
|
previousPassColor.rgb * color,
|
|
previousPassColor.a);
|
|
</pre>
|
|
<p>Adding some simple GUI to set the 3 values of the color</p>
|
|
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
|
|
gui.add(colorPass.uniforms.color.value, 'r', 0, 4).name('red');
|
|
gui.add(colorPass.uniforms.color.value, 'g', 0, 4).name('green');
|
|
gui.add(colorPass.uniforms.color.value, 'b', 0, 4).name('blue');
|
|
</pre>
|
|
<p>Gives us a simple postprocessing effect that multiplies by a color.</p>
|
|
<p></p><div translate="no" class="threejs_example_container notranslate">
|
|
<div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/postprocessing-custom.html"></iframe></div>
|
|
<a class="threejs_center" href="/manual/examples/postprocessing-custom.html" target="_blank">click here to open in a separate window</a>
|
|
</div>
|
|
|
|
<p></p>
|
|
<p>As mentioned about all the details of how to write GLSL and custom shaders is
|
|
too much for these articles. If you really want to know how WebGL itself works
|
|
then check out <a href="https://webglfundamentals.org">these articles</a>. Another great
|
|
resources is just to
|
|
<a href="https://github.com/mrdoob/three.js/tree/master/examples/js/shaders">read through the existing post processing shaders in the THREE.js repo</a>. Some
|
|
are more complicated than others but if you start with the smaller ones you can
|
|
hopefully get an idea of how they work.</p>
|
|
<p>Most of the post processing effects in the THREE.js repo are unfortunately
|
|
undocumented so to use them you'll have to <a href="https://github.com/mrdoob/three.js/tree/master/examples">read through the examples</a> or
|
|
<a href="https://github.com/mrdoob/three.js/tree/master/examples/js/postprocessing">the code for the effects themselves</a>.
|
|
Hopefully these simple example and the article on
|
|
<a href="rendertargets.html">render targets</a> provide enough context to get started.</p>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/manual/resources/prettify.js"></script>
|
|
<script src="/manual/resources/lesson.js"></script>
|
|
|
|
|
|
|
|
|
|
</body></html>
|