Prechádzať zdrojové kódy

EXRExporter: initial version (#23541)

* EXRExporter: initial version

Signed-off-by: Guilherme Avila <[email protected]>

* clean up

Signed-off-by: Guilherme Avila <[email protected]>

* EXRExporter: code style

Signed-off-by: Guilherme Avila <[email protected]>

* EXRExporter: minor fixes

Signed-off-by: Guilherme Avila <[email protected]>
Guilherme Avila 3 rokov pred
rodič
commit
5d351cecaa

+ 76 - 0
docs/examples/en/exporters/EXRExporter.html

@@ -0,0 +1,76 @@
+<!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>
+		<h1>[name]</h1>
+
+		<p class="desc">
+		An exporter for *EXR*.
+		<br /><br />
+		<a href="https://www.openexr.com/">EXR</a> ( Extended Dynamic Range) is an
+		<a href="https://github.com/AcademySoftwareFoundation/openexr">open format specification</a>
+		for professional-grade image storage format of the motion picture industry. The purpose of
+		format is to accurately and efficiently represent high-dynamic-range scene-linear image data
+		and associated metadata. The library is widely used in host application software where accuracy
+		is critical, such as photorealistic rendering, texture access, image compositing, deep compositing,
+		and DI.
+		</p>
+
+		<h2>Code Example</h2>
+
+		<code>
+		// Instantiate a exporter
+		const exporter = new EXRExporter();
+
+		// Parse the input render target data and generate the EXR output
+		const EXR = exporter.parse( renderer, renderTarget, options );
+		downloadFile( EXR );
+		</code>
+
+		<h2>Constructor</h2>
+
+		<h3>[name]()</h3>
+		<p>
+		</p>
+		<p>
+		Creates a new [name].
+		</p>
+
+		<h2>Methods</h2>
+
+		<h3>[method:null parse]( [param:WebGLRenderer renderer], [param:WebGLRenderTarget renderTarget], [param:Object options] )</h3>
+		<p>
+			[page:Function renderTarget] — WebGLRenderTarget containing data used for exporting EXR image.<br />
+			[page:Options options] — Export options.<br />
+			<ul>
+				<li>type - Output datatype for internal EXR data. Available options:<br />
+					<code>
+THREE.HalfFloatType // default option
+THREE.FloatType
+					</code>
+				</li>
+				<li>compression - Internal compression algorithm. Available options:<br />
+					<code>
+NO_COMPRESSION
+ZIP_COMPRESSION // default option
+ZIPS_COMPRESSION
+					</code>
+				</li>
+			</ul>
+		</p>
+		<p>
+		Generates a .exr output from the input render target.
+		</p>
+
+		<h2>Source</h2>
+
+		<p>
+			[link:https://github.com/mrdoob/three.js/blob/master/examples/jsm/exporters/EXRExporter.js examples/jsm/exporters/EXRExporter.js]
+		</p>
+	</body>
+</html>

+ 2 - 1
docs/list.json

@@ -390,7 +390,8 @@
 			"Exporters": {
 			"Exporters": {
 				"GLTFExporter": "examples/en/exporters/GLTFExporter",
 				"GLTFExporter": "examples/en/exporters/GLTFExporter",
 				"PLYExporter": "examples/en/exporters/PLYExporter",
 				"PLYExporter": "examples/en/exporters/PLYExporter",
-				"ColladaExporter": "examples/en/exporters/ColladaExporter"
+				"ColladaExporter": "examples/en/exporters/ColladaExporter",
+				"EXRExporter": "examples/en/exporters/EXRExporter"
 			},
 			},
 
 
 			"Math": {
 			"Math": {

+ 507 - 0
examples/jsm/exporters/EXRExporter.js

@@ -0,0 +1,507 @@
+/**
+ * @author sciecode / https://github.com/sciecode
+ *
+ * EXR format references:
+ * 	https://www.openexr.com/documentation/openexrfilelayout.pdf
+ */
+
+import {
+	FloatType,
+	HalfFloatType,
+	RGBAFormat,
+	DataUtils,
+} from 'three';
+import * as fflate from '../libs/fflate.module.js';
+
+const textEncoder = new TextEncoder();
+
+const NO_COMPRESSION = 0;
+const ZIPS_COMPRESSION = 2;
+const ZIP_COMPRESSION = 3;
+
+class EXRExporter {
+
+	parse( renderer, renderTarget, options ) {
+
+		if ( ! supported( renderer, renderTarget ) ) return undefined;
+
+		const info = buildInfo( renderTarget, options ),
+			dataBuffer = getPixelData( renderer, renderTarget, info ),
+			rawContentBuffer = reorganizeDataBuffer( dataBuffer, info ),
+			chunks = compressData( rawContentBuffer, info );
+
+		return fillData( chunks, info );
+
+	}
+
+}
+
+function supported( renderer, renderTarget ) {
+
+	if ( ! renderer || ! renderer.isWebGLRenderer ) {
+
+		console.error( 'EXRExporter.parse: Unsupported first parameter, expected instance of WebGLRenderer.' );
+
+		return false;
+
+	}
+
+	if ( ! renderTarget || ! renderTarget.isWebGLRenderTarget ) {
+
+		console.error( 'EXRExporter.parse: Unsupported second parameter, expected instance of WebGLRenderTarget.' );
+
+		return false;
+
+	}
+
+	if ( renderTarget.texture.type !== FloatType && renderTarget.texture.type !== HalfFloatType ) {
+
+		console.error( 'EXRExporter.parse: Unsupported WebGLRenderTarget texture type.' );
+
+		return false;
+
+	}
+
+	if ( renderTarget.texture.format !== RGBAFormat ) {
+
+		console.error( 'EXRExporter.parse: Unsupported WebGLRenderTarget texture format, expected RGBAFormat.' );
+
+		return false;
+
+	}
+
+
+	return true;
+
+}
+
+function buildInfo( renderTarget, options = {} ) {
+
+	const compressionSizes = {
+		0: 1,
+		2: 1,
+		3: 16
+	};
+
+	const WIDTH = renderTarget.width,
+		HEIGHT = renderTarget.height,
+		TYPE = renderTarget.texture.type,
+		FORMAT = renderTarget.texture.format,
+		ENCODING = renderTarget.texture.encoding,
+		COMPRESSION = ( options.compression !== undefined ) ? options.compression : ZIP_COMPRESSION,
+		EXPORTER_TYPE = ( options.type !== undefined ) ? options.type : HalfFloatType,
+		OUT_TYPE = ( EXPORTER_TYPE === FloatType ) ? 2 : 1,
+		COMPRESSION_SIZE = compressionSizes[ COMPRESSION ],
+		NUM_CHANNELS = 4;
+
+	return {
+		width: WIDTH,
+		height: HEIGHT,
+		type: TYPE,
+		format: FORMAT,
+		encoding: ENCODING,
+		compression: COMPRESSION,
+		blockLines: COMPRESSION_SIZE,
+		dataType: OUT_TYPE,
+		dataSize: 2 * OUT_TYPE,
+		numBlocks: Math.ceil( HEIGHT / COMPRESSION_SIZE ),
+		numInputChannels: 4,
+		numOutputChannels: NUM_CHANNELS,
+	};
+
+}
+
+function getPixelData( renderer, rtt, info ) {
+
+	let dataBuffer;
+
+	if ( info.type === FloatType ) {
+
+		dataBuffer = new Float32Array( info.width * info.height * info.numInputChannels );
+
+	} else {
+
+		dataBuffer = new Uint16Array( info.width * info.height * info.numInputChannels );
+
+	}
+
+	renderer.readRenderTargetPixels( rtt, 0, 0, info.width, info.height, dataBuffer );
+
+	return dataBuffer;
+
+}
+
+function reorganizeDataBuffer( inBuffer, info ) {
+
+	const w = info.width,
+		h = info.height,
+		dec = { r: 0, g: 0, b: 0, a: 0 },
+		offset = { value: 0 },
+		cOffset = ( info.numOutputChannels == 4 ) ? 1 : 0,
+		getValue = ( info.type == FloatType ) ? getFloat32 : getFloat16,
+		setValue = ( info.dataType == 1 ) ? setFloat16 : setFloat32,
+		outBuffer = new Uint8Array( info.width * info.height * info.numOutputChannels * info.dataSize ),
+		dv = new DataView( outBuffer.buffer );
+
+	for ( let y = 0; y < h; ++ y ) {
+
+		for ( let x = 0; x < w; ++ x ) {
+
+			const i = y * w * 4 + x * 4;
+
+			const r = getValue( inBuffer, i );
+			const g = getValue( inBuffer, i + 1 );
+			const b = getValue( inBuffer, i + 2 );
+			const a = getValue( inBuffer, i + 3 );
+
+			const line = ( h - y - 1 ) * w * ( 3 + cOffset ) * info.dataSize;
+
+			decodeLinear( dec, r, g, b, a );
+
+			offset.value = line + x * info.dataSize;
+			setValue( dv, dec.a, offset );
+
+			offset.value = line + ( cOffset ) * w * info.dataSize + x * info.dataSize;
+			setValue( dv, dec.b, offset );
+
+			offset.value = line + ( 1 + cOffset ) * w * info.dataSize + x * info.dataSize;
+			setValue( dv, dec.g, offset );
+
+			offset.value = line + ( 2 + cOffset ) * w * info.dataSize + x * info.dataSize;
+			setValue( dv, dec.r, offset );
+
+		}
+
+	}
+
+	return outBuffer;
+
+}
+
+function compressData( inBuffer, info ) {
+
+	let compress,
+		tmpBuffer,
+		sum = 0;
+
+	const chunks = { data: new Array(), totalSize: 0 },
+		size = info.width * info.numOutputChannels * info.blockLines * info.dataSize;
+
+	switch ( info.compression ) {
+
+		case 0:
+			compress = compressNONE;
+			break;
+
+		case 2:
+		case 3:
+			compress = compressZIP;
+			break;
+
+	}
+
+	if ( info.compression !== 0 ) {
+
+		tmpBuffer = new Uint8Array( size );
+
+	}
+
+	for ( let i = 0; i < info.numBlocks; ++ i ) {
+
+		const arr = inBuffer.subarray( size * i, size * ( i + 1 ) );
+
+		const block = compress( arr, tmpBuffer );
+
+		sum += block.length;
+
+		chunks.data.push( { dataChunk: block, size: block.length } );
+
+	}
+
+	chunks.totalSize = sum;
+
+	return chunks;
+
+}
+
+function compressNONE( data ) {
+
+	return data;
+
+}
+
+function compressZIP( data, tmpBuffer ) {
+
+	//
+	// Reorder the pixel data.
+	//
+
+	let t1 = 0,
+		t2 = Math.floor( ( data.length + 1 ) / 2 ),
+		s = 0;
+
+	const stop = data.length - 1;
+
+	while ( true ) {
+
+		if ( s > stop ) break;
+		tmpBuffer[ t1 ++ ] = data[ s ++ ];
+
+		if ( s > stop ) break;
+		tmpBuffer[ t2 ++ ] = data[ s ++ ];
+
+	}
+
+	//
+	// Predictor.
+	//
+
+	let p = tmpBuffer[ 0 ];
+
+	for ( let t = 1; t < tmpBuffer.length; t ++ ) {
+
+		const d = tmpBuffer[ t ] - p + ( 128 + 256 );
+		p = tmpBuffer[ t ];
+		tmpBuffer[ t ] = d;
+
+	}
+
+	if ( typeof fflate === 'undefined' ) {
+
+		console.error( 'THREE.EXRLoader: External \`fflate.module.js\` required' );
+
+	}
+
+	const deflate = fflate.zlibSync( tmpBuffer ); // eslint-disable-line no-undef
+
+	return deflate;
+
+}
+
+function fillHeader( outBuffer, chunks, info ) {
+
+	const offset = { value: 0 };
+	const dv = new DataView( outBuffer.buffer );
+
+	setUint32( dv, 20000630, offset ); // magic
+	setUint32( dv, 2, offset ); // mask
+
+	// = HEADER =
+
+	setString( dv, 'compression', offset );
+	setString( dv, 'compression', offset );
+	setUint32( dv, 1, offset );
+	setUint8( dv, info.compression, offset );
+
+	setString( dv, 'screenWindowCenter', offset );
+	setString( dv, 'v2f', offset );
+	setUint32( dv, 8, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, 0, offset );
+
+	setString( dv, 'screenWindowWidth', offset );
+	setString( dv, 'float', offset );
+	setUint32( dv, 4, offset );
+	setFloat32( dv, 1.0, offset );
+
+	setString( dv, 'pixelAspectRatio', offset );
+	setString( dv, 'float', offset );
+	setUint32( dv, 4, offset );
+	setFloat32( dv, 1.0, offset );
+
+	setString( dv, 'lineOrder', offset );
+	setString( dv, 'lineOrder', offset );
+	setUint32( dv, 1, offset );
+	setUint8( dv, 0, offset );
+
+	setString( dv, 'dataWindow', offset );
+	setString( dv, 'box2i', offset );
+	setUint32( dv, 16, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, info.width - 1, offset );
+	setUint32( dv, info.height - 1, offset );
+
+	setString( dv, 'displayWindow', offset );
+	setString( dv, 'box2i', offset );
+	setUint32( dv, 16, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, 0, offset );
+	setUint32( dv, info.width - 1, offset );
+	setUint32( dv, info.height - 1, offset );
+
+	setString( dv, 'channels', offset );
+	setString( dv, 'chlist', offset );
+	setUint32( dv, info.numOutputChannels * 18 + 1, offset );
+
+	setString( dv, 'A', offset );
+	setUint32( dv, info.dataType, offset );
+	offset.value += 4;
+	setUint32( dv, 1, offset );
+	setUint32( dv, 1, offset );
+
+	setString( dv, 'B', offset );
+	setUint32( dv, info.dataType, offset );
+	offset.value += 4;
+	setUint32( dv, 1, offset );
+	setUint32( dv, 1, offset );
+
+	setString( dv, 'G', offset );
+	setUint32( dv, info.dataType, offset );
+	offset.value += 4;
+	setUint32( dv, 1, offset );
+	setUint32( dv, 1, offset );
+
+	setString( dv, 'R', offset );
+	setUint32( dv, info.dataType, offset );
+	offset.value += 4;
+	setUint32( dv, 1, offset );
+	setUint32( dv, 1, offset );
+
+	setUint8( dv, 0, offset );
+
+	// null-byte
+	setUint8( dv, 0, offset );
+
+	// = OFFSET TABLE =
+
+	let sum = offset.value + info.numBlocks * 8;
+
+	for ( let i = 0; i < chunks.data.length; ++ i ) {
+
+		setUint64( dv, sum, offset );
+
+		sum += chunks.data[ i ].size + 8;
+
+	}
+
+}
+
+function fillData( chunks, info ) {
+
+	const TableSize = info.numBlocks * 8,
+		HeaderSize = 259 + ( 18 * info.numOutputChannels ), // 259 + 18 * chlist
+		offset = { value: HeaderSize + TableSize },
+		outBuffer = new Uint8Array( HeaderSize + TableSize + chunks.totalSize + info.numBlocks * 8 ),
+		dv = new DataView( outBuffer.buffer );
+
+	fillHeader( outBuffer, chunks, info );
+
+	for ( let i = 0; i < chunks.data.length; ++ i ) {
+
+		const data = chunks.data[ i ].dataChunk;
+		const size = chunks.data[ i ].size;
+
+		setUint32( dv, i * info.blockLines, offset );
+		setUint32( dv, size, offset );
+
+		outBuffer.set( data, offset.value );
+		offset.value += size;
+
+	}
+
+	return outBuffer;
+
+}
+
+function decodeLinear( dec, r, g, b, a ) {
+
+	dec.r = r;
+	dec.g = g;
+	dec.b = b;
+	dec.a = a;
+
+}
+
+// function decodeSRGB( dec, r, g, b, a ) {
+
+// 	dec.r = r > 0.04045 ? Math.pow( r * 0.9478672986 + 0.0521327014, 2.4 ) : r * 0.0773993808;
+// 	dec.g = g > 0.04045 ? Math.pow( g * 0.9478672986 + 0.0521327014, 2.4 ) : g * 0.0773993808;
+// 	dec.b = b > 0.04045 ? Math.pow( b * 0.9478672986 + 0.0521327014, 2.4 ) : b * 0.0773993808;
+// 	dec.a = a;
+
+// }
+
+
+function setUint8( dv, value, offset ) {
+
+	dv.setUint8( offset.value, value );
+
+	offset.value += 1;
+
+}
+
+function setUint32( dv, value, offset ) {
+
+	dv.setUint32( offset.value, value, true );
+
+	offset.value += 4;
+
+}
+
+function setFloat16( dv, value, offset ) {
+
+	dv.setUint16( offset.value, DataUtils.toHalfFloat( value ), true );
+
+	offset.value += 2;
+
+}
+
+function setFloat32( dv, value, offset ) {
+
+	dv.setFloat32( offset.value, value, true );
+
+	offset.value += 4;
+
+}
+
+function setUint64( dv, value, offset ) {
+
+	dv.setBigUint64( offset.value, BigInt( value ), true );
+
+	offset.value += 8;
+
+}
+
+function setString( dv, string, offset ) {
+
+	const tmp = textEncoder.encode( string + '\0' );
+
+	for ( let i = 0; i < tmp.length; ++ i ) {
+
+		setUint8( dv, tmp[ i ], offset );
+
+	}
+
+}
+
+function decodeFloat16( binary ) {
+
+	const exponent = ( binary & 0x7C00 ) >> 10,
+		fraction = binary & 0x03FF;
+
+	return ( binary >> 15 ? - 1 : 1 ) * (
+		exponent ?
+			(
+				exponent === 0x1F ?
+					fraction ? NaN : Infinity :
+					Math.pow( 2, exponent - 15 ) * ( 1 + fraction / 0x400 )
+			) :
+			6.103515625e-5 * ( fraction / 0x400 )
+	);
+
+}
+
+function getFloat16( arr, i ) {
+
+	return decodeFloat16( arr[ i ] );
+
+}
+
+function getFloat32( arr, i ) {
+
+	return arr[ i ];
+
+}
+
+export { EXRExporter, NO_COMPRESSION, ZIP_COMPRESSION, ZIPS_COMPRESSION };