Sfoglia il codice sorgente

LUT Loaders: Add support for FloatType and add docs (#27431)

* Update LUT loaders

* Add docs for LUT loaders

* Fix lint issues

* Update list.json

* Add module name to error messages

* Remove unnecessary code

* Update comment
Raoul v. R 1 anno fa
parent
commit
60ee95f25d

+ 84 - 0
docs/examples/en/loaders/LUT3dlLoader.html

@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<base href="../../../" />
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+		[page:Loader] &rarr;
+
+		<h1>[name]</h1>
+
+		<p class="desc">
+			A 3D LUT loader that supports the .3dl file format.<br />
+			Based on the following references:
+		</p>
+
+		<ul>
+			<li>[link:http://download.autodesk.com/us/systemdocs/help/2011/lustre/index.html?url=./files/WSc4e151a45a3b785a24c3d9a411df9298473-7ffd.htm,topicNumber=d0e9492]</li>
+			<li>[link:https://community.foundry.com/discuss/topic/103636/format-spec-for-3dl?mode=Post&postID=895258]</li>
+		</ul>
+
+		<h2>Import</h2>
+
+		<p>
+			[name] is an add-on, and must be imported explicitly.
+			See [link:#manual/introduction/Installation Installation / Addons].
+		</p>
+
+		<code>
+			import { LUT3dlLoader } from 'three/addons/loaders/LUT3dlLoader.js';
+		</code>
+
+		<h2>Constructor</h2>
+
+		<h3>[name]( [param:LoadingManager manager] )</h3>
+		<p>
+			[page:LoadingManager manager] — The LoadingManager to use. Defaults to [page:DefaultLoadingManager DefaultLoadingManager]<br />
+		</p>
+		<p>
+			Creates a new [name].
+		</p>
+
+		<h2>Properties</h2>
+		<p>See the base [page:Loader] class for common properties.</p>
+
+		<h2>Methods</h2>
+		<p>See the base [page:Loader] class for common methods.</p>
+
+		<h3>[method:undefined load]( [param:String url], [param:Function onLoad], [param:Function onProgress], [param:Function onError] )</h3>
+		<p>
+			[page:String url] — A string containing the path/URL of the `.3dl` file.<br />
+			[page:Function onLoad] — (optional) A function to be called after the loading is successfully completed. The function receives the result of the [page:Function parse] method.<br />
+			[page:Function onProgress] — (optional) A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains [page:Integer total] and [page:Integer loaded] bytes. If the server does not set the Content-Length header; .[page:Integer total] will be 0.<br />
+			[page:Function onError] — (optional) A function to be called if an error occurs during loading. The function receives the error as an argument.<br />
+		</p>
+		<p>
+			Begin loading from url and return the loaded LUT.
+		</p>
+
+		<h3>[method:Object parse]( [param:String input] )</h3>
+		<p>
+			[page:String input] — The 3dl data string.<br />
+		</p>
+		<p>
+			Parse a 3dl data string and fire [page:Function onLoad] callback when complete. The argument to [page:Function onLoad] will be an [page:Object object] containing the following LUT data: [page:Number .size], [page:DataTexture .texture] and [page:Data3DTexture .texture3D].
+		</p>
+
+		<h3>[method:this setType]( [param:Number type] )</h3>
+		<p>
+			[page:Number type] - The texture type. See the [page:Textures texture constants] page for details.<br />
+		</p>
+		<p>
+			Sets the desired texture type. Only [page:Textures THREE.UnsignedByteType] and [page:Textures THREE.FloatType] are supported. The default is [page:Textures THREE.UnsignedByteType].
+		</p>
+
+		<h2>Source</h2>
+
+		<p>
+			[link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/[name].js examples/jsm/loaders/[name].js]
+		</p>
+	</body>
+</html>

+ 83 - 0
docs/examples/en/loaders/LUTCubeLoader.html

@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<meta charset="utf-8" />
+		<base href="../../../" />
+		<script src="page.js"></script>
+		<link type="text/css" rel="stylesheet" href="page.css" />
+	</head>
+	<body>
+		[page:Loader] &rarr;
+
+		<h1>[name]</h1>
+
+		<p class="desc">
+			A 3D LUT loader that supports the .cube file format.<br />
+			Based on the following reference:
+		</p>
+
+		<ul>
+			<li>[link:https://wwwimages2.adobe.com/content/dam/acom/en/products/speedgrade/cc/pdfs/cube-lut-specification-1.0.pdf]</li>
+		</ul>
+
+		<h2>Import</h2>
+
+		<p>
+			[name] is an add-on, and must be imported explicitly.
+			See [link:#manual/introduction/Installation Installation / Addons].
+		</p>
+
+		<code>
+			import { LUTCubeLoader } from 'three/addons/loaders/LUTCubeLoader.js';
+		</code>
+
+		<h2>Constructor</h2>
+
+		<h3>[name]( [param:LoadingManager manager] )</h3>
+		<p>
+			[page:LoadingManager manager] — The LoadingManager to use. Defaults to [page:DefaultLoadingManager DefaultLoadingManager]<br />
+		</p>
+		<p>
+			Creates a new [name].
+		</p>
+
+		<h2>Properties</h2>
+		<p>See the base [page:Loader] class for common properties.</p>
+
+		<h2>Methods</h2>
+		<p>See the base [page:Loader] class for common methods.</p>
+
+		<h3>[method:undefined load]( [param:String url], [param:Function onLoad], [param:Function onProgress], [param:Function onError] )</h3>
+		<p>
+			[page:String url] — A string containing the path/URL of the `.cube` file.<br />
+			[page:Function onLoad] — (optional) A function to be called after the loading is successfully completed. The function receives the result of the [page:Function parse] method.<br />
+			[page:Function onProgress] — (optional) A function to be called while the loading is in progress. The argument will be the XMLHttpRequest instance, which contains [page:Integer total] and [page:Integer loaded] bytes. If the server does not set the Content-Length header; .[page:Integer total] will be 0.<br />
+			[page:Function onError] — (optional) A function to be called if an error occurs during loading. The function receives the error as an argument.<br />
+		</p>
+		<p>
+			Begin loading from url and return the loaded LUT.
+		</p>
+
+		<h3>[method:Object parse]( [param:String input] )</h3>
+		<p>
+			[page:String input] — The cube data string.<br />
+		</p>
+		<p>
+			Parse a cube data string and fire [page:Function onLoad] callback when complete. The argument to [page:Function onLoad] will be an [page:Object object] containing the following LUT data: [page:String .title], [page:Number .size], [page:Vector3 .domainMin], [page:Vector3 .domainMax], [page:DataTexture .texture] and [page:Data3DTexture .texture3D].
+		</p>
+
+		<h3>[method:this setType]( [param:Number type] )</h3>
+		<p>
+			[page:Number type] - The texture type. See the [page:Textures texture constants] page for details.<br />
+		</p>
+		<p>
+			Sets the desired texture type. Only [page:Textures THREE.UnsignedByteType] and [page:Textures THREE.FloatType] are supported. The default is [page:Textures THREE.UnsignedByteType].
+		</p>
+
+		<h2>Source</h2>
+
+		<p>
+			[link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/loaders/[name].js examples/jsm/loaders/[name].js]
+		</p>
+	</body>
+</html>

+ 2 - 0
docs/list.json

@@ -368,6 +368,8 @@
 				"GLTFLoader": "examples/en/loaders/GLTFLoader",
 				"KTX2Loader": "examples/en/loaders/KTX2Loader",
 				"LDrawLoader": "examples/en/loaders/LDrawLoader",
+				"LUT3dlLoader": "examples/en/loaders/LUT3dlLoader",
+				"LUTCubeLoader": "examples/en/loaders/LUTCubeLoader",
 				"MMDLoader": "examples/en/loaders/MMDLoader",
 				"MTLLoader": "examples/en/loaders/MTLLoader",
 				"OBJLoader": "examples/en/loaders/OBJLoader",

+ 78 - 46
examples/jsm/loaders/LUT3dlLoader.js

@@ -1,18 +1,42 @@
 // http://download.autodesk.com/us/systemdocs/help/2011/lustre/index.html?url=./files/WSc4e151a45a3b785a24c3d9a411df9298473-7ffd.htm,topicNumber=d0e9492
 // https://community.foundry.com/discuss/topic/103636/format-spec-for-3dl?mode=Post&postID=895258
+
 import {
-	Loader,
-	FileLoader,
+	ClampToEdgeWrapping,
 	DataTexture,
 	Data3DTexture,
+	FileLoader,
+	FloatType,
+	LinearFilter,
+	Loader,
 	RGBAFormat,
 	UnsignedByteType,
-	ClampToEdgeWrapping,
-	LinearFilter,
 } from 'three';
 
 export class LUT3dlLoader extends Loader {
 
+	constructor( manager ) {
+
+		super( manager );
+
+		this.type = UnsignedByteType;
+
+	}
+
+	setType( type ) {
+
+		if ( type !== UnsignedByteType && type !== FloatType ) {
+
+			throw new Error( 'LUT3dlLoader: Unsupported type' );
+
+		}
+
+		this.type = type;
+
+		return this;
+
+	}
+
 	load( url, onLoad, onProgress, onError ) {
 
 		const loader = new FileLoader( this.manager );
@@ -44,80 +68,88 @@ export class LUT3dlLoader extends Loader {
 
 	}
 
-	parse( str ) {
+	parse( input ) {
+
+		const regExpGridInfo = /^[\d ]+$/m;
+		const regExpDataPoints = /^([\d.e+-]+) +([\d.e+-]+) +([\d.e+-]+) *$/gm;
 
-		// remove empty lines and comment lints
-		str = str
-			.replace( /^#.*?(\n|\r)/gm, '' )
-			.replace( /^\s*?(\n|\r)/gm, '' )
-			.trim();
+		// The first line describes the positions of values on the LUT grid.
+		let result = regExpGridInfo.exec( input );
 
-		const lines = str.split( /[\n\r]+/g );
+		if ( result === null ) {
 
-		// first line is the positions on the grid that are provided by the LUT
-		const gridLines = lines[ 0 ].trim().split( /\s+/g ).map( e => parseFloat( e ) );
+			throw new Error( 'LUT3dlLoader: Missing grid information' );
+
+		}
+
+		const gridLines = result[ 0 ].trim().split( /\s+/g ).map( Number );
 		const gridStep = gridLines[ 1 ] - gridLines[ 0 ];
 		const size = gridLines.length;
+		const sizeSq = size ** 2;
 
-		for ( let i = 1, l = gridLines.length; i < l; i ++ ) {
+		for ( let i = 1, l = gridLines.length; i < l; ++ i ) {
 
 			if ( gridStep !== ( gridLines[ i ] - gridLines[ i - 1 ] ) ) {
 
-				throw new Error( 'LUT3dlLoader: Inconsistent grid size not supported.' );
+				throw new Error( 'LUT3dlLoader: Inconsistent grid size' );
 
 			}
 
 		}
 
-		const dataArray = new Array( size * size * size * 4 );
+		const dataFloat = new Float32Array( size ** 3 * 4 );
+		let maxValue = 0.0;
 		let index = 0;
-		let maxOutputValue = 0.0;
-		for ( let i = 1, l = lines.length; i < l; i ++ ) {
 
-			const line = lines[ i ].trim();
-			const split = line.split( /\s/g );
+		while ( ( result = regExpDataPoints.exec( input ) ) !== null ) {
+
+			const r = Number( result[ 1 ] );
+			const g = Number( result[ 2 ] );
+			const b = Number( result[ 3 ] );
 
-			const r = parseFloat( split[ 0 ] );
-			const g = parseFloat( split[ 1 ] );
-			const b = parseFloat( split[ 2 ] );
-			maxOutputValue = Math.max( maxOutputValue, r, g, b );
+			maxValue = Math.max( maxValue, r, g, b );
 
 			const bLayer = index % size;
 			const gLayer = Math.floor( index / size ) % size;
-			const rLayer = Math.floor( index / ( size * size ) ) % size;
+			const rLayer = Math.floor( index / ( sizeSq ) ) % size;
 
-			// b grows first, then g, then r
-			const pixelIndex = bLayer * size * size + gLayer * size + rLayer;
-			dataArray[ 4 * pixelIndex + 0 ] = r;
-			dataArray[ 4 * pixelIndex + 1 ] = g;
-			dataArray[ 4 * pixelIndex + 2 ] = b;
-			dataArray[ 4 * pixelIndex + 3 ] = 1.0;
-			index += 1;
+			// b grows first, then g, then r.
+			const d4 = ( bLayer * sizeSq + gLayer * size + rLayer ) * 4;
+			dataFloat[ d4 + 0 ] = r;
+			dataFloat[ d4 + 1 ] = g;
+			dataFloat[ d4 + 2 ] = b;
+
+			++ index;
 
 		}
 
-		// Find the apparent bit depth of the stored RGB values and map the
-		// values to [ 0, 255 ].
-		const bits = Math.ceil( Math.log2( maxOutputValue ) );
-		const maxBitValue = Math.pow( 2.0, bits );
-		for ( let i = 0, l = dataArray.length; i < l; i += 4 ) {
+		// Determine the bit depth to scale the values to [0.0, 1.0].
+		const bits = Math.ceil( Math.log2( maxValue ) );
+		const maxBitValue = Math.pow( 2, bits );
+
+		const data = this.type === UnsignedByteType ? new Uint8Array( dataFloat.length ) : dataFloat;
+		const scale = this.type === UnsignedByteType ? 255 : 1;
+
+		for ( let i = 0, l = data.length; i < l; i += 4 ) {
+
+			const i1 = i + 1;
+			const i2 = i + 2;
+			const i3 = i + 3;
 
-			const r = dataArray[ i + 0 ];
-			const g = dataArray[ i + 1 ];
-			const b = dataArray[ i + 2 ];
-			dataArray[ i + 0 ] = 255 * r / maxBitValue; // r
-			dataArray[ i + 1 ] = 255 * g / maxBitValue; // g
-			dataArray[ i + 2 ] = 255 * b / maxBitValue; // b
+			// Note: data is dataFloat when type is FloatType.
+			data[ i ] = dataFloat[ i ] / maxBitValue * scale;
+			data[ i1 ] = dataFloat[ i1 ] / maxBitValue * scale;
+			data[ i2 ] = dataFloat[ i2 ] / maxBitValue * scale;
+			data[ i3 ] = scale;
 
 		}
 
-		const data = new Uint8Array( dataArray );
 		const texture = new DataTexture();
 		texture.image.data = data;
 		texture.image.width = size;
 		texture.image.height = size * size;
 		texture.format = RGBAFormat;
-		texture.type = UnsignedByteType;
+		texture.type = this.type;
 		texture.magFilter = LinearFilter;
 		texture.minFilter = LinearFilter;
 		texture.wrapS = ClampToEdgeWrapping;
@@ -131,7 +163,7 @@ export class LUT3dlLoader extends Loader {
 		texture3D.image.height = size;
 		texture3D.image.depth = size;
 		texture3D.format = RGBAFormat;
-		texture3D.type = UnsignedByteType;
+		texture3D.type = this.type;
 		texture3D.magFilter = LinearFilter;
 		texture3D.minFilter = LinearFilter;
 		texture3D.wrapS = ClampToEdgeWrapping;

+ 81 - 67
examples/jsm/loaders/LUTCubeLoader.js

@@ -1,18 +1,41 @@
 // https://wwwimages2.adobe.com/content/dam/acom/en/products/speedgrade/cc/pdfs/cube-lut-specification-1.0.pdf
 
 import {
-	Loader,
-	FileLoader,
-	Vector3,
+	ClampToEdgeWrapping,
 	DataTexture,
 	Data3DTexture,
-	UnsignedByteType,
-	ClampToEdgeWrapping,
+	FileLoader,
+	FloatType,
 	LinearFilter,
+	Loader,
+	UnsignedByteType,
+	Vector3,
 } from 'three';
 
 export class LUTCubeLoader extends Loader {
 
+	constructor( manager ) {
+
+		super( manager );
+
+		this.type = UnsignedByteType;
+
+	}
+
+	setType( type ) {
+
+		if ( type !== UnsignedByteType && type !== FloatType ) {
+
+			throw new Error( 'LUTCubeLoader: Unsupported type' );
+
+		}
+
+		this.type = type;
+
+		return this;
+
+	}
+
 	load( url, onLoad, onProgress, onError ) {
 
 		const loader = new FileLoader( this.manager );
@@ -44,72 +67,63 @@ export class LUTCubeLoader extends Loader {
 
 	}
 
-	parse( str ) {
+	parse( input ) {
 
-		// Remove empty lines and comments
-		str = str
-			.replace( /^#.*?(\n|\r)/gm, '' )
-			.replace( /^\s*?(\n|\r)/gm, '' )
-			.trim();
+		const regExpTitle = /TITLE +"([^"]*)"/;
+		const regExpSize = /LUT_3D_SIZE +(\d+)/;
+		const regExpDomainMin = /DOMAIN_MIN +([\d.]+) +([\d.]+) +([\d.]+)/;
+		const regExpDomainMax = /DOMAIN_MAX +([\d.]+) +([\d.]+) +([\d.]+)/;
+		const regExpDataPoints = /^([\d.e+-]+) +([\d.e+-]+) +([\d.e+-]+) *$/gm;
+
+		let result = regExpTitle.exec( input );
+		const title = ( result !== null ) ? result[ 1 ] : null;
+
+		result = regExpSize.exec( input );
+
+		if ( result === null ) {
+
+			throw new Error( 'LUTCubeLoader: Missing LUT_3D_SIZE information' );
+
+		}
+
+		const size = Number( result[ 1 ] );
+		const length = size ** 3 * 4;
+		const data = this.type === UnsignedByteType ? new Uint8Array( length ) : new Float32Array( length );
 
-		let title = null;
-		let size = null;
 		const domainMin = new Vector3( 0, 0, 0 );
 		const domainMax = new Vector3( 1, 1, 1 );
 
-		const lines = str.split( /[\n\r]+/g );
-		let data = null;
-
-		let currIndex = 0;
-		for ( let i = 0, l = lines.length; i < l; i ++ ) {
-
-			const line = lines[ i ].trim();
-			const split = line.split( /\s/g );
-
-			switch ( split[ 0 ] ) {
-
-				case 'TITLE':
-					title = line.substring( 7, line.length - 1 );
-					break;
-				case 'LUT_3D_SIZE':
-					// TODO: A .CUBE LUT file specifies floating point values and could be represented with
-					// more precision than can be captured with Uint8Array.
-					const sizeToken = split[ 1 ];
-					size = parseFloat( sizeToken );
-					data = new Uint8Array( size * size * size * 4 );
-					break;
-				case 'DOMAIN_MIN':
-					domainMin.x = parseFloat( split[ 1 ] );
-					domainMin.y = parseFloat( split[ 2 ] );
-					domainMin.z = parseFloat( split[ 3 ] );
-					break;
-				case 'DOMAIN_MAX':
-					domainMax.x = parseFloat( split[ 1 ] );
-					domainMax.y = parseFloat( split[ 2 ] );
-					domainMax.z = parseFloat( split[ 3 ] );
-					break;
-				default:
-					const r = parseFloat( split[ 0 ] );
-					const g = parseFloat( split[ 1 ] );
-					const b = parseFloat( split[ 2 ] );
-
-					if (
-						r > 1.0 || r < 0.0 ||
-						g > 1.0 || g < 0.0 ||
-						b > 1.0 || b < 0.0
-					) {
-
-						throw new Error( 'LUTCubeLoader : Non normalized values not supported.' );
-
-					}
-
-					data[ currIndex + 0 ] = r * 255;
-					data[ currIndex + 1 ] = g * 255;
-					data[ currIndex + 2 ] = b * 255;
-					data[ currIndex + 3 ] = 255;
-					currIndex += 4;
+		result = regExpDomainMin.exec( input );
 
-			}
+		if ( result !== null ) {
+
+			domainMin.set( Number( result[ 1 ] ), Number( result[ 2 ] ), Number( result[ 3 ] ) );
+
+		}
+
+		result = regExpDomainMax.exec( input );
+
+		if ( result !== null ) {
+
+			domainMax.set( Number( result[ 1 ] ), Number( result[ 2 ] ), Number( result[ 3 ] ) );
+
+		}
+
+		if ( domainMin.x > domainMax.x || domainMin.y > domainMax.y || domainMin.z > domainMax.z ) {
+
+			throw new Error( 'LUTCubeLoader: Invalid input domain' );
+
+		}
+
+		const scale = this.type === UnsignedByteType ? 255 : 1;
+		let i = 0;
+
+		while ( ( result = regExpDataPoints.exec( input ) ) !== null ) {
+
+			data[ i ++ ] = Number( result[ 1 ] ) * scale;
+			data[ i ++ ] = Number( result[ 2 ] ) * scale;
+			data[ i ++ ] = Number( result[ 3 ] ) * scale;
+			data[ i ++ ] = scale;
 
 		}
 
@@ -117,7 +131,7 @@ export class LUTCubeLoader extends Loader {
 		texture.image.data = data;
 		texture.image.width = size;
 		texture.image.height = size * size;
-		texture.type = UnsignedByteType;
+		texture.type = this.type;
 		texture.magFilter = LinearFilter;
 		texture.minFilter = LinearFilter;
 		texture.wrapS = ClampToEdgeWrapping;
@@ -130,7 +144,7 @@ export class LUTCubeLoader extends Loader {
 		texture3D.image.width = size;
 		texture3D.image.height = size;
 		texture3D.image.depth = size;
-		texture3D.type = UnsignedByteType;
+		texture3D.type = this.type;
 		texture3D.magFilter = LinearFilter;
 		texture3D.minFilter = LinearFilter;
 		texture3D.wrapS = ClampToEdgeWrapping;