UltraHDRLoader.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. import {
  2. ClampToEdgeWrapping,
  3. DataTexture,
  4. DataUtils,
  5. FileLoader,
  6. HalfFloatType,
  7. LinearFilter,
  8. LinearMipMapLinearFilter,
  9. LinearSRGBColorSpace,
  10. Loader,
  11. RGBAFormat,
  12. UVMapping,
  13. } from 'three';
  14. /**
  15. * UltraHDR Image Format - https://developer.android.com/media/platform/hdr-image-format
  16. *
  17. * Short format brief:
  18. *
  19. * [JPEG headers]
  20. * [Metadata describing the MPF container and both SDR and gainmap images]
  21. * - XMP metadata (legacy format)
  22. * - ISO 21496-1 metadata (current standard)
  23. * [Optional metadata] [EXIF] [ICC Profile]
  24. * [SDR image]
  25. * [Gainmap image with metadata]
  26. *
  27. * Each section is separated by a 0xFFXX byte followed by a descriptor byte (0xFFE0, 0xFFE1, 0xFFE2.)
  28. * Binary image storages are prefixed with a unique 0xFFD8 16-bit descriptor.
  29. */
  30. // Pre-calculated sRGB to linear lookup table for values 0-1023
  31. const SRGB_TO_LINEAR = new Float64Array( 1024 );
  32. for ( let i = 0; i < 1024; i ++ ) {
  33. // (1/255) * 0.9478672986 = 0.003717127
  34. SRGB_TO_LINEAR[ i ] = Math.pow( i * 0.003717127 + 0.0521327014, 2.4 );
  35. }
  36. /**
  37. * A loader for the Ultra HDR Image Format.
  38. *
  39. * Existing HDR or EXR textures can be converted to Ultra HDR with this [tool](https://gainmap-creator.monogrid.com/).
  40. *
  41. * Current feature set:
  42. * - JPEG headers (required)
  43. * - XMP metadata (legacy format, supported)
  44. * - ISO 21496-1 metadata (current standard, supported)
  45. * - XMP validation (not implemented)
  46. * - EXIF profile (not implemented)
  47. * - ICC profile (not implemented)
  48. * - Binary storage for SDR & HDR images (required)
  49. * - Gainmap metadata (required)
  50. * - Non-JPEG image formats (not implemented)
  51. * - Primary image as an HDR image (not implemented)
  52. *
  53. * ```js
  54. * const loader = new UltraHDRLoader();
  55. * const texture = await loader.loadAsync( 'textures/equirectangular/ice_planet_close.jpg' );
  56. * texture.mapping = THREE.EquirectangularReflectionMapping;
  57. *
  58. * scene.background = texture;
  59. * scene.environment = texture;
  60. * ```
  61. *
  62. * @augments Loader
  63. * @three_import import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js';
  64. */
  65. class UltraHDRLoader extends Loader {
  66. /**
  67. * Constructs a new Ultra HDR loader.
  68. *
  69. * @param {LoadingManager} [manager] - The loading manager.
  70. */
  71. constructor( manager ) {
  72. super( manager );
  73. /**
  74. * The texture type.
  75. *
  76. * @type {(HalfFloatType|FloatType)}
  77. * @default HalfFloatType
  78. */
  79. this.type = HalfFloatType;
  80. }
  81. /**
  82. * Sets the texture type.
  83. *
  84. * @param {(HalfFloatType|FloatType)} value - The texture type to set.
  85. * @return {UltraHDRLoader} A reference to this loader.
  86. */
  87. setDataType( value ) {
  88. this.type = value;
  89. return this;
  90. }
  91. /**
  92. * Parses the given Ultra HDR texture data.
  93. *
  94. * @param {ArrayBuffer} buffer - The raw texture data.
  95. * @param {Function} onLoad - The `onLoad` callback.
  96. */
  97. parse( buffer, onLoad ) {
  98. const metadata = {
  99. version: null,
  100. baseRenditionIsHDR: null,
  101. gainMapMin: null,
  102. gainMapMax: null,
  103. gamma: null,
  104. offsetSDR: null,
  105. offsetHDR: null,
  106. hdrCapacityMin: null,
  107. hdrCapacityMax: null,
  108. };
  109. const textDecoder = new TextDecoder();
  110. const bytes = new Uint8Array( buffer );
  111. const sections = [];
  112. // JPEG segment-aware scanner using length headers
  113. let offset = 0;
  114. while ( offset < bytes.length - 1 ) {
  115. // Find marker prefix
  116. if ( bytes[ offset ] !== 0xff ) {
  117. offset ++;
  118. continue;
  119. }
  120. const markerType = bytes[ offset + 1 ];
  121. // SOI (0xD8) - Start of Image, no length field
  122. if ( markerType === 0xd8 ) {
  123. sections.push( {
  124. sectionType: markerType,
  125. section: bytes.subarray( offset, offset + 2 ),
  126. sectionOffset: offset + 2,
  127. } );
  128. offset += 2;
  129. continue;
  130. }
  131. // APP0-APP2 segments have length headers
  132. if ( markerType === 0xe0 || markerType === 0xe1 || markerType === 0xe2 ) {
  133. // Length is stored as big-endian 16-bit value (includes length bytes, excludes marker)
  134. const segmentLength = ( bytes[ offset + 2 ] << 8 ) | bytes[ offset + 3 ];
  135. const segmentEnd = offset + 2 + segmentLength;
  136. sections.push( {
  137. sectionType: markerType,
  138. section: bytes.subarray( offset, segmentEnd ),
  139. sectionOffset: offset + 2,
  140. } );
  141. offset = segmentEnd;
  142. continue;
  143. }
  144. // Skip other markers with length fields (0xC0-0xFE range, except RST and EOI)
  145. if ( markerType >= 0xc0 && markerType <= 0xfe && markerType !== 0xd9 && ( markerType < 0xd0 || markerType > 0xd7 ) ) {
  146. const segmentLength = ( bytes[ offset + 2 ] << 8 ) | bytes[ offset + 3 ];
  147. offset += 2 + segmentLength;
  148. continue;
  149. }
  150. // EOI (0xD9) or RST markers (0xD0-0xD7) - no length field
  151. offset += 2;
  152. }
  153. let primaryImage, gainmapImage;
  154. for ( let i = 0; i < sections.length; i ++ ) {
  155. const { sectionType, section, sectionOffset } = sections[ i ];
  156. if ( sectionType === 0xe0 ) {
  157. /* JPEG Header - no useful information */
  158. } else if ( sectionType === 0xe1 ) {
  159. /* APP1: XMP Metadata */
  160. this._parseXMPMetadata(
  161. textDecoder.decode( new Uint8Array( section ) ),
  162. metadata
  163. );
  164. } else if ( sectionType === 0xe2 ) {
  165. /* APP2: Data Sections - MPF / ICC Profile / ISO 21496-1 Metadata */
  166. const sectionData = new DataView( section.buffer, section.byteOffset + 2, section.byteLength - 2 );
  167. // Check for ISO 21496-1 namespace: "urn:iso:std:iso:ts:21496:-1\0"
  168. const isoNameSpace = 'urn:iso:std:iso:ts:21496:-1\0';
  169. if ( section.byteLength >= isoNameSpace.length + 2 ) {
  170. let isISO = true;
  171. for ( let j = 0; j < isoNameSpace.length; j ++ ) {
  172. if ( section[ 2 + j ] !== isoNameSpace.charCodeAt( j ) ) {
  173. isISO = false;
  174. break;
  175. }
  176. }
  177. if ( isISO ) {
  178. // Parse ISO 21496-1 metadata
  179. const isoData = section.subarray( 2 + isoNameSpace.length );
  180. this._parseISOMetadata( isoData, metadata );
  181. continue;
  182. }
  183. }
  184. // Check for MPF
  185. const sectionHeader = sectionData.getUint32( 2, false );
  186. if ( sectionHeader === 0x4d504600 ) {
  187. /* MPF Section */
  188. /* Section contains a list of static bytes and ends with offsets indicating location of SDR and gainmap images */
  189. /* First bytes after header indicate little / big endian ordering (0x49492A00 - LE / 0x4D4D002A - BE) */
  190. /*
  191. ... 60 bytes indicating tags, versions, etc. ...
  192. bytes | bits | description
  193. 4 32 primary image size
  194. 4 32 primary image offset
  195. 2 16 0x0000
  196. 2 16 0x0000
  197. 4 32 0x00000000
  198. 4 32 gainmap image size
  199. 4 32 gainmap image offset
  200. 2 16 0x0000
  201. 2 16 0x0000
  202. */
  203. const mpfLittleEndian = sectionData.getUint32( 6 ) === 0x49492a00;
  204. const mpfBytesOffset = 60;
  205. /* SDR size includes the metadata length, SDR offset is always 0 */
  206. const primaryImageSize = sectionData.getUint32(
  207. mpfBytesOffset,
  208. mpfLittleEndian
  209. );
  210. const primaryImageOffset = sectionData.getUint32(
  211. mpfBytesOffset + 4,
  212. mpfLittleEndian
  213. );
  214. /* Gainmap size is an absolute value starting from its offset, gainmap offset needs 6 bytes padding to take into account 0x00 bytes at the end of XMP */
  215. const gainmapImageSize = sectionData.getUint32(
  216. mpfBytesOffset + 16,
  217. mpfLittleEndian
  218. );
  219. const gainmapImageOffset =
  220. sectionData.getUint32( mpfBytesOffset + 20, mpfLittleEndian ) +
  221. sectionOffset +
  222. 6;
  223. primaryImage = new Uint8Array(
  224. buffer,
  225. primaryImageOffset,
  226. primaryImageSize
  227. );
  228. gainmapImage = new Uint8Array(
  229. buffer,
  230. gainmapImageOffset,
  231. gainmapImageSize
  232. );
  233. }
  234. }
  235. }
  236. /* Minimal sufficient validation - https://developer.android.com/media/platform/hdr-image-format#signal_of_the_format */
  237. // Version can come from either XMP or ISO metadata
  238. if ( ! metadata.version ) {
  239. throw new Error( 'THREE.UltraHDRLoader: Not a valid UltraHDR image' );
  240. }
  241. if ( primaryImage && gainmapImage ) {
  242. this._applyGainmapToSDR(
  243. metadata,
  244. primaryImage,
  245. gainmapImage,
  246. ( hdrBuffer, width, height ) => {
  247. onLoad( {
  248. width,
  249. height,
  250. data: hdrBuffer,
  251. format: RGBAFormat,
  252. type: this.type,
  253. } );
  254. },
  255. ( error ) => {
  256. throw new Error( error );
  257. }
  258. );
  259. } else {
  260. throw new Error( 'THREE.UltraHDRLoader: Could not parse UltraHDR images' );
  261. }
  262. }
  263. /**
  264. * Parses ISO 21496-1 gainmap metadata from binary data.
  265. *
  266. * @private
  267. * @param {Uint8Array} data - The binary ISO metadata.
  268. * @param {Object} metadata - The metadata object to populate.
  269. */
  270. _parseISOMetadata( data, metadata ) {
  271. const view = new DataView( data.buffer, data.byteOffset, data.byteLength );
  272. // Skip minimum version (2 bytes) and writer version (2 bytes)
  273. let offset = 4;
  274. // Read flags (1 byte)
  275. const flags = view.getUint8( offset );
  276. offset += 1;
  277. const backwardDirection = ( flags & 0x4 ) !== 0;
  278. const useCommonDenominator = ( flags & 0x8 ) !== 0;
  279. let gainMapMin, gainMapMax, gamma, offsetSDR, offsetHDR, hdrCapacityMin, hdrCapacityMax;
  280. if ( useCommonDenominator ) {
  281. // Read common denominator (4 bytes, unsigned)
  282. const commonDenominator = view.getUint32( offset, false );
  283. offset += 4;
  284. // Read baseHdrHeadroom (4 bytes, unsigned)
  285. const baseHdrHeadroomN = view.getUint32( offset, false );
  286. offset += 4;
  287. hdrCapacityMin = Math.log2( baseHdrHeadroomN / commonDenominator );
  288. // Read alternateHdrHeadroom (4 bytes, unsigned)
  289. const alternateHdrHeadroomN = view.getUint32( offset, false );
  290. offset += 4;
  291. hdrCapacityMax = Math.log2( alternateHdrHeadroomN / commonDenominator );
  292. // Read first channel (or only channel) parameters
  293. const gainMapMinN = view.getInt32( offset, false );
  294. offset += 4;
  295. gainMapMin = gainMapMinN / commonDenominator;
  296. const gainMapMaxN = view.getInt32( offset, false );
  297. offset += 4;
  298. gainMapMax = gainMapMaxN / commonDenominator;
  299. const gammaN = view.getUint32( offset, false );
  300. offset += 4;
  301. gamma = gammaN / commonDenominator;
  302. const offsetSDRN = view.getInt32( offset, false );
  303. offset += 4;
  304. offsetSDR = ( offsetSDRN / commonDenominator ) * 255.0;
  305. const offsetHDRN = view.getInt32( offset, false );
  306. offsetHDR = ( offsetHDRN / commonDenominator ) * 255.0;
  307. } else {
  308. // Read baseHdrHeadroom numerator and denominator
  309. const baseHdrHeadroomN = view.getUint32( offset, false );
  310. offset += 4;
  311. const baseHdrHeadroomD = view.getUint32( offset, false );
  312. offset += 4;
  313. hdrCapacityMin = Math.log2( baseHdrHeadroomN / baseHdrHeadroomD );
  314. // Read alternateHdrHeadroom numerator and denominator
  315. const alternateHdrHeadroomN = view.getUint32( offset, false );
  316. offset += 4;
  317. const alternateHdrHeadroomD = view.getUint32( offset, false );
  318. offset += 4;
  319. hdrCapacityMax = Math.log2( alternateHdrHeadroomN / alternateHdrHeadroomD );
  320. // Read first channel parameters
  321. const gainMapMinN = view.getInt32( offset, false );
  322. offset += 4;
  323. const gainMapMinD = view.getUint32( offset, false );
  324. offset += 4;
  325. gainMapMin = gainMapMinN / gainMapMinD;
  326. const gainMapMaxN = view.getInt32( offset, false );
  327. offset += 4;
  328. const gainMapMaxD = view.getUint32( offset, false );
  329. offset += 4;
  330. gainMapMax = gainMapMaxN / gainMapMaxD;
  331. const gammaN = view.getUint32( offset, false );
  332. offset += 4;
  333. const gammaD = view.getUint32( offset, false );
  334. offset += 4;
  335. gamma = gammaN / gammaD;
  336. const offsetSDRN = view.getInt32( offset, false );
  337. offset += 4;
  338. const offsetSDRD = view.getUint32( offset, false );
  339. offset += 4;
  340. offsetSDR = ( offsetSDRN / offsetSDRD ) * 255.0;
  341. const offsetHDRN = view.getInt32( offset, false );
  342. offset += 4;
  343. const offsetHDRD = view.getUint32( offset, false );
  344. offsetHDR = ( offsetHDRN / offsetHDRD ) * 255.0;
  345. }
  346. // Convert log2 values to linear
  347. metadata.version = '1.0'; // ISO standard doesn't encode version string, use default
  348. metadata.baseRenditionIsHDR = backwardDirection;
  349. metadata.gainMapMin = gainMapMin;
  350. metadata.gainMapMax = gainMapMax;
  351. metadata.gamma = gamma;
  352. metadata.offsetSDR = offsetSDR;
  353. metadata.offsetHDR = offsetHDR;
  354. metadata.hdrCapacityMin = hdrCapacityMin;
  355. metadata.hdrCapacityMax = hdrCapacityMax;
  356. }
  357. /**
  358. * Starts loading from the given URL and passes the loaded Ultra HDR texture
  359. * to the `onLoad()` callback.
  360. *
  361. * @param {string} url - The path/URL of the files to be loaded. This can also be a data URI.
  362. * @param {function(DataTexture, Object)} onLoad - Executed when the loading process has been finished.
  363. * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
  364. * @param {onErrorCallback} onError - Executed when errors occur.
  365. * @return {DataTexture} The Ultra HDR texture.
  366. */
  367. load( url, onLoad, onProgress, onError ) {
  368. const texture = new DataTexture(
  369. this.type === HalfFloatType ? new Uint16Array() : new Float32Array(),
  370. 0,
  371. 0,
  372. RGBAFormat,
  373. this.type,
  374. UVMapping,
  375. ClampToEdgeWrapping,
  376. ClampToEdgeWrapping,
  377. LinearFilter,
  378. LinearMipMapLinearFilter,
  379. 1,
  380. LinearSRGBColorSpace
  381. );
  382. texture.generateMipmaps = true;
  383. texture.flipY = true;
  384. const loader = new FileLoader( this.manager );
  385. loader.setResponseType( 'arraybuffer' );
  386. loader.setRequestHeader( this.requestHeader );
  387. loader.setPath( this.path );
  388. loader.setWithCredentials( this.withCredentials );
  389. loader.load( url, ( buffer ) => {
  390. try {
  391. this.parse(
  392. buffer,
  393. ( texData ) => {
  394. texture.image = {
  395. data: texData.data,
  396. width: texData.width,
  397. height: texData.height,
  398. };
  399. texture.needsUpdate = true;
  400. if ( onLoad ) onLoad( texture, texData );
  401. }
  402. );
  403. } catch ( error ) {
  404. if ( onError ) onError( error );
  405. console.error( error );
  406. }
  407. }, onProgress, onError );
  408. return texture;
  409. }
  410. _parseXMPMetadata( xmpDataString, metadata ) {
  411. const domParser = new DOMParser();
  412. const xmpXml = domParser.parseFromString(
  413. xmpDataString.substring(
  414. xmpDataString.indexOf( '<' ),
  415. xmpDataString.lastIndexOf( '>' ) + 1
  416. ),
  417. 'text/xml'
  418. );
  419. /* Determine if given XMP metadata is the primary GContainer descriptor or a gainmap descriptor */
  420. const [ hasHDRContainerDescriptor ] = xmpXml.getElementsByTagName(
  421. 'Container:Directory'
  422. );
  423. if ( hasHDRContainerDescriptor ) {
  424. /* There's not much useful information in the container descriptor besides memory-validation */
  425. } else {
  426. /* Gainmap descriptor - defaults from https://developer.android.com/media/platform/hdr-image-format#HDR_gain_map_metadata */
  427. const [ gainmapNode ] = xmpXml.getElementsByTagName( 'rdf:Description' );
  428. metadata.version = gainmapNode.getAttribute( 'hdrgm:Version' );
  429. metadata.baseRenditionIsHDR =
  430. gainmapNode.getAttribute( 'hdrgm:BaseRenditionIsHDR' ) === 'True';
  431. metadata.gainMapMin = parseFloat(
  432. gainmapNode.getAttribute( 'hdrgm:GainMapMin' ) || 0.0
  433. );
  434. metadata.gainMapMax = parseFloat(
  435. gainmapNode.getAttribute( 'hdrgm:GainMapMax' ) || 1.0
  436. );
  437. metadata.gamma = parseFloat(
  438. gainmapNode.getAttribute( 'hdrgm:Gamma' ) || 1.0
  439. );
  440. metadata.offsetSDR = parseFloat(
  441. gainmapNode.getAttribute( 'hdrgm:OffsetSDR' ) / ( 1 / 64 )
  442. );
  443. metadata.offsetHDR = parseFloat(
  444. gainmapNode.getAttribute( 'hdrgm:OffsetHDR' ) / ( 1 / 64 )
  445. );
  446. metadata.hdrCapacityMin = parseFloat(
  447. gainmapNode.getAttribute( 'hdrgm:HDRCapacityMin' ) || 0.0
  448. );
  449. metadata.hdrCapacityMax = parseFloat(
  450. gainmapNode.getAttribute( 'hdrgm:HDRCapacityMax' ) || 1.0
  451. );
  452. }
  453. }
  454. _srgbToLinear( value ) {
  455. // 0.04045 * 255 = 10.31475
  456. if ( value < 10.31475 ) {
  457. // (1/255) * 0.0773993808
  458. return value * 0.000303527;
  459. }
  460. if ( value < 1024 ) {
  461. return SRGB_TO_LINEAR[ value | 0 ];
  462. }
  463. // (1/255) * 0.9478672986 = 0.003717127
  464. return Math.pow( value * 0.003717127 + 0.0521327014, 2.4 );
  465. }
  466. _applyGainmapToSDR(
  467. metadata,
  468. sdrBuffer,
  469. gainmapBuffer,
  470. onSuccess,
  471. onError
  472. ) {
  473. const decodeImage = ( data ) => createImageBitmap( new Blob( [ data ], { type: 'image/jpeg' } ) );
  474. Promise.all( [ decodeImage( sdrBuffer ), decodeImage( gainmapBuffer ) ] )
  475. .then( ( [ sdrImage, gainmapImage ] ) => {
  476. const sdrWidth = sdrImage.width;
  477. const sdrHeight = sdrImage.height;
  478. const sdrImageAspect = sdrWidth / sdrHeight;
  479. const gainmapImageAspect = gainmapImage.width / gainmapImage.height;
  480. if ( sdrImageAspect !== gainmapImageAspect ) {
  481. onError(
  482. 'THREE.UltraHDRLoader Error: Aspect ratio mismatch between SDR and Gainmap images'
  483. );
  484. return;
  485. }
  486. const canvas = document.createElement( 'canvas' );
  487. const ctx = canvas.getContext( '2d', {
  488. willReadFrequently: true,
  489. colorSpace: 'srgb',
  490. } );
  491. canvas.width = sdrWidth;
  492. canvas.height = sdrHeight;
  493. /* Use out-of-the-box interpolation of Canvas API to scale gainmap to fit the SDR resolution */
  494. ctx.drawImage(
  495. gainmapImage,
  496. 0,
  497. 0,
  498. gainmapImage.width,
  499. gainmapImage.height,
  500. 0,
  501. 0,
  502. sdrWidth,
  503. sdrHeight
  504. );
  505. const gainmapImageData = ctx.getImageData(
  506. 0,
  507. 0,
  508. sdrWidth,
  509. sdrHeight,
  510. { colorSpace: 'srgb' }
  511. );
  512. ctx.drawImage( sdrImage, 0, 0 );
  513. const sdrImageData = ctx.getImageData(
  514. 0,
  515. 0,
  516. sdrWidth,
  517. sdrHeight,
  518. { colorSpace: 'srgb' }
  519. );
  520. /* HDR Recovery formula - https://developer.android.com/media/platform/hdr-image-format#use_the_gain_map_to_create_adapted_HDR_rendition */
  521. /* 1.8 instead of 2 near-perfectly rectifies approximations introduced by precalculated SRGB_TO_LINEAR values */
  522. const maxDisplayBoost = 1.8 ** ( metadata.hdrCapacityMax * 0.5 );
  523. const unclampedWeightFactor =
  524. ( Math.log2( maxDisplayBoost ) - metadata.hdrCapacityMin ) /
  525. ( metadata.hdrCapacityMax - metadata.hdrCapacityMin );
  526. const weightFactor = Math.min(
  527. Math.max( unclampedWeightFactor, 0.0 ),
  528. 1.0
  529. );
  530. const sdrData = sdrImageData.data;
  531. const gainmapData = gainmapImageData.data;
  532. const dataLength = sdrData.length;
  533. const gainMapMin = metadata.gainMapMin;
  534. const gainMapMax = metadata.gainMapMax;
  535. const offsetSDR = metadata.offsetSDR;
  536. const offsetHDR = metadata.offsetHDR;
  537. const invGamma = 1.0 / metadata.gamma;
  538. const useGammaOne = metadata.gamma === 1.0;
  539. const isHalfFloat = this.type === HalfFloatType;
  540. const toHalfFloat = DataUtils.toHalfFloat;
  541. const srgbToLinear = this._srgbToLinear;
  542. const hdrBuffer = isHalfFloat
  543. ? new Uint16Array( dataLength ).fill( 15360 )
  544. : new Float32Array( dataLength ).fill( 1.0 );
  545. for ( let i = 0; i < dataLength; i += 4 ) {
  546. for ( let c = 0; c < 3; c ++ ) {
  547. const idx = i + c;
  548. const sdrValue = sdrData[ idx ];
  549. const gainmapValue = gainmapData[ idx ] * 0.00392156862745098; // 1/255
  550. const logRecovery = useGammaOne
  551. ? gainmapValue
  552. : Math.pow( gainmapValue, invGamma );
  553. const logBoost = gainMapMin + ( gainMapMax - gainMapMin ) * logRecovery;
  554. const hdrValue =
  555. ( sdrValue + offsetSDR ) *
  556. ( logBoost * weightFactor === 0.0
  557. ? 1.0
  558. : Math.pow( 2, logBoost * weightFactor ) ) -
  559. offsetHDR;
  560. const linearHDRValue = Math.min(
  561. Math.max( srgbToLinear( hdrValue ), 0 ),
  562. 65504
  563. );
  564. hdrBuffer[ idx ] = isHalfFloat
  565. ? toHalfFloat( linearHDRValue )
  566. : linearHDRValue;
  567. }
  568. }
  569. onSuccess( hdrBuffer, sdrWidth, sdrHeight );
  570. } )
  571. .catch( ( e ) => {
  572. onError( e );
  573. } );
  574. }
  575. }
  576. export { UltraHDRLoader };
粤ICP备19079148号