gainmap-js

repository·main·Indexed 41 days ago

https://github.com/monogrid/gainmap-js

A JavaScript and TypeScript implementation of Adobe Gain Map technology for storing HDR images using an SDR image and a gain map. Specifically designed for use with three.js, it provides utilities to encode and decode HDR images via WebGL and WebGPU, including support for HDRJPEG and separate file loading through HDRJPGLoader and GainMapLoader. Version 3.4.0.

Tokens
13.8K
Snippets
32
Records
57
Agent score
51%

What's inside gainmap-js

  1. What is a Gain map?

    main

    A Gain map is a technology used to store HDR images by combining an SDR base image with a secondary set of data. This allows for optimized rendering on HDR displays while maintaining compatibility with SDR software.

    A Gain map consists of three components:

    1. Base (default) image: An SDR or HDR image. For JPG gain maps, the base is always an SDR image. If the viewing software does not support gain maps, it simply renders this base image.
    2. Gain map: A secondary "image" (data) embedded in the file that contains instructions to convert each pixel from the base image into an HDR or SDR version.
    3. Gain map metadata: Information that tells the browser how the gain map is encoded and provides critical data for optimizing rendering on different displays.

    Note: While Google refers to this as the "Ultra HDR Image Format" (or JPEGR), this library refers to JPEG files with embedded gain maps as HDRJPEG.

  2. Install @monogrid/gainmap-js and three

    main

    To use this library, you must install both @monogrid/gainmap-js and three. This library is specifically intended for encoding and decoding gain map images for the three.js 3D library. Note that because it depends on three.js, the dependency footprint is significant if you only require gain map functionality.

    $ npm install @monogrid/gainmap-js three
  3. Decode from separate files using WebGL or WebGPU

    main

    If your gain map data is stored in separate files rather than embedded in a JPEG, you can decode them using either WebGL or WebGPU:

    • WebGL: Use the low-level decode function (examples/decode-from-separate-data.ts) or the GainMapLoader (examples/decode-from-separate-data-using-loader.ts).
    • WebGPU: Use the low-level decode function (examples/decode-from-separate-data-webgpu.ts) or the GainMapLoader (examples/decode-from-separate-data-using-loader-webgpu.ts).
  4. Configure KTX2Loader with Basis Universal transcoders

    main

    To use .basis or .ktx2 texture files with KTX2Loader, you must provide the Basis Universal WebAssembly transcoder files. This requires two specific files to be hosted at a reachable path:

    1. basis_transcoder.js (the JavaScript wrapper)
    2. basis_transcoder.wasm (the WebAssembly binary)

    You configure this by calling setTranscoderPath() on your KTX2Loader instance, pointing it to the directory containing these files.

    const ktx2Loader = new KTX2Loader();
    ktx2Loader.setTranscoderPath( 'examples/jsm/libs/basis/' );
    // ...
  5. Decode JPEG with embedded gain maps using WebGL

    main

    You can decode JPEG images that have embedded gain map metadata using WebGL. This can be achieved in two ways:

    1. Low-level approach: Use the low-level decode function directly with WebGL.
    2. High-level approach: Use the HDRJPGLoader for a more abstracted integration.

    Refer to examples/decode-from-jpeg.ts for the low-level implementation and examples/decode-from-jpeg-using-loader.ts for the loader-based implementation.

  6. Decode with WebGPU acceleration

    main

    For improved performance and modern GPU support, use the @monogrid/gainmap-js/webgpu entry point.

    Requirements:

    • three.js r163 or higher
    • A browser that supports WebGPU

    Key differences from WebGL:

    1. Import path: Use @monogrid/gainmap-js/webgpu.
    2. Three.js imports: Use three/webgpu.
    3. Renderer: Use WebGPURenderer and call await renderer.init() before use.
    4. Async decoding: The decode() function is async in WebGPU.
    import { HDRJPGLoader } from '@monogrid/gainmap-js/webgpu'
    import {
      EquirectangularReflectionMapping,
      Mesh,
      MeshBasicMaterial,
      PerspectiveCamera,
      Scene,
      WebGPURenderer
    } from 'three/webgpu'
    
    const renderer = new WebGPURenderer()
    await renderer.init()
    
    const loader = new HDRJPGLoader(renderer)
      .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
    
    const result = await loader.loadAsync('gainmap.jpeg')
    // `result` can be used to populate a Texture
    
    const scene = new Scene()
    const mesh = new Mesh(
      new PlaneGeometry(),
      new MeshBasicMaterial({ map: result.renderTarget.texture })
    )
    scene.add(mesh)
    renderer.render(scene, new PerspectiveCamera())
    
    // Starting from three.js r159
    // `result.renderTarget.texture` can
    // also be used as Equirectangular scene background
    scene.background = result.renderTarget.texture
    
    // result must be manually disposed
    // when you are done using it
    result.dispose()
  7. Decode JPEG with embedded gain maps using WebGPU

    main

    For hardware-accelerated decoding using WebGPU, you can decode JPEG images with embedded gain maps using:

    1. Low-level approach: Use the low-level decode function with WebGPU.
    2. High-level approach: Use the HDRJPGLoader with WebGPU.

    Refer to examples/decode-from-jpeg-webgpu.ts for the low-level implementation and examples/decode-from-jpeg-using-loader-webgpu.ts for the loader-based implementation.

  8. Encode gain maps and JPEG metadata

    main

    The library provides several encoding workflows:

    • From HDR texture: Encode a gain map directly from an HDR texture (examples/encode.ts).
    • With compression: Encode and compress a gain map (examples/encode-and-compress.ts).
    • JPEG Metadata: Encode gain map metadata specifically into a JPEG file (examples/encode-jpeg-metadata.ts).
    • Image Compression: Compress raw image data (examples/compress.ts).
  9. Configure Vite to exclude libultrahdr from optimization

    main

    If you are using Vite and importing @monogrid/gainmap-js/libultrahdr, you must exclude it from dependency optimization in your vite.config.js to avoid issues.

    // vite.config.js
    
    module.exports = defineConfig({
      ...optimizeDeps: {
        exclude: ['@monogrid/gainmap-js/libultrahdr']
      },
      ...
    })
  10. Encode an EXR or HDR file into a JPEG with embedded Gain map

    main

    You can encode high dynamic range images into a single JPEG with an embedded gain map using the encoding utilities. This is useful for creating compatible HDR files from .exr or .hdr sources.

    Workflow:

    1. Load the HDR image (e.g., using Three.js EXRLoader).
    2. Use findTextureMinMax to find the RAW RGB max value.
    3. Call encode() to generate the SDR and Gain map buffers.
    4. Compress the buffers using compress().
    5. Use encodeJPEGMetadata() to combine the compressed images and metadata into a single Uint8Array JPEG.

    Note: You must manually call .dispose() on the encodingResult.gainMap and encodingResult.sdr objects to prevent memory leaks.

    import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js/encode'
    import { encodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr'
    import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js'
    
    // load an HDR file
    const loader = new EXRLoader()
    const image = await loader.loadAsync('image.exr')
    
    // find RAW RGB Max value of a texture
    const textureMax = findTextureMinMax(image)
    
    // Encode the gainmap
    const encodingResult = encode({
      image,
      // this will encode the full HDR range
      maxContentBoost: Math.max.apply(this, textureMax)
    })
    
    // obtain the RAW RGBA SDR buffer and create an ImageData
    const sdrImageData = new ImageData(encodingResult.sdr.toArray(), encodingResult.sdr.width, encodingResult.sdr.height)
    // obtain the RAW RGBA Gain map buffer and create an ImageData
    const gainMapImageData = new ImageData(encodingResult.gainMap.toArray(), encodingResult.gainMap.width, encodingResult.gainMap.height)
    
    // parallel compress the RAW buffers into the specified mimeType
    const mimeType = 'image/jpeg'
    const quality = 0.9
    
    const [sdr, gainMap] = await Promise.all([
      compress({
        source: sdrImageData,
        mimeType,
        quality,
        flipY: true // output needs to be flipped
      }),
      compress({
        source: gainMapImageData,
        mimeType,
        quality,
        flipY: true // output needs to be flipped
      })
    ])
    
    // obtain the metadata which will be embedded into
    // and XMP tag inside the final JPEG file
    const metadata = encodingResult.getMetadata()
    
    // embed the compressed images + metadata into a single
    // JPEG file
    const jpeg = encodeJPEGMetadata({
      ...encodingResult,
      ...metadata,
      sdr,
      gainMap
    })
    
    // `jpeg` will be an `Uint8Array` which can be saved somewhere
    
    // encoder must be manually disposed
    // when no longer needed
    encodingResult.gainMap.dispose()
    encodingResult.sdr.dispose()
  11. Decode using separate files (SDR, Gainmap, and Metadata)

    main

    If you want to avoid the limitations of a single JPEG (like lack of WEBP compression), you can use GainMapLoader to load three separate files:

    1. An SDR Representation file
    2. A Gainmap file
    3. A JSON file containing the gainmap metadata

    Access the texture via result.renderTarget.texture and call result.dispose() when finished.

    import { GainMapLoader } from '@monogrid/gainmap-js'
    import {
      EquirectangularReflectionMapping,
      Mesh,
      MeshBasicMaterial,
      PerspectiveCamera,
      PlaneGeometry,
      Scene,
      WebGLRenderer
    } from 'three'
    
    const renderer = new WebGLRenderer()
    
    const loader = new GainMapLoader(renderer)
      .setRenderTargetOptions({ mapping: EquirectangularReflectionMapping })
    
    const result = await loader.loadAsync(['sdr.jpeg', 'gainmap.jpeg', 'metadata.json'])
    // `result` can be used to populate a Texture
    
    const scene = new Scene()
    const mesh = new Mesh(
      new PlaneGeometry(),
      new MeshBasicMaterial({ map: result.renderTarget.texture })
    )
    scene.add(mesh)
    renderer.render(scene, new PerspectiveCamera())
    
    // Starting from three.js r159
    // `result.renderTarget.texture` can
    // also be used as Equirectangular scene background
    scene.background = result.renderTarget.texture
    
    // result must be manually disposed
    // when you are done using it
    result.dispose()