ソースを参照

Node: Add `Lut3DNode`. (#28779)

* Node: Add `Lut3DNode`.

* E2E: Add example to exception list.
Michael Herzog 1 年間 前
コミット
570f72713f

+ 1 - 0
examples/files.json

@@ -367,6 +367,7 @@
 		"webgpu_pmrem_equirectangular",
 		"webgpu_pmrem_scene",
 		"webgpu_portal",
+		"webgpu_postprocessing_3dlut",
 		"webgpu_postprocessing_afterimage",
 		"webgpu_postprocessing_anamorphic",
 		"webgpu_postprocessing_dof",

BIN
examples/screenshots/webgpu_postprocessing_3dlut.jpg


+ 195 - 0
examples/webgpu_postprocessing_3dlut.html

@@ -0,0 +1,195 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - 3d luts</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+
+	<body>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - 3D LUTs<br />
+			Battle Damaged Sci-fi Helmet by
+			<a href="https://sketchfab.com/theblueturtle_" target="_blank" rel="noopener">theblueturtle_</a><br />
+			<a href="https://hdrihaven.com/hdri/?h=royal_esplanade" target="_blank" rel="noopener">Royal Esplanade</a> from <a href="https://hdrihaven.com/" target="_blank" rel="noopener">HDRI Haven</a><br />
+			LUTs from <a href="https://www.rocketstock.com/free-after-effects-templates/35-free-luts-for-color-grading-videos/">RocketStock</a>, <a href="https://www.freepresets.com/product/free-luts-cinematic/">FreePresets.com</a>
+		</div>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.webgpu.js",
+					"three/tsl": "../build/three.webgpu.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+			import * as THREE from 'three';
+			import { pass, texture3D, uniform } from 'three/tsl';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
+			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
+			import { LUTCubeLoader } from 'three/addons/loaders/LUTCubeLoader.js';
+			import { LUT3dlLoader } from 'three/addons/loaders/LUT3dlLoader.js';
+			import { LUTImageLoader } from 'three/addons/loaders/LUTImageLoader.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			const params = {
+				lut: 'Bourbon 64.CUBE',
+				intensity: 1
+			};
+
+			const lutMap = {
+				'Bourbon 64.CUBE': null,
+				'Chemical 168.CUBE': null,
+				'Clayton 33.CUBE': null,
+				'Cubicle 99.CUBE': null,
+				'Remy 24.CUBE': null,
+				'Presetpro-Cinematic.3dl': null,
+				'NeutralLUT': null,
+				'B&WLUT': null,
+				'NightLUT': null
+			};
+
+			let gui;
+			let camera, scene, renderer;
+			let postProcessing, lutPass;
+
+			init();
+
+			async function init() {
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 20 );
+				camera.position.set( - 1.8, 0.6, 2.7 );
+
+				scene = new THREE.Scene();
+
+				new RGBELoader()
+					.setPath( 'textures/equirectangular/' )
+					.load( 'royal_esplanade_1k.hdr', function ( texture ) {
+
+						texture.mapping = THREE.EquirectangularReflectionMapping;
+
+						scene.background = texture;
+						scene.environment = texture;
+
+						// model
+
+						const loader = new GLTFLoader().setPath( 'models/gltf/DamagedHelmet/glTF/' );
+						loader.load( 'DamagedHelmet.gltf', function ( gltf ) {
+
+							scene.add( gltf.scene );
+
+						} );
+
+					} );
+
+				const lutCubeLoader = new LUTCubeLoader();
+				const lutImageLoader = new LUTImageLoader();
+				const lut3dlLoader = new LUT3dlLoader();
+
+				for ( const name in lutMap ) {
+
+					if ( /\.CUBE$/i.test( name ) ) {
+
+						lutMap[ name ] = lutCubeLoader.loadAsync( 'luts/' + name );
+
+					} else if ( /\LUT$/i.test( name ) ) {
+			
+						lutMap[ name ] = lutImageLoader.loadAsync( `luts/${name}.png` );
+			
+					} else {
+
+						lutMap[ name ] = lut3dlLoader.loadAsync( 'luts/' + name );
+
+					}
+
+				}
+
+				const pendings = Object.values( lutMap );
+				await Promise.all( pendings );
+
+				for ( const name in lutMap ) {
+
+					lutMap[ name ] = await lutMap[ name ];
+
+				}
+
+				renderer = new THREE.WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.setAnimationLoop( animate );
+				renderer.toneMapping = THREE.NoToneMapping;
+				renderer.outputColorSpace = THREE.LinearSRGBColorSpace;
+				container.appendChild( renderer.domElement );
+
+				// postprocessing
+
+				postProcessing = new THREE.PostProcessing( renderer );
+
+				const scenePass = pass( scene, camera );
+				const scenePassColor = scenePass.getTextureNode();
+
+				const outputPass = scenePassColor.toneMapping( THREE.ACESFilmicToneMapping ).linearTosRGB();
+
+				lutPass = outputPass.lut3D();
+				lutPass.lutNode = texture3D( lutMap[ params.lut ] );
+				lutPass.intensityNode = uniform( 1 );
+
+				postProcessing.outputNode = lutPass;
+
+				//
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 2;
+				controls.maxDistance = 10;
+				controls.target.set( 0, 0, - 0.2 );
+				controls.update();
+
+				gui = new GUI();
+				gui.width = 350;
+				gui.add( params, 'lut', Object.keys( lutMap ) );
+				gui.add( params, 'intensity' ).min( 0 ).max( 1 );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				lutPass.intensityNode.value = params.intensity;
+
+				if ( lutMap[ params.lut ] ) {
+
+					const lut = lutMap[ params.lut ];
+					lutPass.lutNode.value = lut.texture3D;
+					lutPass.size.value = lut.texture3D.image.width;
+
+				}
+
+				postProcessing.render();
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1 - 0
src/nodes/Nodes.js

@@ -132,6 +132,7 @@ export { default as DepthOfFieldNode, dof } from './display/DepthOfFieldNode.js'
 export { default as DotScreenNode, dotScreen } from './display/DotScreenNode.js';
 export { default as RGBShiftNode, rgbShift } from './display/RGBShiftNode.js';
 export { default as FilmNode, film } from './display/FilmNode.js';
+export { default as Lut3DNode, lut3D } from './display/Lut3DNode.js';
 
 export { default as PassNode, pass, texturePass, depthPass } from './display/PassNode.js';
 

+ 53 - 0
src/nodes/display/Lut3DNode.js

@@ -0,0 +1,53 @@
+import TempNode from '../core/TempNode.js';
+import { addNodeElement, tslFn, nodeObject, vec3, vec4, float } from '../shadernode/ShaderNode.js';
+import { uniform } from '../core/UniformNode.js';
+import { mix } from '../math/MathNode.js';
+
+class Lut3DNode extends TempNode {
+
+	constructor( inputNode, lutNode, size, intensityNode ) {
+
+		super();
+
+		this.inputNode = inputNode;
+		this.lutNode = lutNode;
+		this.size = uniform( size );
+		this.intensityNode = intensityNode;
+
+	}
+
+	setup() {
+
+		const { inputNode, lutNode } = this;
+
+		const sampleLut = ( uv ) => lutNode.uv( uv );
+
+		const lut3D = tslFn( () => {
+
+			const base = inputNode;
+
+			// pull the sample in by half a pixel so the sample begins at the center of the edge pixels.
+
+			const pixelWidth = float( 1.0 ).div( this.size );
+			const halfPixelWidth = float( 0.5 ).div( this.size );
+			const uvw = vec3( halfPixelWidth ).add( base.rgb.mul( float( 1.0 ).sub( pixelWidth ) ) );
+
+			const lutValue = vec4( sampleLut( uvw ).rgb, base.a );
+
+			return vec4( mix( base, lutValue, this.intensityNode ) );
+
+		} );
+
+		const outputNode = lut3D();
+
+		return outputNode;
+
+	}
+
+}
+
+export const lut3D = ( node, lut, size, intensity ) => nodeObject( new Lut3DNode( nodeObject( node ), nodeObject( lut ), size, nodeObject( intensity ) ) );
+
+addNodeElement( 'lut3D', lut3D );
+
+export default Lut3DNode;

+ 1 - 0
test/e2e/puppeteer.js

@@ -124,6 +124,7 @@ const exceptionList = [
 
 	// WebGPURenderer: Unknown problem
 	'webgpu_postprocessing_afterimage',
+	'webgpu_postprocessing_3dlut',
 	'webgpu_backdrop_water',
 	'webgpu_camera_logarithmicdepthbuffer',
 	'webgpu_clipping',