Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node: Add Lut3DNode. #28779

Merged
merged 2 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@
"webgpu_pmrem_equirectangular",
"webgpu_pmrem_scene",
"webgpu_portal",
"webgpu_postprocessing_3dlut",
"webgpu_postprocessing_afterimage",
"webgpu_postprocessing_anamorphic",
"webgpu_postprocessing_dof",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
195 changes: 195 additions & 0 deletions examples/webgpu_postprocessing_3dlut.html
Original file line number Diff line number Diff line change
@@ -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();
sunag marked this conversation as resolved.
Show resolved Hide resolved

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 change: 1 addition & 0 deletions src/nodes/Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
53 changes: 53 additions & 0 deletions src/nodes/display/Lut3DNode.js
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions test/e2e/puppeteer.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const exceptionList = [

// WebGPURenderer: Unknown problem
'webgpu_postprocessing_afterimage',
'webgpu_postprocessing_3dlut',
Copy link
Collaborator

@sunag sunag Jul 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I added the previous builds was because it was failing in e2e. So exceptions would not be necessary. One way to solve this would be to split the PR between API and examples, I'm not sure if puppeteer did the builds at test time in three.module.js? If yes, I think that it could be done in three.webgpu.js too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if puppeteer did the builds at test time in three.module.js?

The CI executes a build here before running the E2E tests:

run: npm run build-module

But it seems the command only updates three.module.js.

Copy link
Collaborator

@sunag sunag Jul 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could add it rollup.config.js?

export default ( args ) => args.configOnlyModule ? [ builds[ 0 ], builds[ 3 ] ] : builds;

export default ( args ) => args.configOnlyModule ? builds[ 0 ] : builds;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

'webgpu_backdrop_water',
'webgpu_camera_logarithmicdepthbuffer',
'webgpu_clipping',
Expand Down