Can HDR be compressed?

Hello everyone, I have a question to ask everyone.
I used an HDR to apply to the background of the scene and model. It can render objects very beautifully.

   const rgbeLoader = new RGBELoader();
                rgbeLoader.load("sunflowers_puresky_4k.hdr", (texture) => {
                    texture.mapping = THREE.EquirectangularReflectionMapping;

                    scene.background = texture;
                    scene.environment = texture;

                    cube.material.envMap = texture;
                    cube.material.needsUpdate = true;
                    this.render();
                });

There is a question here, which is the HDR file. Its size is too large (20M).
Is it possible to compress it and maintain its original effect?
I hope to receive your help, thank you!

first and foremost, check out the resolution. do you need it to be 4k?

You can give HDR JPG a try:

https://threejs.org/examples/webgl_loader_texture_hdrjpg

Some background information about this format can be found at GitHub:

2 Likes

Other options:

  • keep as .hdr, apply zstd or gzip compression to file
  • convert to .exr, try out its (many) compression types, OpenImageIO is a good CLI for this
  • convert to .ktx2, try out its (many) compression types, KTX-Software is a good CLI for this
2 Likes

you can compress hdr with imagemagic, we’re doing it with an automated process here GitHub - pmndrs/assets: 📦 Importable base64 encoded CC0 assets a typical polyhaven hdr (~1-2mb) will be ~100-200kb, resized to 512x512 and converted to EXR with DWAB compression. the makefile contains the commands assets/Makefile at e46e0fc9ebb5faff61d19dabdb5c2fdbabb75ad0 · pmndrs/assets · GitHub

ps, jpg/webp looks impressive, might be better than exr + dwab, i don’t know.

2 Likes

is this is same as the Ultra HDR that model viewer now supports?

Yes, the three.js example as well as model-viewer both use @monogrid/gainmap-js.

Hello, I have used this method.
But the effect is not ideal.
This is a comparison chart.
As you can see Using HDR_JPG to apply background, the image displays jagged.
But using HDR is relatively smooth.


My steps are:

  1. Download a 4K HDR file (sunflowers_puresky_4k. hdr)
  2. Use tools to convert it to HDR_JPG (sunflowers_puresky_4k. jpg)
  3. Refer to the official code of gainmap js

This is my code:

<template>
    <div>
    </div>
</template>

<script>

    import * as THREE from 'three';
    import {GUI} from 'three/examples/jsm/libs/lil-gui.module.min.js';
    import Stats from 'three/examples/jsm/libs/stats.module.js';
    import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js';
    import {RGBELoader} from 'three/examples/jsm/loaders/RGBELoader.js';
    import {GainMapLoader, HDRJPGLoader} from '@monogrid/gainmap-js';

    const params = {
        envMap: 'HDR JPG',
        roughness: 0.0,
        metalness: 1.0,
        exposure: 1.0
    };

    let container, stats;
    let camera, scene, renderer, controls;
    let torusMesh;
    let hdrJpg, hdrJpgPMREMRenderTarget, hdrJpgEquirectangularMap;
    let gainMap, gainMapPMREMRenderTarget, gainMapBackground;
    let hdrPMREMRenderTarget, hdrEquirectangularMap;

    export default {
        mounted() {
            this.init();

            this.animate();

            // 监听窗口变化
            window.addEventListener("resize", () => {
                // 重置渲染器宽高比
                renderer.setSize(window.innerWidth, window.innerHeight);
                // 重置相机宽高比
                camera.aspect = window.innerWidth / window.innerHeight;
                // 更新相机投影矩阵
                camera.updateProjectionMatrix();
            });
        },
        methods: {
            animate() {
                requestAnimationFrame(this.animate);
                stats.begin();
                this.render();
                stats.end();
            },
            render() {

                let pmremRenderTarget, equirectangularMap;

                switch (params.envMap) {
                    case 'HDR JPG':
                        pmremRenderTarget = hdrJpgPMREMRenderTarget;
                        equirectangularMap = hdrJpgEquirectangularMap;
                        break;
                    case 'Webp Gain map (separate)':
                        pmremRenderTarget = gainMapPMREMRenderTarget;
                        equirectangularMap = gainMapBackground;
                        break;
                    case 'HDR':
                        pmremRenderTarget = hdrPMREMRenderTarget;
                        equirectangularMap = hdrEquirectangularMap;
                        break;
                }

                torusMesh.material.roughness = params.roughness;
                torusMesh.material.metalness = params.metalness;

                scene.environment = equirectangularMap;
                scene.background = equirectangularMap;
                renderer.toneMappingExposure = params.exposure;

                renderer.render(scene, camera);
            },
            init() {
                //
                container = document.createElement('div');
                document.body.appendChild(container);

                //
                camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 500);
                camera.position.set(0, 0, -120);

                //
                scene = new THREE.Scene();

                //
                renderer = new THREE.WebGLRenderer();
                renderer.toneMapping = THREE.ACESFilmicToneMapping;
                renderer.setPixelRatio(window.devicePixelRatio);
                renderer.setSize(window.innerWidth, window.innerHeight);
                container.appendChild(renderer.domElement);

                // let geometry = new THREE.TorusKnotGeometry(18, 8, 200, 40, 1, 3);
                let geometry = new THREE.BoxGeometry(10, 10, 10);

                let material = new THREE.MeshStandardMaterial({
                    color: 0xffffff,
                    metalness: params.metalness,
                    roughness: params.roughness
                });
                torusMesh = new THREE.Mesh(geometry, material);
                scene.add(torusMesh);
                geometry = new THREE.PlaneGeometry(200, 200);
                material = new THREE.MeshBasicMaterial();
                const pmremGenerator = new THREE.PMREMGenerator(renderer);
                pmremGenerator.compileEquirectangularShader();

                THREE.DefaultLoadingManager.onLoad = function () {
                    pmremGenerator.dispose();
                };

                hdrJpg = new HDRJPGLoader(renderer)
                    .load('textures/sunflowers_puresky_4k.jpg', function () {
                        hdrJpgEquirectangularMap = hdrJpg.renderTarget.texture;
                        hdrJpgPMREMRenderTarget = pmremGenerator.fromEquirectangular(hdrJpgEquirectangularMap);
                        hdrJpgEquirectangularMap.mapping = THREE.EquirectangularReflectionMapping;
                        hdrJpgEquirectangularMap.needsUpdate = true;
                        hdrJpg.dispose();

                    }, function (progress) {
                        console.log('jpg', progress);
                    });

                gainMap = new GainMapLoader(renderer)
                    .load([
                        'textures/gainmap/spruit_sunrise_4k.webp',
                        'textures/gainmap/spruit_sunrise_4k-gainmap.webp',
                        'textures/gainmap/spruit_sunrise_4k.json'
                    ], function () {
                        gainMapBackground = hdrJpg.renderTarget.texture;
                        gainMapPMREMRenderTarget = pmremGenerator.fromEquirectangular(gainMapBackground);
                        gainMapBackground.mapping = THREE.EquirectangularReflectionMapping;
                        gainMapBackground.needsUpdate = true;
                        gainMap.dispose();
                    }, function (progress) {
                    });

                hdrEquirectangularMap = new RGBELoader()
                    .load('textures/sunflowers_puresky_4k.hdr', function () {
                        hdrPMREMRenderTarget = pmremGenerator.fromEquirectangular(hdrEquirectangularMap);
                        hdrEquirectangularMap.mapping = THREE.EquirectangularReflectionMapping;
                        hdrEquirectangularMap.minFilter = THREE.LinearFilter;
                        hdrEquirectangularMap.magFilter = THREE.LinearFilter;
                        hdrEquirectangularMap.needsUpdate = true;
                    }, function (progress) {
                    });

                //
                controls = new OrbitControls(camera, renderer.domElement);
                controls.minDistance = 50;
                controls.maxDistance = 300;

                //
                stats = new Stats();
                container.appendChild(stats.dom);

                //
                const gui = new GUI();
                gui.add(params, 'envMap', ['HDR JPG', 'Webp Gain map (separate)', 'HDR']).onChange();
                gui.add(params, 'roughness', 0, 1, 0.01);
                gui.add(params, 'metalness', 0, 1, 0.01);
                gui.add(params, 'exposure', 0, 2, 0.01);
                gui.open();
            },
        },
    };
</script>

Excuse me, is there an error in any step?

The problem was ultimately resolved, and here is the code

1 Like

hey @drcmda do you know if R3F / drei currently supports ( or plans to support ) hooks for { GainMapLoader, HDRJPGLoader }?

you can do

import { useLoader } from '@react-three/fiber'
import { GainMapLoader } from ???

...
const result = useLoader(GainMapLoader, url)

haven’t gotten to trying gainmap out myself. more convenient drei hooks would definitively help.

1 Like

Good shout, I’ll give that a try, the official three webp gain map format looks ideal in terms of file size and resolution quality… 4k =~ 1.6MB

i have a working box now https://codesandbox.io/p/sandbox/sleepy-jackson-hqct6n?file=%2Fsrc%2FApp.js%3A33%2C41

btw i think the official example can’t be accurate, @Mugen87 could you check?

is pmremgen needed? it seems to work without. also the webp gainmap isn’t using the gainmap but the hdr-jpg, this seems to be a typo.

gainMap = new GainMapLoader(renderer)
  .load( [
    'textures/gainmap/spruit_sunrise_4k.webp',
    'textures/gainmap/spruit_sunrise_4k-gainmap.webp',
    'textures/gainmap/spruit_sunrise_4k.json'
  ], () => {
    gainMapBackground = hdrJpg.renderTarget.texture // ❌ ?
    gainMapPMREMRenderTarget = pmremGenerator.fromEquirectangular(gainMapBackground)

it should most likely be

    gainMapBackground = gainMap.renderTarget.texture
    gainMapBackground.mapping = THREE.EquirectangularReflectionMapping
    ...
    scene.background = gainMapBackground
    scene.environment = gainMapBackground
1 Like

Explicit usage isn’t required. You can directly pass in the renderTarget.texture to the background or environment. But the mapping of the texture should be THREE.EquirectangularReflectionMapping so the internal PMREMGenerator knows it’s a equirectangular texture.

Yes, it should be gainMapBackground = gainMap.renderTarget.texture;. I’ll file a PR (Examples: Clean up. by Mugen87 · Pull Request #27997 · mrdoob/three.js · GitHub).

1 Like

@Lawrence3DPK it’s up https://codesandbox.io/p/sandbox/sleepy-jackson-hqct6n?file=%2Fsrc%2FApp.js%3A10%2C28

refreshed docs GitHub - pmndrs/drei: 🥉 useful helpers for react-three-fiber

monogrid was kind enough to expose some internal (setRenderer) and they were interested in having it in drei. this will for sure boost the signal and help people to get bundles down.

1 Like

@drcmda awesome news that’s great work :ok_hand:

@Mugen87 can it be that envMapIntensity is broken in latest threejs? the setting does nothing any longer.

@drcmda migration guide 162->163 may be related…

wow. that’s another hard breaking change issued as a minor. no wonder our end to end snapshot CIs are crashing. updating three is like russian roulette, maybe it works, maybe it doesn’t, maybe it breaks eventually. :tada: i have no problem with the changes, but knowing that something breaks would be nice, the whole tooling eco system is prepared to do that ootb if a package sticks to semver.

So what would be the migration steps to get this working again like it did in 162- ?