Skip to content

Commit 399951c

Browse files
authored
UltraHDRLoader: Add support for ISO 21496-1 gainmap metadata (#32862)
1 parent b59b5e7 commit 399951c

File tree

1 file changed

+182
-30
lines changed

1 file changed

+182
-30
lines changed

examples/jsm/loaders/UltraHDRLoader.js

Lines changed: 182 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import {
1818
* Short format brief:
1919
*
2020
* [JPEG headers]
21-
* [XMP metadata describing the MPF container and *both* SDR and gainmap images]
21+
* [Metadata describing the MPF container and both SDR and gainmap images]
22+
* - XMP metadata (legacy format)
23+
* - ISO 21496-1 metadata (current standard)
2224
* [Optional metadata] [EXIF] [ICC Profile]
2325
* [SDR image]
24-
* [XMP metadata describing only the gainmap image]
25-
* [Gainmap image]
26+
* [Gainmap image with metadata]
2627
*
2728
* Each section is separated by a 0xFFXX byte followed by a descriptor byte (0xFFE0, 0xFFE1, 0xFFE2.)
2829
* Binary image storages are prefixed with a unique 0xFFD8 16-bit descriptor.
@@ -45,7 +46,8 @@ for ( let i = 0; i < 1024; i ++ ) {
4546
*
4647
* Current feature set:
4748
* - JPEG headers (required)
48-
* - XMP metadata (required)
49+
* - XMP metadata (legacy format, supported)
50+
* - ISO 21496-1 metadata (current standard, supported)
4951
* - XMP validation (not implemented)
5052
* - EXIF profile (not implemented)
5153
* - ICC profile (not implemented)
@@ -109,7 +111,7 @@ class UltraHDRLoader extends Loader {
109111
*/
110112
parse( buffer, onLoad ) {
111113

112-
const xmpMetadata = {
114+
const metadata = {
113115
version: null,
114116
baseRenditionIsHDR: null,
115117
gainMapMin: null,
@@ -197,18 +199,47 @@ class UltraHDRLoader extends Loader {
197199
/* JPEG Header - no useful information */
198200
} else if ( sectionType === 0xe1 ) {
199201

200-
/* XMP Metadata */
202+
/* APP1: XMP Metadata */
201203

202204
this._parseXMPMetadata(
203205
textDecoder.decode( new Uint8Array( section ) ),
204-
xmpMetadata
206+
metadata
205207
);
206208

207209
} else if ( sectionType === 0xe2 ) {
208210

209-
/* Data Sections - MPF / EXIF / ICC Profile */
211+
/* APP2: Data Sections - MPF / ICC Profile / ISO 21496-1 Metadata */
210212

211213
const sectionData = new DataView( section.buffer, section.byteOffset + 2, section.byteLength - 2 );
214+
215+
// Check for ISO 21496-1 namespace: "urn:iso:std:iso:ts:21496:-1\0"
216+
const isoNameSpace = 'urn:iso:std:iso:ts:21496:-1\0';
217+
if ( section.byteLength >= isoNameSpace.length + 2 ) {
218+
219+
let isISO = true;
220+
for ( let j = 0; j < isoNameSpace.length; j ++ ) {
221+
222+
if ( section[ 2 + j ] !== isoNameSpace.charCodeAt( j ) ) {
223+
224+
isISO = false;
225+
break;
226+
227+
}
228+
229+
}
230+
231+
if ( isISO ) {
232+
233+
// Parse ISO 21496-1 metadata
234+
const isoData = section.subarray( 2 + isoNameSpace.length );
235+
this._parseISOMetadata( isoData, metadata );
236+
continue;
237+
238+
}
239+
240+
}
241+
242+
// Check for MPF
212243
const sectionHeader = sectionData.getUint32( 2, false );
213244

214245
if ( sectionHeader === 0x4d504600 ) {
@@ -277,7 +308,8 @@ class UltraHDRLoader extends Loader {
277308
}
278309

279310
/* Minimal sufficient validation - https://developer.android.com/media/platform/hdr-image-format#signal_of_the_format */
280-
if ( ! xmpMetadata.version ) {
311+
// Version can come from either XMP or ISO metadata
312+
if ( ! metadata.version ) {
281313

282314
throw new Error( 'THREE.UltraHDRLoader: Not a valid UltraHDR image' );
283315

@@ -286,7 +318,7 @@ class UltraHDRLoader extends Loader {
286318
if ( primaryImage && gainmapImage ) {
287319

288320
this._applyGainmapToSDR(
289-
xmpMetadata,
321+
metadata,
290322
primaryImage,
291323
gainmapImage,
292324
( hdrBuffer, width, height ) => {
@@ -315,6 +347,126 @@ class UltraHDRLoader extends Loader {
315347

316348
}
317349

350+
/**
351+
* Parses ISO 21496-1 gainmap metadata from binary data.
352+
*
353+
* @private
354+
* @param {Uint8Array} data - The binary ISO metadata.
355+
* @param {Object} metadata - The metadata object to populate.
356+
*/
357+
_parseISOMetadata( data, metadata ) {
358+
359+
const view = new DataView( data.buffer, data.byteOffset, data.byteLength );
360+
361+
// Skip minimum version (2 bytes) and writer version (2 bytes)
362+
let offset = 4;
363+
364+
// Read flags (1 byte)
365+
const flags = view.getUint8( offset );
366+
offset += 1;
367+
368+
const backwardDirection = ( flags & 0x4 ) !== 0;
369+
const useCommonDenominator = ( flags & 0x8 ) !== 0;
370+
371+
let gainMapMin, gainMapMax, gamma, offsetSDR, offsetHDR, hdrCapacityMin, hdrCapacityMax;
372+
373+
if ( useCommonDenominator ) {
374+
375+
// Read common denominator (4 bytes, unsigned)
376+
const commonDenominator = view.getUint32( offset, false );
377+
offset += 4;
378+
379+
// Read baseHdrHeadroom (4 bytes, unsigned)
380+
const baseHdrHeadroomN = view.getUint32( offset, false );
381+
offset += 4;
382+
hdrCapacityMin = Math.log2( baseHdrHeadroomN / commonDenominator );
383+
384+
// Read alternateHdrHeadroom (4 bytes, unsigned)
385+
const alternateHdrHeadroomN = view.getUint32( offset, false );
386+
offset += 4;
387+
hdrCapacityMax = Math.log2( alternateHdrHeadroomN / commonDenominator );
388+
389+
// Read first channel (or only channel) parameters
390+
const gainMapMinN = view.getInt32( offset, false );
391+
offset += 4;
392+
gainMapMin = gainMapMinN / commonDenominator;
393+
394+
const gainMapMaxN = view.getInt32( offset, false );
395+
offset += 4;
396+
gainMapMax = gainMapMaxN / commonDenominator;
397+
398+
const gammaN = view.getUint32( offset, false );
399+
offset += 4;
400+
gamma = gammaN / commonDenominator;
401+
402+
const offsetSDRN = view.getInt32( offset, false );
403+
offset += 4;
404+
offsetSDR = ( offsetSDRN / commonDenominator ) * 255.0;
405+
406+
const offsetHDRN = view.getInt32( offset, false );
407+
offsetHDR = ( offsetHDRN / commonDenominator ) * 255.0;
408+
409+
} else {
410+
411+
// Read baseHdrHeadroom numerator and denominator
412+
const baseHdrHeadroomN = view.getUint32( offset, false );
413+
offset += 4;
414+
const baseHdrHeadroomD = view.getUint32( offset, false );
415+
offset += 4;
416+
hdrCapacityMin = Math.log2( baseHdrHeadroomN / baseHdrHeadroomD );
417+
418+
// Read alternateHdrHeadroom numerator and denominator
419+
const alternateHdrHeadroomN = view.getUint32( offset, false );
420+
offset += 4;
421+
const alternateHdrHeadroomD = view.getUint32( offset, false );
422+
offset += 4;
423+
hdrCapacityMax = Math.log2( alternateHdrHeadroomN / alternateHdrHeadroomD );
424+
425+
// Read first channel parameters
426+
const gainMapMinN = view.getInt32( offset, false );
427+
offset += 4;
428+
const gainMapMinD = view.getUint32( offset, false );
429+
offset += 4;
430+
gainMapMin = gainMapMinN / gainMapMinD;
431+
432+
const gainMapMaxN = view.getInt32( offset, false );
433+
offset += 4;
434+
const gainMapMaxD = view.getUint32( offset, false );
435+
offset += 4;
436+
gainMapMax = gainMapMaxN / gainMapMaxD;
437+
438+
const gammaN = view.getUint32( offset, false );
439+
offset += 4;
440+
const gammaD = view.getUint32( offset, false );
441+
offset += 4;
442+
gamma = gammaN / gammaD;
443+
444+
const offsetSDRN = view.getInt32( offset, false );
445+
offset += 4;
446+
const offsetSDRD = view.getUint32( offset, false );
447+
offset += 4;
448+
offsetSDR = ( offsetSDRN / offsetSDRD ) * 255.0;
449+
450+
const offsetHDRN = view.getInt32( offset, false );
451+
offset += 4;
452+
const offsetHDRD = view.getUint32( offset, false );
453+
offsetHDR = ( offsetHDRN / offsetHDRD ) * 255.0;
454+
455+
}
456+
457+
// Convert log2 values to linear
458+
metadata.version = '1.0'; // ISO standard doesn't encode version string, use default
459+
metadata.baseRenditionIsHDR = backwardDirection;
460+
metadata.gainMapMin = gainMapMin;
461+
metadata.gainMapMax = gainMapMax;
462+
metadata.gamma = gamma;
463+
metadata.offsetSDR = offsetSDR;
464+
metadata.offsetHDR = offsetHDR;
465+
metadata.hdrCapacityMin = hdrCapacityMin;
466+
metadata.hdrCapacityMax = hdrCapacityMax;
467+
468+
}
469+
318470
/**
319471
* Starts loading from the given URL and passes the loaded Ultra HDR texture
320472
* to the `onLoad()` callback.
@@ -383,7 +535,7 @@ class UltraHDRLoader extends Loader {
383535

384536
}
385537

386-
_parseXMPMetadata( xmpDataString, xmpMetadata ) {
538+
_parseXMPMetadata( xmpDataString, metadata ) {
387539

388540
const domParser = new DOMParser();
389541

@@ -408,28 +560,28 @@ class UltraHDRLoader extends Loader {
408560

409561
const [ gainmapNode ] = xmpXml.getElementsByTagName( 'rdf:Description' );
410562

411-
xmpMetadata.version = gainmapNode.getAttribute( 'hdrgm:Version' );
412-
xmpMetadata.baseRenditionIsHDR =
563+
metadata.version = gainmapNode.getAttribute( 'hdrgm:Version' );
564+
metadata.baseRenditionIsHDR =
413565
gainmapNode.getAttribute( 'hdrgm:BaseRenditionIsHDR' ) === 'True';
414-
xmpMetadata.gainMapMin = parseFloat(
566+
metadata.gainMapMin = parseFloat(
415567
gainmapNode.getAttribute( 'hdrgm:GainMapMin' ) || 0.0
416568
);
417-
xmpMetadata.gainMapMax = parseFloat(
569+
metadata.gainMapMax = parseFloat(
418570
gainmapNode.getAttribute( 'hdrgm:GainMapMax' ) || 1.0
419571
);
420-
xmpMetadata.gamma = parseFloat(
572+
metadata.gamma = parseFloat(
421573
gainmapNode.getAttribute( 'hdrgm:Gamma' ) || 1.0
422574
);
423-
xmpMetadata.offsetSDR = parseFloat(
575+
metadata.offsetSDR = parseFloat(
424576
gainmapNode.getAttribute( 'hdrgm:OffsetSDR' ) / ( 1 / 64 )
425577
);
426-
xmpMetadata.offsetHDR = parseFloat(
578+
metadata.offsetHDR = parseFloat(
427579
gainmapNode.getAttribute( 'hdrgm:OffsetHDR' ) / ( 1 / 64 )
428580
);
429-
xmpMetadata.hdrCapacityMin = parseFloat(
581+
metadata.hdrCapacityMin = parseFloat(
430582
gainmapNode.getAttribute( 'hdrgm:HDRCapacityMin' ) || 0.0
431583
);
432-
xmpMetadata.hdrCapacityMax = parseFloat(
584+
metadata.hdrCapacityMax = parseFloat(
433585
gainmapNode.getAttribute( 'hdrgm:HDRCapacityMax' ) || 1.0
434586
);
435587

@@ -459,7 +611,7 @@ class UltraHDRLoader extends Loader {
459611
}
460612

461613
_applyGainmapToSDR(
462-
xmpMetadata,
614+
metadata,
463615
sdrBuffer,
464616
gainmapBuffer,
465617
onSuccess,
@@ -527,10 +679,10 @@ class UltraHDRLoader extends Loader {
527679
/* HDR Recovery formula - https://developer.android.com/media/platform/hdr-image-format#use_the_gain_map_to_create_adapted_HDR_rendition */
528680

529681
/* 1.8 instead of 2 near-perfectly rectifies approximations introduced by precalculated SRGB_TO_LINEAR values */
530-
const maxDisplayBoost = 1.8 ** ( xmpMetadata.hdrCapacityMax * 0.5 );
682+
const maxDisplayBoost = 1.8 ** ( metadata.hdrCapacityMax * 0.5 );
531683
const unclampedWeightFactor =
532-
( Math.log2( maxDisplayBoost ) - xmpMetadata.hdrCapacityMin ) /
533-
( xmpMetadata.hdrCapacityMax - xmpMetadata.hdrCapacityMin );
684+
( Math.log2( maxDisplayBoost ) - metadata.hdrCapacityMin ) /
685+
( metadata.hdrCapacityMax - metadata.hdrCapacityMin );
534686
const weightFactor = Math.min(
535687
Math.max( unclampedWeightFactor, 0.0 ),
536688
1.0
@@ -539,12 +691,12 @@ class UltraHDRLoader extends Loader {
539691
const sdrData = sdrImageData.data;
540692
const gainmapData = gainmapImageData.data;
541693
const dataLength = sdrData.length;
542-
const gainMapMin = xmpMetadata.gainMapMin;
543-
const gainMapMax = xmpMetadata.gainMapMax;
544-
const offsetSDR = xmpMetadata.offsetSDR;
545-
const offsetHDR = xmpMetadata.offsetHDR;
546-
const invGamma = 1.0 / xmpMetadata.gamma;
547-
const useGammaOne = xmpMetadata.gamma === 1.0;
694+
const gainMapMin = metadata.gainMapMin;
695+
const gainMapMax = metadata.gainMapMax;
696+
const offsetSDR = metadata.offsetSDR;
697+
const offsetHDR = metadata.offsetHDR;
698+
const invGamma = 1.0 / metadata.gamma;
699+
const useGammaOne = metadata.gamma === 1.0;
548700
const isHalfFloat = this.type === HalfFloatType;
549701
const toHalfFloat = DataUtils.toHalfFloat;
550702
const srgbToLinear = this._srgbToLinear;

0 commit comments

Comments
 (0)