EXRExporter.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. /**
  2. * @author sciecode / https://github.com/sciecode
  3. *
  4. * EXR format references:
  5. * https://www.openexr.com/documentation/openexrfilelayout.pdf
  6. */
  7. import {
  8. FloatType,
  9. HalfFloatType,
  10. RGBAFormat,
  11. DataUtils,
  12. } from 'three';
  13. import * as fflate from '../libs/fflate.module.js';
  14. const textEncoder = new TextEncoder();
  15. const NO_COMPRESSION = 0;
  16. const ZIPS_COMPRESSION = 2;
  17. const ZIP_COMPRESSION = 3;
  18. class EXRExporter {
  19. parse( renderer, renderTarget, options ) {
  20. if ( ! supported( renderer, renderTarget ) ) return undefined;
  21. const info = buildInfo( renderTarget, options ),
  22. dataBuffer = getPixelData( renderer, renderTarget, info ),
  23. rawContentBuffer = reorganizeDataBuffer( dataBuffer, info ),
  24. chunks = compressData( rawContentBuffer, info );
  25. return fillData( chunks, info );
  26. }
  27. }
  28. function supported( renderer, renderTarget ) {
  29. if ( ! renderer || ! renderer.isWebGLRenderer ) {
  30. console.error( 'EXRExporter.parse: Unsupported first parameter, expected instance of WebGLRenderer.' );
  31. return false;
  32. }
  33. if ( ! renderTarget || ! renderTarget.isWebGLRenderTarget ) {
  34. console.error( 'EXRExporter.parse: Unsupported second parameter, expected instance of WebGLRenderTarget.' );
  35. return false;
  36. }
  37. if ( renderTarget.texture.type !== FloatType && renderTarget.texture.type !== HalfFloatType ) {
  38. console.error( 'EXRExporter.parse: Unsupported WebGLRenderTarget texture type.' );
  39. return false;
  40. }
  41. if ( renderTarget.texture.format !== RGBAFormat ) {
  42. console.error( 'EXRExporter.parse: Unsupported WebGLRenderTarget texture format, expected RGBAFormat.' );
  43. return false;
  44. }
  45. return true;
  46. }
  47. function buildInfo( renderTarget, options = {} ) {
  48. const compressionSizes = {
  49. 0: 1,
  50. 2: 1,
  51. 3: 16
  52. };
  53. const WIDTH = renderTarget.width,
  54. HEIGHT = renderTarget.height,
  55. TYPE = renderTarget.texture.type,
  56. FORMAT = renderTarget.texture.format,
  57. ENCODING = renderTarget.texture.encoding,
  58. COMPRESSION = ( options.compression !== undefined ) ? options.compression : ZIP_COMPRESSION,
  59. EXPORTER_TYPE = ( options.type !== undefined ) ? options.type : HalfFloatType,
  60. OUT_TYPE = ( EXPORTER_TYPE === FloatType ) ? 2 : 1,
  61. COMPRESSION_SIZE = compressionSizes[ COMPRESSION ],
  62. NUM_CHANNELS = 4;
  63. return {
  64. width: WIDTH,
  65. height: HEIGHT,
  66. type: TYPE,
  67. format: FORMAT,
  68. encoding: ENCODING,
  69. compression: COMPRESSION,
  70. blockLines: COMPRESSION_SIZE,
  71. dataType: OUT_TYPE,
  72. dataSize: 2 * OUT_TYPE,
  73. numBlocks: Math.ceil( HEIGHT / COMPRESSION_SIZE ),
  74. numInputChannels: 4,
  75. numOutputChannels: NUM_CHANNELS,
  76. };
  77. }
  78. function getPixelData( renderer, rtt, info ) {
  79. let dataBuffer;
  80. if ( info.type === FloatType ) {
  81. dataBuffer = new Float32Array( info.width * info.height * info.numInputChannels );
  82. } else {
  83. dataBuffer = new Uint16Array( info.width * info.height * info.numInputChannels );
  84. }
  85. renderer.readRenderTargetPixels( rtt, 0, 0, info.width, info.height, dataBuffer );
  86. return dataBuffer;
  87. }
  88. function reorganizeDataBuffer( inBuffer, info ) {
  89. const w = info.width,
  90. h = info.height,
  91. dec = { r: 0, g: 0, b: 0, a: 0 },
  92. offset = { value: 0 },
  93. cOffset = ( info.numOutputChannels == 4 ) ? 1 : 0,
  94. getValue = ( info.type == FloatType ) ? getFloat32 : getFloat16,
  95. setValue = ( info.dataType == 1 ) ? setFloat16 : setFloat32,
  96. outBuffer = new Uint8Array( info.width * info.height * info.numOutputChannels * info.dataSize ),
  97. dv = new DataView( outBuffer.buffer );
  98. for ( let y = 0; y < h; ++ y ) {
  99. for ( let x = 0; x < w; ++ x ) {
  100. const i = y * w * 4 + x * 4;
  101. const r = getValue( inBuffer, i );
  102. const g = getValue( inBuffer, i + 1 );
  103. const b = getValue( inBuffer, i + 2 );
  104. const a = getValue( inBuffer, i + 3 );
  105. const line = ( h - y - 1 ) * w * ( 3 + cOffset ) * info.dataSize;
  106. decodeLinear( dec, r, g, b, a );
  107. offset.value = line + x * info.dataSize;
  108. setValue( dv, dec.a, offset );
  109. offset.value = line + ( cOffset ) * w * info.dataSize + x * info.dataSize;
  110. setValue( dv, dec.b, offset );
  111. offset.value = line + ( 1 + cOffset ) * w * info.dataSize + x * info.dataSize;
  112. setValue( dv, dec.g, offset );
  113. offset.value = line + ( 2 + cOffset ) * w * info.dataSize + x * info.dataSize;
  114. setValue( dv, dec.r, offset );
  115. }
  116. }
  117. return outBuffer;
  118. }
  119. function compressData( inBuffer, info ) {
  120. let compress,
  121. tmpBuffer,
  122. sum = 0;
  123. const chunks = { data: new Array(), totalSize: 0 },
  124. size = info.width * info.numOutputChannels * info.blockLines * info.dataSize;
  125. switch ( info.compression ) {
  126. case 0:
  127. compress = compressNONE;
  128. break;
  129. case 2:
  130. case 3:
  131. compress = compressZIP;
  132. break;
  133. }
  134. if ( info.compression !== 0 ) {
  135. tmpBuffer = new Uint8Array( size );
  136. }
  137. for ( let i = 0; i < info.numBlocks; ++ i ) {
  138. const arr = inBuffer.subarray( size * i, size * ( i + 1 ) );
  139. const block = compress( arr, tmpBuffer );
  140. sum += block.length;
  141. chunks.data.push( { dataChunk: block, size: block.length } );
  142. }
  143. chunks.totalSize = sum;
  144. return chunks;
  145. }
  146. function compressNONE( data ) {
  147. return data;
  148. }
  149. function compressZIP( data, tmpBuffer ) {
  150. //
  151. // Reorder the pixel data.
  152. //
  153. let t1 = 0,
  154. t2 = Math.floor( ( data.length + 1 ) / 2 ),
  155. s = 0;
  156. const stop = data.length - 1;
  157. while ( true ) {
  158. if ( s > stop ) break;
  159. tmpBuffer[ t1 ++ ] = data[ s ++ ];
  160. if ( s > stop ) break;
  161. tmpBuffer[ t2 ++ ] = data[ s ++ ];
  162. }
  163. //
  164. // Predictor.
  165. //
  166. let p = tmpBuffer[ 0 ];
  167. for ( let t = 1; t < tmpBuffer.length; t ++ ) {
  168. const d = tmpBuffer[ t ] - p + ( 128 + 256 );
  169. p = tmpBuffer[ t ];
  170. tmpBuffer[ t ] = d;
  171. }
  172. if ( typeof fflate === 'undefined' ) {
  173. console.error( 'THREE.EXRLoader: External \`fflate.module.js\` required' );
  174. }
  175. const deflate = fflate.zlibSync( tmpBuffer ); // eslint-disable-line no-undef
  176. return deflate;
  177. }
  178. function fillHeader( outBuffer, chunks, info ) {
  179. const offset = { value: 0 };
  180. const dv = new DataView( outBuffer.buffer );
  181. setUint32( dv, 20000630, offset ); // magic
  182. setUint32( dv, 2, offset ); // mask
  183. // = HEADER =
  184. setString( dv, 'compression', offset );
  185. setString( dv, 'compression', offset );
  186. setUint32( dv, 1, offset );
  187. setUint8( dv, info.compression, offset );
  188. setString( dv, 'screenWindowCenter', offset );
  189. setString( dv, 'v2f', offset );
  190. setUint32( dv, 8, offset );
  191. setUint32( dv, 0, offset );
  192. setUint32( dv, 0, offset );
  193. setString( dv, 'screenWindowWidth', offset );
  194. setString( dv, 'float', offset );
  195. setUint32( dv, 4, offset );
  196. setFloat32( dv, 1.0, offset );
  197. setString( dv, 'pixelAspectRatio', offset );
  198. setString( dv, 'float', offset );
  199. setUint32( dv, 4, offset );
  200. setFloat32( dv, 1.0, offset );
  201. setString( dv, 'lineOrder', offset );
  202. setString( dv, 'lineOrder', offset );
  203. setUint32( dv, 1, offset );
  204. setUint8( dv, 0, offset );
  205. setString( dv, 'dataWindow', offset );
  206. setString( dv, 'box2i', offset );
  207. setUint32( dv, 16, offset );
  208. setUint32( dv, 0, offset );
  209. setUint32( dv, 0, offset );
  210. setUint32( dv, info.width - 1, offset );
  211. setUint32( dv, info.height - 1, offset );
  212. setString( dv, 'displayWindow', offset );
  213. setString( dv, 'box2i', offset );
  214. setUint32( dv, 16, offset );
  215. setUint32( dv, 0, offset );
  216. setUint32( dv, 0, offset );
  217. setUint32( dv, info.width - 1, offset );
  218. setUint32( dv, info.height - 1, offset );
  219. setString( dv, 'channels', offset );
  220. setString( dv, 'chlist', offset );
  221. setUint32( dv, info.numOutputChannels * 18 + 1, offset );
  222. setString( dv, 'A', offset );
  223. setUint32( dv, info.dataType, offset );
  224. offset.value += 4;
  225. setUint32( dv, 1, offset );
  226. setUint32( dv, 1, offset );
  227. setString( dv, 'B', offset );
  228. setUint32( dv, info.dataType, offset );
  229. offset.value += 4;
  230. setUint32( dv, 1, offset );
  231. setUint32( dv, 1, offset );
  232. setString( dv, 'G', offset );
  233. setUint32( dv, info.dataType, offset );
  234. offset.value += 4;
  235. setUint32( dv, 1, offset );
  236. setUint32( dv, 1, offset );
  237. setString( dv, 'R', offset );
  238. setUint32( dv, info.dataType, offset );
  239. offset.value += 4;
  240. setUint32( dv, 1, offset );
  241. setUint32( dv, 1, offset );
  242. setUint8( dv, 0, offset );
  243. // null-byte
  244. setUint8( dv, 0, offset );
  245. // = OFFSET TABLE =
  246. let sum = offset.value + info.numBlocks * 8;
  247. for ( let i = 0; i < chunks.data.length; ++ i ) {
  248. setUint64( dv, sum, offset );
  249. sum += chunks.data[ i ].size + 8;
  250. }
  251. }
  252. function fillData( chunks, info ) {
  253. const TableSize = info.numBlocks * 8,
  254. HeaderSize = 259 + ( 18 * info.numOutputChannels ), // 259 + 18 * chlist
  255. offset = { value: HeaderSize + TableSize },
  256. outBuffer = new Uint8Array( HeaderSize + TableSize + chunks.totalSize + info.numBlocks * 8 ),
  257. dv = new DataView( outBuffer.buffer );
  258. fillHeader( outBuffer, chunks, info );
  259. for ( let i = 0; i < chunks.data.length; ++ i ) {
  260. const data = chunks.data[ i ].dataChunk;
  261. const size = chunks.data[ i ].size;
  262. setUint32( dv, i * info.blockLines, offset );
  263. setUint32( dv, size, offset );
  264. outBuffer.set( data, offset.value );
  265. offset.value += size;
  266. }
  267. return outBuffer;
  268. }
  269. function decodeLinear( dec, r, g, b, a ) {
  270. dec.r = r;
  271. dec.g = g;
  272. dec.b = b;
  273. dec.a = a;
  274. }
  275. // function decodeSRGB( dec, r, g, b, a ) {
  276. // dec.r = r > 0.04045 ? Math.pow( r * 0.9478672986 + 0.0521327014, 2.4 ) : r * 0.0773993808;
  277. // dec.g = g > 0.04045 ? Math.pow( g * 0.9478672986 + 0.0521327014, 2.4 ) : g * 0.0773993808;
  278. // dec.b = b > 0.04045 ? Math.pow( b * 0.9478672986 + 0.0521327014, 2.4 ) : b * 0.0773993808;
  279. // dec.a = a;
  280. // }
  281. function setUint8( dv, value, offset ) {
  282. dv.setUint8( offset.value, value );
  283. offset.value += 1;
  284. }
  285. function setUint32( dv, value, offset ) {
  286. dv.setUint32( offset.value, value, true );
  287. offset.value += 4;
  288. }
  289. function setFloat16( dv, value, offset ) {
  290. dv.setUint16( offset.value, DataUtils.toHalfFloat( value ), true );
  291. offset.value += 2;
  292. }
  293. function setFloat32( dv, value, offset ) {
  294. dv.setFloat32( offset.value, value, true );
  295. offset.value += 4;
  296. }
  297. function setUint64( dv, value, offset ) {
  298. dv.setBigUint64( offset.value, BigInt( value ), true );
  299. offset.value += 8;
  300. }
  301. function setString( dv, string, offset ) {
  302. const tmp = textEncoder.encode( string + '\0' );
  303. for ( let i = 0; i < tmp.length; ++ i ) {
  304. setUint8( dv, tmp[ i ], offset );
  305. }
  306. }
  307. function decodeFloat16( binary ) {
  308. const exponent = ( binary & 0x7C00 ) >> 10,
  309. fraction = binary & 0x03FF;
  310. return ( binary >> 15 ? - 1 : 1 ) * (
  311. exponent ?
  312. (
  313. exponent === 0x1F ?
  314. fraction ? NaN : Infinity :
  315. Math.pow( 2, exponent - 15 ) * ( 1 + fraction / 0x400 )
  316. ) :
  317. 6.103515625e-5 * ( fraction / 0x400 )
  318. );
  319. }
  320. function getFloat16( arr, i ) {
  321. return decodeFloat16( arr[ i ] );
  322. }
  323. function getFloat32( arr, i ) {
  324. return arr[ i ];
  325. }
  326. export { EXRExporter, NO_COMPRESSION, ZIP_COMPRESSION, ZIPS_COMPRESSION };