<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Three.js - postprocessing - 3DLUT</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <style>
    html, body {
        margin: 0;
        height: 100%;
    }
    #c {
        width: 100%;
        height: 100%;
        display: block;
    }
  </style>
  </head>
  <body>
    <canvas id="c"></canvas>
  </body>
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>

<script type="importmap">
{
  "imports": {
    "three": "../../build/three.module.js",
    "three/addons/": "../../examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

function main() {

	const canvas = document.querySelector( '#c' );
	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );

	const fov = 45;
	const aspect = 2; // the canvas default
	const near = 0.1;
	const far = 100;
	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
	camera.position.set( 0, 10, 20 );

	const controls = new OrbitControls( camera, canvas );
	controls.target.set( 0, 5, 0 );
	controls.update();

	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' },
	];

	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 );
			texture.minFilter = texture.magFilter = filter ? THREE.LinearFilter : THREE.NearestFilter;
			texture.needsUpdate = true;
			texture.flipY = false;
			return texture;

		};

	}();

	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;

		};

	}();

	lutTextures.forEach( ( info ) => {

		// if not size set get it from the filename
		if ( ! info.size ) {

			// assumes filename ends in '-s<num>[n]'
			// where <num> 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 );

		} else {

			info.texture = makeIdentityLutTexture( info.filter );

		}

	} );

	const lutNameIndexMap = {};
	lutTextures.forEach( ( info, ndx ) => {

		lutNameIndexMap[ info.name ] = ndx;

	} );

	const lutSettings = {
		lut: lutNameIndexMap.custom,
	};
	const gui = new GUI( { width: 300 } );
	gui.add( lutSettings, 'lut', lutNameIndexMap );

	const scene = new THREE.Scene();

	const sceneBG = new THREE.Scene();
	const cameraBG = new THREE.OrthographicCamera( - 1, 1, 1, - 1, - 1, 1 );

	let bgMesh;
	let bgTexture;
	{

		const loader = new THREE.TextureLoader();
		bgTexture = loader.load( 'resources/images/beach.jpg' );
		bgTexture.colorSpace = THREE.SRGBColorSpace;
		const planeGeo = new THREE.PlaneGeometry( 2, 2 );
		const planeMat = new THREE.MeshBasicMaterial( {
			map: bgTexture,
			depthTest: false,
		} );
		bgMesh = new THREE.Mesh( planeGeo, planeMat );
		sceneBG.add( bgMesh );

	}

	function frameArea( sizeToFitOnScreen, boxSize, boxCenter, camera ) {

		const halfSizeToFitOnScreen = sizeToFitOnScreen * 0.5;
		const halfFovY = THREE.MathUtils.degToRad( camera.fov * .5 );
		const distance = halfSizeToFitOnScreen / Math.tan( halfFovY );
		// compute a unit vector that points in the direction the camera is now
		// in the xz plane from the center of the box
		const direction = ( new THREE.Vector3() )
			.subVectors( camera.position, boxCenter )
			.multiply( new THREE.Vector3( 1, 0, 1 ) )
			.normalize();

		// move the camera to a position distance units way from the center
		// in whatever direction the camera was from the center already
		camera.position.copy( direction.multiplyScalar( distance ).add( boxCenter ) );

		// pick some near and far values for the frustum that
		// will contain the box.
		camera.near = boxSize / 100;
		camera.far = boxSize * 100;

		camera.updateProjectionMatrix();

		// point the camera to look at the center of the box
		camera.lookAt( boxCenter.x, boxCenter.y, boxCenter.z );

	}

	{

		const gltfLoader = new GLTFLoader();
		gltfLoader.load( 'resources/models/3dbustchallange_submission/scene.gltf', ( gltf ) => {

			const root = gltf.scene;
			scene.add( root );

			// fix materials from r114
			root.traverse( ( { material } ) => {

				if ( material ) {

					material.depthWrite = true;

				}

			} );

			root.updateMatrixWorld();
			// compute the box that contains all the stuff
			// from root and below
			const box = new THREE.Box3().setFromObject( root );

			const boxSize = box.getSize( new THREE.Vector3() ).length();
			const boxCenter = box.getCenter( new THREE.Vector3() );
			frameArea( boxSize * 0.4, boxSize, boxCenter, camera );

			// update the Trackball controls to handle the new size
			controls.maxDistance = boxSize * 10;
			controls.target.copy( boxCenter );
			controls.update();

		} );

	}

	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 <common>

      #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', '//' ),
	};

	const effectLUT = new ShaderPass( lutShader );
	const effectLUTNearest = new ShaderPass( lutNearestShader );

	const renderModel = new RenderPass( scene, camera );
	renderModel.clear = false; // so we don't clear out the background
	const renderBG = new RenderPass( sceneBG, cameraBG );
	const outputPass = new OutputPass();

	const composer = new EffectComposer( renderer );

	composer.addPass( renderBG );
	composer.addPass( renderModel );
	composer.addPass( effectLUT );
	composer.addPass( effectLUTNearest );
	composer.addPass( outputPass );

	function resizeRendererToDisplaySize( renderer ) {

		const canvas = renderer.domElement;
		const width = canvas.clientWidth * window.devicePixelRatio | 0;
		const height = canvas.clientHeight * window.devicePixelRatio | 0;

		const needResize = canvas.width !== width || canvas.height !== height;
		if ( needResize ) {

			renderer.setSize( width, height, false );

		}

		return needResize;

	}

	let then = 0;
	function render( now ) {

		now *= 0.001; // convert to seconds
		const delta = now - then;
		then = now;

		if ( resizeRendererToDisplaySize( renderer ) ) {

			const canvas = renderer.domElement;
			const canvasAspect = canvas.clientWidth / canvas.clientHeight;
			camera.aspect = canvasAspect;
			camera.updateProjectionMatrix();
			composer.setSize( canvas.width, canvas.height );

			// scale the background plane to keep the image's
			// aspect correct.
			// Note the image may not have loaded yet.
			const imageAspect = bgTexture.image ? bgTexture.image.width / bgTexture.image.height : 1;
			const aspect = imageAspect / canvasAspect;
			bgMesh.scale.x = aspect > 1 ? aspect : 1;
			bgMesh.scale.y = aspect > 1 ? 1 : 1 / aspect;

		}

		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 );

		requestAnimationFrame( render );

	}

	requestAnimationFrame( render );

}

main();
</script>
</html>