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.
 
 
 
 
 

449 lines
26 KiB

<!DOCTYPE html><html lang="en"><head>
<meta charset="utf-8">
<title>Post Processing 3DLUT</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 3DLUT">
<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 3DLUT</h1>
</div>
<div class="lesson">
<div class="lesson-main">
<p>In the last article we went over <a href="post-processing.html">post processing</a>.
One of the common ways to post process is called a LUT or 3DLUT. LUT stands for LookUp Table. A 3DLUT is therefore a 3 dimensional look up table.</p>
<p>How it works is we make a cube of colors. Then we index the cube using the colors of our source image. For each pixel in the original image we look up a position in the cube based on the red, green, and blue colors of the original pixel. The value we pull out of the 3DLUT is the new color.</p>
<p>In Javascript we might do it like this. Imagine the colors are specified in integers from 0 to 255 and we have a large 3 dimensional array 256x256x256 in size. Then for to translate a color through the look up table</p>
<pre class="prettyprint showlinemods notranslate notranslate" translate="no">const newColor = lut[origColor.red][origColor.green][origColor.bue]
</pre><p>Of course a 256x256x256 array would be rather large but as we pointed out in <a href="textures.html">the article on textures</a> textures are referenced from values of 0.0 to 1.0 regardless of the dimensions of the texture.</p>
<p>Let's imagine an 8x8x8 cube.</p>
<div class="threejs_center"><img src="../resources/images/3dlut-rgb.svg" class="noinvertdark" style="width: 500px"></div>
<p>First we might fill in the corners with 0,0,0 corner being pure black, the opposite 1,1,1 corner pure white. 1,0,0 being pure <span style="color:red;">red</span>. 0,1,0 being pure <span style="color:green;">green</span> and 0,0,1 being <span style="color:blue;">blue</span>. </p>
<div class="threejs_center"><img src="../resources/images/3dlut-axis.svg" class="noinvertdark" style="width: 500px"></div>
<p>We'd add in the colors down each axis.</p>
<div class="threejs_center"><img src="../resources/images/3dlut-edges.svg" class="noinvertdark" style="width: 500px"></div>
<p>And the colors on edges that use 2 or more channels.</p>
<div class="threejs_center"><img src="../resources/images/3dlut-standard.svg" class="noinvertdark" style="width: 500px"></div>
<p>And finally fill in all the colors in between. This is an "identity" 3DLUT. It produces the exact same output as input. If you look up a color you'll get the same color out.</p>
<div class="threejs_center"><object type="image/svg+xml" data="resources/images/3dlut-standard-lookup.svg" class="noinvertdark" data-diagram="lookup" style="width: 600px"></object></div>
<p>If we change the cube to shades of amber though then as we look up colors, we look up the same locations in the 3D lookup table but they produce different output.</p>
<div class="threejs_center"><object type="image/svg+xml" data="resources/images/3dlut-amber-lookup.svg" class="noinvertdark" data-diagram="lookup" style="width: 600px"></object></div>
<p>Using this techinque by supplying a different lookup table we can apply all kinds of effects. Basically any effect that can be computed based only on a single color input. Those effects include adjusting hue, contrast, saturation, color cast, tint, brightness, exposure, levels, curves, posterize, shadows, highlghts, and many others. Even better they can all be combined into a single look up table.</p>
<p>To use it we need a scene to apply it to. Let's throw together a quick scene. We'll start with a glTF file and display it like we covered in <a href="load-gltf.html">the article on loading a glTF</a>. The model we're loading is <a href="https://sketchfab.com/models/a1d315908e9f45e5a3bc618bdfd2e7ee">this model</a> by <a href="https://sketchfab.com/sarath.irn.kat005">The Ice Wolves</a>. It uses no lights so I removed the lights.</p>
<p>We'll also add a background image like we covered in <a href="backgrounds.html">backgrounds and skyboxs</a>.</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-3dlut-prep.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/postprocessing-3dlut-prep.html" target="_blank">click here to open in a separate window</a>
</div>
<p></p>
<p>Now that we have a scene we need a 3DLUT. The simplest 3DLUT is a 2x2x2 identity LUT where <em>identity</em> means nothing happens. It's like multiplying by 1 or doing nothing, even though we're looking up colors in the LUT each color in maps to the same color out.</p>
<div class="threejs_center"><img src="../resources/images/3dlut-standard-2x2.svg" class="noinvertdark" style="width: 200px"></div>
<p>WebGL1 doesn't support 3D textures so we'll use 4x2 2D texture and treat it as a 3D texture inside a custom shader where each slice of the cube is spread out horizontally across the texture.</p>
<p>Here's the code to make 4x2 2D texture with the colors required for an identity LUT.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const makeIdentityLutTexture = function() {
const identityLUT = new Uint8Array([
0, 0, 0, 255, // black
255, 0, 0, 255, // red
0, 0, 255, 255, // blue
255, 0, 255, 255, // magenta
0, 255, 0, 255, // green
255, 255, 0, 255, // yellow
0, 255, 255, 255, // cyan
255, 255, 255, 255, // white
]);
return function(filter) {
const texture = new THREE.DataTexture(identityLUT, 4, 2, THREE.RGBAFormat);
texture.minFilter = filter;
texture.magFilter = filter;
texture.needsUpdate = true;
texture.flipY = false;
return texture;
};
}();
</pre>
<p>Let's make 2 of them, one filtered and one not</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lutTextures = [
{ name: 'identity', size: 2, texture: makeIdentityLutTexture(THREE.LinearFilter) },
{ name: 'identity not filtered', size: 2, texture: makeIdentityLutTexture(THREE.NearestFilter) },
];
</pre>
<p>Taking the example using a custom shader from <a href="post-processing.html">the article on post processing</a> lets use these 2 custom shaders instead.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lutShader = {
uniforms: {
tDiffuse: { value: null },
lutMap: { value: null },
lutMapSize: { value: 1, },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,
fragmentShader: `
#include &lt;common&gt;
#define FILTER_LUT true
uniform sampler2D tDiffuse;
uniform sampler2D lutMap;
uniform float lutMapSize;
varying vec2 vUv;
vec4 sampleAs3DTexture(sampler2D tex, vec3 texCoord, float size) {
float sliceSize = 1.0 / size; // space of 1 slice
float slicePixelSize = sliceSize / size; // space of 1 pixel
float width = size - 1.0;
float sliceInnerSize = slicePixelSize * width; // space of size pixels
float zSlice0 = floor( texCoord.z * width);
float zSlice1 = min( zSlice0 + 1.0, width);
float xOffset = slicePixelSize * 0.5 + texCoord.x * sliceInnerSize;
float yRange = (texCoord.y * width + 0.5) / size;
float s0 = xOffset + (zSlice0 * sliceSize);
#ifdef FILTER_LUT
float s1 = xOffset + (zSlice1 * sliceSize);
vec4 slice0Color = texture2D(tex, vec2(s0, yRange));
vec4 slice1Color = texture2D(tex, vec2(s1, yRange));
float zOffset = mod(texCoord.z * width, 1.0);
return mix(slice0Color, slice1Color, zOffset);
#else
return texture2D(tex, vec2( s0, yRange));
#endif
}
void main() {
vec4 originalColor = texture2D(tDiffuse, vUv);
gl_FragColor = sampleAs3DTexture(lutMap, originalColor.xyz, lutMapSize);
}
`,
};
const lutNearestShader = {
uniforms: {...lutShader.uniforms},
vertexShader: lutShader.vertexShader,
fragmentShader: lutShader.fragmentShader.replace('#define FILTER_LUT', '//'),
};
</pre>
<p>You can see in the fragment shader there is this line</p>
<pre class="prettyprint showlinemods notranslate lang-glsl" translate="no">#define FILTER_LUT true
</pre>
<p>To generate the second shader we comment out that line.</p>
<p>Then we use them to make 2 custom effects</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const effectLUT = new THREE.ShaderPass(lutShader);
const effectLUTNearest = new THREE.ShaderPass(lutNearestShader);
</pre>
<p>Translating our existing code that draws the background as a separate scene we a <code class="notranslate" translate="no">RenderPass</code> for both the scene drawing the glTF and the scene drawing the background.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const renderModel = new THREE.RenderPass(scene, camera);
renderModel.clear = false; // so we don't clear out the background
const renderBG = new THREE.RenderPass(sceneBG, cameraBG);
</pre>
<p>and we can setup our <code class="notranslate" translate="no">EffectComposer</code> to use all the passes</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const composer = new THREE.EffectComposer(renderer);
composer.addPass(renderBG);
composer.addPass(renderModel);
composer.addPass(effectLUT);
composer.addPass(effectLUTNearest);
composer.addPass(effectLUTNearest);
composer.addPass(gammaPass);
</pre>
<p>Let's make some GUI code to select one lut or the other</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lutNameIndexMap = {};
lutTextures.forEach((info, ndx) =&gt; {
lutNameIndexMap[info.name] = ndx;
});
const lutSettings = {
lut: lutNameIndexMap.identity,
};
const gui = new GUI({ width: 300 });
gui.add(lutSettings, 'lut', lutNameIndexMap);
</pre>
<p>The last thing to do is turn on one effect or the other, depending on whether or not we want filtering, set the effect to use the selected texture, and render via the <code class="notranslate" translate="no">EffectComposer</code></p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lutInfo = lutTextures[lutSettings.lut];
const effect = lutInfo.filter ? effectLUT : effectLUTNearest;
effectLUT.enabled = lutInfo.filter;
effectLUTNearest.enabled = !lutInfo.filter;
const lutTexture = lutInfo.texture;
effect.uniforms.lutMap.value = lutTexture;
effect.uniforms.lutMapSize.value = lutInfo.size;
composer.render(delta);
</pre>
<p>Given it's the identity 3DLUT nothing changes</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-3dlut-identity.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/postprocessing-3dlut-identity.html" target="_blank">click here to open in a separate window</a>
</div>
<p></p>
<p>but we select the unfiltered LUT we get something much more interesting</p>
<div class="threejs_center"><img src="../resources/images/unfiltered-3dlut.jpg" style="width: 500px"></div>
<p>Why does this happen? Because with filtering on, the GPU linearly interpolates between the colors. With filtering off it does no interpolation so looking up colors in the 3DLUT only gives one of the exact colors in the 3DLUT.</p>
<p>So how do we go about making more interesting 3DLUTs?</p>
<p>First decide on the resolution of the table you want and generate the slices of the lookup cube using a simple script.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const ctx = document.querySelector('canvas').getContext('2d');
function drawColorCubeImage(ctx, size) {
const canvas = ctx.canvas;
canvas.width = size * size;
canvas.height = size;
for (let zz = 0; zz &lt; size; ++zz) {
for (let yy = 0; yy &lt; size; ++yy) {
for (let xx = 0; xx &lt; size; ++xx) {
const r = Math.floor(xx / (size - 1) * 255);
const g = Math.floor(yy / (size - 1) * 255);
const b = Math.floor(zz / (size - 1) * 255);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(zz * size + xx, yy, 1, 1);
}
}
}
document.querySelector('#width').textContent = canvas.width;
document.querySelector('#height').textContent = canvas.height;
}
drawColorCubeImage(ctx, 8);
</pre>
<p>and we need a canvas </p>
<pre class="prettyprint showlinemods notranslate lang-html" translate="no">&lt;canvas&gt;&lt;/canvas&gt;
</pre>
<p>then we can generate a identity 3d lookup table for any size.</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/3dlut-base-cube-maker.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/3dlut-base-cube-maker.html" target="_blank">click here to open in a separate window</a>
</div>
<p></p>
<p>The larger the resolution the more fine adjustments we can make but being a cube of data the size required grows quickly. A size 8 cube only requires 2k but a size 64 cube requires 1meg. So use the smallest that reproduces the effect you want.</p>
<p>Let's set the size to 16 and then click save the file which gives us this file.</p>
<div class="threejs_center"><img src="../resources/images/identity-lut-s16.png"></div>
<p>We also need to capture an image of the thing we want to apply the LUT to, in this case the scene we created above before applying any effects. Note that normally we could right click on the scene above and pick "Save As..." but the <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> might be preventing right clicking depending on your OS. In my case I used my OSes screen capture feature to get a screenshot.</p>
<div class="threejs_center"><img src="../resources/images/3dlut-screen-capture.jpg" style="width: 600px"></div>
<p>We then go it into an image editor, in my case Photoshop, load up the sample image, and paste the 3DLUT in the top left corner</p>
<blockquote>
<p>note: I first tried dragging and dropping the lut file on top of the image
in Photoshop but that didn't work. Photoshop made the image twice as large.
I'm guessing it was trying to match DPI or something. Loading the lut file
separately and then copying and pasting it into the screen capture worked.</p>
</blockquote>
<div class="threejs_center"><img src="../resources/images/3dlut-photoshop-before.jpg" style="width: 600px"></div>
<p>We then use any of the color based full image adjustments to adjust the image. For Photoshop most of the adjustments we can use are available under the Image-&gt;Adjustments menu.</p>
<div class="threejs_center"><img src="../resources/images/3dlut-photoshop-after.jpg" style="width: 600px"></div>
<p>After we've adjusted the image to our liking you can see the 3DLUT slices we placed in the top left corner have the same adjustments applied.</p>
<p>Okay but how do we use it?</p>
<p>First I saved it as a png <code class="notranslate" translate="no">3dlut-red-only-s16.png</code>. To save memory we could have cropped it to just the 16x256 top left corner of the LUT table but just for fun we'll crop it after loading. The good thing about using this method is we can get some idea of the effective of the LUT just by looking at the .png file. The bad thing is of course wasted bandwidth.</p>
<p>Here's some code to load it. The code starts with an identity lut so the texture is usable immediately. It then loads the image, copies out only the 3DLUT part into a canvas, gets the data from the canvas, set it on the texture and sets <code class="notranslate" translate="no">needsUpdate</code> to true to tell THREE.js to get the new data.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const makeLUTTexture = function() {
const imgLoader = new THREE.ImageLoader();
const ctx = document.createElement('canvas').getContext('2d');
return function(info) {
const lutSize = info.size;
const width = lutSize * lutSize;
const height = lutSize;
const texture = new THREE.DataTexture(new Uint8Array(width * height), width, height);
texture.minFilter = texture.magFilter = info.filter ? THREE.LinearFilter : THREE.NearestFilter;
texture.flipY = false;
if (info.url) {
imgLoader.load(info.url, function(image) {
ctx.canvas.width = width;
ctx.canvas.height = height;
ctx.drawImage(image, 0, 0);
const imageData = ctx.getImageData(0, 0, width, height);
texture.image.data = new Uint8Array(imageData.data.buffer);
texture.image.width = width;
texture.image.height = height;
texture.needsUpdate = true;
});
}
return texture;
};
}();
</pre>
<p>Let's use it to load the lut png we just created.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lutTextures = [
{ name: 'identity', size: 2, filter: true , },
{ name: 'identity no filter', size: 2, filter: false, },
+ { name: 'custom', url: 'resources/images/lut/3dlut-red-only-s16.png' },
];
+lutTextures.forEach((info) =&gt; {
+ // if not size set get it from the filename
+ if (!info.size) {
+ // assumes filename ends in '-s&lt;num&gt;[n]'
+ // where &lt;num&gt; is the size of the 3DLUT cube
+ // and [n] means 'no filtering' or 'nearest'
+ //
+ // examples:
+ // 'foo-s16.png' = size:16, filter: true
+ // 'bar-s8n.png' = size:8, filter: false
+ const m = /-s(\d+)(n*)\.[^.]+$/.exec(info.url);
+ if (m) {
+ info.size = parseInt(m[1]);
+ info.filter = info.filter === undefined ? m[2] !== 'n' : info.filter;
+ }
+ }
+
+ info.texture = makeLUTTexture(info);
+});
</pre>
<p>Above you can see we encoded the size of the LUT into the end of the filename. This makes it easier to pass around LUTs as pngs.</p>
<p>While we're at it lets add a bunch more existing lut png files.</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">const lutTextures = [
{ name: 'identity', size: 2, filter: true , },
{ name: 'identity no filter', size: 2, filter: false, },
{ name: 'custom', url: 'resources/images/lut/3dlut-red-only-s16.png' },
+ { name: 'monochrome', url: 'resources/images/lut/monochrome-s8.png' },
+ { name: 'sepia', url: 'resources/images/lut/sepia-s8.png' },
+ { name: 'saturated', url: 'resources/images/lut/saturated-s8.png', },
+ { name: 'posterize', url: 'resources/images/lut/posterize-s8n.png', },
+ { name: 'posterize-3-rgb', url: 'resources/images/lut/posterize-3-rgb-s8n.png', },
+ { name: 'posterize-3-lab', url: 'resources/images/lut/posterize-3-lab-s8n.png', },
+ { name: 'posterize-4-lab', url: 'resources/images/lut/posterize-4-lab-s8n.png', },
+ { name: 'posterize-more', url: 'resources/images/lut/posterize-more-s8n.png', },
+ { name: 'inverse', url: 'resources/images/lut/inverse-s8.png', },
+ { name: 'color negative', url: 'resources/images/lut/color-negative-s8.png', },
+ { name: 'high contrast', url: 'resources/images/lut/high-contrast-bw-s8.png', },
+ { name: 'funky contrast', url: 'resources/images/lut/funky-contrast-s8.png', },
+ { name: 'nightvision', url: 'resources/images/lut/nightvision-s8.png', },
+ { name: 'thermal', url: 'resources/images/lut/thermal-s8.png', },
+ { name: 'b/w', url: 'resources/images/lut/black-white-s8n.png', },
+ { name: 'hue +60', url: 'resources/images/lut/hue-plus-60-s8.png', },
+ { name: 'hue +180', url: 'resources/images/lut/hue-plus-180-s8.png', },
+ { name: 'hue -60', url: 'resources/images/lut/hue-minus-60-s8.png', },
+ { name: 'red to cyan', url: 'resources/images/lut/red-to-cyan-s8.png' },
+ { name: 'blues', url: 'resources/images/lut/blues-s8.png' },
+ { name: 'infrared', url: 'resources/images/lut/infrared-s8.png' },
+ { name: 'radioactive', url: 'resources/images/lut/radioactive-s8.png' },
+ { name: 'goolgey', url: 'resources/images/lut/googley-s8.png' },
+ { name: 'bgy', url: 'resources/images/lut/bgy-s8.png' },
];
</pre>
<p>And here's a bunch of luts to choose from.</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-3dlut.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/postprocessing-3dlut.html" target="_blank">click here to open in a separate window</a>
</div>
<p></p>
<p>One last thing, just for fun, it turns out there's a standard LUT format defined by Adobe. If you <a href="https://www.google.com/search?q=lut+files">search on the net you can find lots of these LUT files</a>.</p>
<p>I wrote a quick loader. Unfortunately there's 4 variations of the format but I could only find examples of 1 variation so I couldn't easily test that all variations work.</p>
<p>I also wrote a quick drag and drop library. Let's use both to make it so you can drag and drop an Adobe LUT file to see it take affect.</p>
<p>First we need the 2 libraries</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as lutParser from './resources/lut-reader.js';
import * as dragAndDrop from './resources/drag-and-drop.js';
</pre>
<p>Then we can use them like this</p>
<pre class="prettyprint showlinemods notranslate lang-js" translate="no">dragAndDrop.setup({msg: 'Drop LUT File here'});
dragAndDrop.onDropFile(readLUTFile);
function ext(s) {
const period = s.lastIndexOf('.');
return s.slice(period + 1);
}
function readLUTFile(file) {
const reader = new FileReader();
reader.onload = (e) =&gt; {
const type = ext(file.name);
const lut = lutParser.lutTo2D3Drgba8(lutParser.parse(e.target.result, type));
const {size, data, name} = lut;
const texture = new THREE.DataTexture(data, size * size, size);
texture.minFilter = THREE.LinearFilter;
texture.needsUpdate = true;
texture.flipY = false;
const lutTexture = {
name: (name &amp;&amp; name.toLowerCase().trim() !== 'untitled')
? name
: file.name,
size: size,
filter: true,
texture,
};
lutTextures.push(lutTexture);
lutSettings.lut = lutTextures.length - 1;
updateGUI();
};
reader.readAsText(file);
}
</pre>
<p>so you should be able to <a href="https://www.google.com/search?q=lut+files">download an Adobe LUT</a> and then drag and drop it on the example below.</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-3dlut-w-loader.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/postprocessing-3dlut-w-loader.html" target="_blank">click here to open in a separate window</a>
</div>
<p></p>
<p>Note that Adobe LUTs are not designed for online usage. They are large files. You can convert them to smaller files and save as our PNG format by dragging and dropping on the sample below, choosing a size and clicking "Save...".</p>
<p>The sample below is just a modification of the code above. We only draw the background picture, no glTF file. That picture is an identity lut image created from the script above. We then use the effect to apply whatever LUT file is loaded so the result is the image we'd need to reproduce the LUT file as a PNG.</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-adobe-lut-to-png-converter.html"></iframe></div>
<a class="threejs_center" href="/manual/examples/postprocessing-adobe-lut-to-png-converter.html" target="_blank">click here to open in a separate window</a>
</div>
<p></p>
<p>One thing completely skipped is how the shader itself works. Hopefully we can cover a little more GLSL in the future. For now, if you're curious, you can follow the links in the <a href="post-processing.html">post processing article</a> as well as maybe <a href="https://www.youtube.com/watch?v=rfQ8rKGTVlg#t=24m30s">take a look at this video</a>.</p>
<script type="module" src="../resources/threejs-post-processing-3dlut.js"></script>
</div>
</div>
</div>
<script src="/manual/resources/prettify.js"></script>
<script src="/manual/resources/lesson.js"></script>
</body></html>