|
@@ -0,0 +1,346 @@
|
|
|
+/**
|
|
|
+ * @author Richard M. / https://github.com/richardmonette
|
|
|
+ */
|
|
|
+
|
|
|
+// https://github.com/mrdoob/three.js/issues/10652
|
|
|
+// https://en.wikipedia.org/wiki/OpenEXR
|
|
|
+
|
|
|
+THREE.EXRLoader = function ( manager ) {
|
|
|
+
|
|
|
+ this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
|
|
|
+
|
|
|
+};
|
|
|
+
|
|
|
+THREE.EXRLoader.prototype = Object.create( THREE.DataTextureLoader.prototype );
|
|
|
+
|
|
|
+THREE.EXRLoader.prototype._parser = function ( buffer ) {
|
|
|
+
|
|
|
+ var parseNullTerminatedString = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var uintBuffer = new Uint8Array( buffer );
|
|
|
+ var endOffset = 0;
|
|
|
+
|
|
|
+ while ( uintBuffer[ offset.value + endOffset ] != 0 ) {
|
|
|
+
|
|
|
+ endOffset += 1;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var stringValue = new TextDecoder().decode(
|
|
|
+ new Uint8Array( buffer ).slice( offset.value, offset.value + endOffset )
|
|
|
+ );
|
|
|
+
|
|
|
+ offset.value = offset.value + endOffset + 1;
|
|
|
+
|
|
|
+ return stringValue;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseFixedLengthString = function( buffer, offset, size ) {
|
|
|
+
|
|
|
+ var stringValue = new TextDecoder().decode(
|
|
|
+ new Uint8Array( buffer ).slice( offset.value, offset.value + size )
|
|
|
+ );
|
|
|
+
|
|
|
+ offset.value = offset.value + size;
|
|
|
+
|
|
|
+ return stringValue;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseUlong = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var uLong = new DataView( buffer.slice( offset.value, offset.value + 4 ) ).getUint32( 0, true );
|
|
|
+
|
|
|
+ offset.value = offset.value + 8;
|
|
|
+
|
|
|
+ return uLong;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseUint32 = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var Uint32 = new DataView( buffer.slice( offset.value, offset.value + 4 ) ).getUint32( 0, true );
|
|
|
+
|
|
|
+ offset.value = offset.value + 4;
|
|
|
+
|
|
|
+ return Uint32;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseUint8 = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var Uint8 = new DataView( buffer.slice( offset.value, offset.value + 1 ) ).getUint8( 0, true );
|
|
|
+
|
|
|
+ offset.value = offset.value + 1;
|
|
|
+
|
|
|
+ return Uint8;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseFloat32 = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var float = new DataView( buffer.slice( offset.value, offset.value + 4 ) ).getFloat32( 0, true );
|
|
|
+
|
|
|
+ offset.value += 4;
|
|
|
+
|
|
|
+ return float;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // https://stackoverflow.com/questions/5678432/decompressing-half-precision-floats-in-javascript
|
|
|
+ var decodeFloat16 = function( binary ) {
|
|
|
+
|
|
|
+ var 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 )
|
|
|
+ );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseFloat16 = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var float = new DataView( buffer.slice( offset.value, offset.value + 2 ) ).getUint16( 0, true );
|
|
|
+
|
|
|
+ offset.value += 2;
|
|
|
+
|
|
|
+ return decodeFloat16( float );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseChlist = function( buffer, offset, size ) {
|
|
|
+
|
|
|
+ var startOffset = offset.value;
|
|
|
+ var channels = [];
|
|
|
+
|
|
|
+ while ( offset.value < ( startOffset + size - 1 ) ) {
|
|
|
+
|
|
|
+ var name = parseNullTerminatedString( buffer, offset );
|
|
|
+ var pixelType = parseUint32( buffer, offset ); // TODO: Cast this to UINT, HALF or FLOAT
|
|
|
+ var pLinear = parseUint8( buffer, offset );
|
|
|
+ offset.value += 3; // reserved, three chars
|
|
|
+ var xSampling = parseUint32( buffer, offset );
|
|
|
+ var ySampling = parseUint32( buffer, offset );
|
|
|
+
|
|
|
+ channels.push( {
|
|
|
+ name: name,
|
|
|
+ pixelType: pixelType,
|
|
|
+ pLinear: pLinear,
|
|
|
+ xSampling: xSampling,
|
|
|
+ ySampling: ySampling
|
|
|
+ } );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ offset.value += 1;
|
|
|
+
|
|
|
+ return channels;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseChromaticities = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var redX = parseFloat32( buffer, offset );
|
|
|
+ var redY = parseFloat32( buffer, offset );
|
|
|
+ var greenX = parseFloat32( buffer, offset );
|
|
|
+ var greenY = parseFloat32( buffer, offset );
|
|
|
+ var blueX = parseFloat32( buffer, offset );
|
|
|
+ var blueY = parseFloat32( buffer, offset );
|
|
|
+ var whiteX = parseFloat32( buffer, offset );
|
|
|
+ var whiteY = parseFloat32( buffer, offset );
|
|
|
+
|
|
|
+ return { redX: redX, redY: redY, greenX, greenY, blueX, blueY, whiteX, whiteY };
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseCompression = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var compressionCodes = [
|
|
|
+ 'NO_COMPRESSION',
|
|
|
+ 'PIZ_COMPRESSION'
|
|
|
+ ];
|
|
|
+
|
|
|
+ var compression = parseUint8( buffer, offset );
|
|
|
+
|
|
|
+ return compressionCodes[ compression ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseBox2i = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var xMin = parseUint32( buffer, offset );
|
|
|
+ var yMin = parseUint32( buffer, offset );
|
|
|
+ var xMax = parseUint32( buffer, offset );
|
|
|
+ var yMax = parseUint32( buffer, offset );
|
|
|
+
|
|
|
+ return { xMin: xMin, yMin: yMin, xMax: xMax, yMax: yMax };
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseLineOrder = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var lineOrders = [
|
|
|
+ 'INCREASING_Y'
|
|
|
+ ];
|
|
|
+
|
|
|
+ var lineOrder = parseUint8( buffer, offset );
|
|
|
+
|
|
|
+ return lineOrders[ lineOrder ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseV2f = function( buffer, offset ) {
|
|
|
+
|
|
|
+ var x = parseFloat32( buffer, offset );
|
|
|
+ var y = parseFloat32( buffer, offset );
|
|
|
+
|
|
|
+ return [ x, y ];
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var parseValue = function( buffer, offset, type, size ) {
|
|
|
+
|
|
|
+ if ( type == 'string' || type == 'iccProfile' ) {
|
|
|
+
|
|
|
+ return parseFixedLengthString( buffer, offset, size );
|
|
|
+
|
|
|
+ } else if ( type == 'chlist' ) {
|
|
|
+
|
|
|
+ return parseChlist( buffer, offset, size );
|
|
|
+
|
|
|
+ } else if ( type == 'chromaticities' ) {
|
|
|
+
|
|
|
+ return parseChromaticities( buffer, offset );
|
|
|
+
|
|
|
+ } else if ( type == 'compression' ) {
|
|
|
+
|
|
|
+ return parseCompression( buffer, offset );
|
|
|
+
|
|
|
+ } else if ( type == 'box2i' ) {
|
|
|
+
|
|
|
+ return parseBox2i( buffer, offset );
|
|
|
+
|
|
|
+ } else if ( type == 'lineOrder' ) {
|
|
|
+
|
|
|
+ return parseLineOrder( buffer, offset );
|
|
|
+
|
|
|
+ } else if ( type == 'float' ) {
|
|
|
+
|
|
|
+ return parseFloat32( buffer, offset );
|
|
|
+
|
|
|
+ } else if ( type == 'v2f' ) {
|
|
|
+
|
|
|
+ return parseV2f( buffer, offset );
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ throw 'Cannot parse value for unsupported type: ' + type;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ var EXRHeader = {};
|
|
|
+
|
|
|
+ var magic = new DataView( buffer ).getUint32( 0, true );
|
|
|
+ var versionByteZero = new DataView( buffer ).getUint8( 4, true );
|
|
|
+ var fullMask = new DataView( buffer ).getUint8( 5, true );
|
|
|
+
|
|
|
+ // start of header
|
|
|
+
|
|
|
+ var offset = { value: 8 }; // start at 8, after magic stuff
|
|
|
+
|
|
|
+ var keepReading = true;
|
|
|
+
|
|
|
+ while ( keepReading ) {
|
|
|
+
|
|
|
+ var attributeName = parseNullTerminatedString( buffer, offset );
|
|
|
+
|
|
|
+ if ( attributeName == 0 ) {
|
|
|
+
|
|
|
+ keepReading = false;
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ var attributeType = parseNullTerminatedString( buffer, offset );
|
|
|
+ var attributeSize = parseUint32( buffer, offset );
|
|
|
+ var attributeValue = parseValue( buffer, offset, attributeType, attributeSize );
|
|
|
+
|
|
|
+ EXRHeader[ attributeName ] = attributeValue;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // offsets
|
|
|
+
|
|
|
+ var dataWindowHeight = EXRHeader.dataWindow.yMax + 1;
|
|
|
+ var scanlineBlockSize = 1; // 1 for no compression, 32 for PIZ
|
|
|
+ var numBlocks = dataWindowHeight / scanlineBlockSize;
|
|
|
+
|
|
|
+ for ( var i = 0; i < numBlocks; i ++ ) {
|
|
|
+
|
|
|
+ var scanlineOffset = parseUlong( buffer, offset );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ // we should be passed the scanline offset table, start reading pixel data
|
|
|
+
|
|
|
+ var width = EXRHeader.dataWindow.xMax - EXRHeader.dataWindow.xMin + 1;
|
|
|
+ var height = EXRHeader.dataWindow.yMax - EXRHeader.dataWindow.yMin + 1;
|
|
|
+ var numChannels = EXRHeader.channels.length;
|
|
|
+
|
|
|
+ var byteArray = new Float32Array( width * height * numChannels );
|
|
|
+
|
|
|
+ var channelOffsets = {
|
|
|
+ R: 0,
|
|
|
+ G: 1,
|
|
|
+ B: 2,
|
|
|
+ A: 3
|
|
|
+ };
|
|
|
+
|
|
|
+ for ( var y = 0; y < height; y ++ ) {
|
|
|
+
|
|
|
+ var y_scanline = parseUint32( buffer, offset );
|
|
|
+ var dataSize = parseUint32( buffer, offset );
|
|
|
+
|
|
|
+ for ( var channelID = 0; channelID < EXRHeader.channels.length; channelID ++ ) {
|
|
|
+ if ( EXRHeader.channels[ channelID ].pixelType == 1 ) {
|
|
|
+ // HALF
|
|
|
+ for ( var x = 0; x < width; x ++ ) {
|
|
|
+
|
|
|
+ var val = parseFloat16( buffer, offset );
|
|
|
+ var cOff = channelOffsets[ EXRHeader.channels[ channelID ].name ];
|
|
|
+
|
|
|
+ byteArray[ ( ( ( width - y_scanline ) * ( height * numChannels ) ) + ( x * numChannels ) ) + cOff ] = val;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+
|
|
|
+ throw 'Only supported pixel format is HALF';
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ header: EXRHeader,
|
|
|
+ width: width,
|
|
|
+ height: height,
|
|
|
+ data: byteArray,
|
|
|
+ format: THREE.RGBAFormat,
|
|
|
+ type: THREE.FloatType
|
|
|
+ };
|
|
|
+
|
|
|
+};
|