Three.js: Blurred Environment Map

Created on 26 Jun 2020  路  13Comments  路  Source: mrdoob/three.js

On using HDR loader and PMREMGenerator as in the example https://threejs.org/examples/?q=tone#webgl_tonemapping
you will always get the scene background blurred no matter what the size of the HDR you are using. I tried 1k,4k and 8k hdr texture from https://hdrihaven.com/hdri/?h=small_cathedral, I still get the same blurred background.

Three.js version
  • [ ] Dev
  • [x ] r118
  • [ ] ...
Browser
  • [x] All of them
  • [ ] Chrome
  • [ ] Firefox
  • [ ] Internet Explorer
OS
  • [x] All of them
  • [ ] Windows
  • [ ] macOS
  • [ ] Linux
  • [ ] Android
  • [ ] iOS

Most helpful comment

@ManishJu This is a two fold problem.

The low res background happens because, in the example, we are setting the output of PMREMGenerator directly to the background. Currently, the highest MIP level of the generated texture is fixed and limited to 256px, so no matter how high quality the original texture is, the background will always be 256px. Good news is that I'm currently working on a solution to allow customizable resolution for the max MIP level, so we should have an out of the box solution for this very soon.

The other point of view, is that we should be setting the equirectangular original texture to the background, not the PMREM version. However, the internal shader does not know how to deal with equirectangular textures, but IMO it should.
I'll take a look at it, to see if there's something simple that can be done about that.

All 13 comments

So you are expecting a sharp background?

Yes 4k and above used to give very sharp and clean background before in THREE version 110. Here is the comparison of a portion of the 4k hdr in each

Screenshot from 2020-06-26 11-18-15
Screenshot from 2020-06-26 11-00-54

@ManishJu This is a two fold problem.

The low res background happens because, in the example, we are setting the output of PMREMGenerator directly to the background. Currently, the highest MIP level of the generated texture is fixed and limited to 256px, so no matter how high quality the original texture is, the background will always be 256px. Good news is that I'm currently working on a solution to allow customizable resolution for the max MIP level, so we should have an out of the box solution for this very soon.

The other point of view, is that we should be setting the equirectangular original texture to the background, not the PMREM version. However, the internal shader does not know how to deal with equirectangular textures, but IMO it should.
I'll take a look at it, to see if there's something simple that can be done about that.

However, the internal shader does not know how to deal with equirectangular textures, but IMO it should.

Related #9733.

I also remember that @mrdoob lately mentioned the idea to introduce THREE.EquirectTexture (unfortunately I don't find the comment right now). I'm not sure @WestLangley is already experimenting in this context so it's maybe good to clarify this in order to avoid redundant work (and PRs).

However, the internal shader does not know how to deal with equirectangular textures, but IMO it should.

Related #9733.

I see, however, I find that the approaches suggested by that thread are no longer relevant in our current design. We wouldn't need to create a cube map from an equirectangular texture. It's really just a simple 2 liner fix.

https://github.com/mrdoob/three.js/blob/7df06a0e4054247f87a7b5aa555ca97cd6b15662/src/renderers/webgl/WebGLBackground.js#L59
We just add || background.mapping === EquirectangularReflectionMapping to this line, so that WebGLBackground understand that it can read an equirectangular texture. And, inside the shader, we also include #include <common>, so that it understands equirectUv. That's it.

Here's how it looks: https://rawcdn.githack.com/sciecode/three.js/0ac95becdc228f836b60caeffa41b3e84c39b7ea/examples/webgl_tonemapping.html

I don't see any seams, or any other problems exposed by that thread. What are your guys opinion on this temporary fix? @WestLangley @Mugen87 ?

I also remember that @mrdoob lately mentioned the idea to introduce THREE.EquirectTexture (unfortunately I don't find the comment right now).

In working on the CUBE_UV I find that it would also be beneficial to have a CubeUVTexture kind of class, because we would need to keep track of the max mip level allowed for the texture, which is not something we do in the current Texture class. I also would like to re-use maxMipLevel uniform for this purpose, however this uniform is usually set internally, so it's kind of tricky. I'll create a WIP PR later on to discuss these things, just giving more insights to support the idea of creating different texture classes for the different textures mappings.

@ManishJu See if https://github.com/mrdoob/three.js/issues/9733#issuecomment-588996459 works for you.

Thanks @WestLangley it worked fine. I changed the background precison from 512px to 4096px and it looks amazing.
One thing I noticed was that the minification filter THREE.LinearMipmapLinearFilter did not work properly and produced numerous pattern(s) (lighting bolt) on the hdr (white in this case) whereas the THREE.LinearFilter produced a smooth background as can be seen in the below images :
Screenshot_2020-06-28 ZEG ai(1)
Screenshot_2020-06-28 ZEG ai

I will just start another issue for this if you guys want.

@ManishJu See https://github.com/mrdoob/three.js/issues/19716#issuecomment-648242588 and try NearestFilter.

@WestLangley thanks for your input. Changing magnification filter has no effect for me. The minification filter introduces the artifacts. In short the first 2 code snippets below give results as that of image 1 posted above and the last 2 produces image 2 from above :

//displays correctly
 const cubeRenderOpts = {
        format: THREE.RGBAFormat,
        generateMipmaps: true,
        magFilter: THREE.LinearFilter,
        minFilter: THREE.LinearFilter 
    }
//displays correctly
const cubeRenderOpts = {
        format: THREE.RGBAFormat,
        generateMipmaps: true,
        magFilter: THREE.NearestFilter,
        minFilter: THREE.LinearFilter 
    }

//cause of  banding artifact
const cubeRenderOpts = {
        format: THREE.RGBAFormat,
        generateMipmaps: true,
        magFilter: THREE.NearestFilter,
        minFilter: THREE.LinearMipmapLinearFilter 
    }
//cause of  banding artifact
  const cubeRenderOpts = {
        format: THREE.RGBAFormat,
        generateMipmaps: true,
        magFilter: THREE.LinearFilter,
        minFilter: THREE.LinearMipMapLinearFilter 
    }

Please show all your code so it is clear what you are doing. A live example is preferable.

@WestLangley thanks for waiting, below is the complete code. It is directly modified version of example "webgl_tonemapping". You can change line 121 with the 4 options already present.

 <!DOCTYPE html>
<html lang="en">
    <head>
        <title>three.js webgl - tone mapping</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
        <link type="text/css" rel="stylesheet" href="main.css">
    </head>

    <body>
        <div id="info">
            <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Tone Mapping<br />
            Battle Damaged Sci-fi Helmet by
            <a href="https://sketchfab.com/theblueturtle_" target="_blank" rel="noopener">theblueturtle_</a><br />
            <a href="https://hdrihaven.com/hdri/?h=royal_esplanade" target="_blank" rel="noopener">Royal Esplanade</a> by <a href="https://hdrihaven.com/" target="_blank" rel="noopener">HDRI Haven</a>
        </div>

        <script type="module">

            import * as THREE from '../build/three.module.js';

            import { OrbitControls } from './jsm/controls/OrbitControls.js';
            import { RGBELoader } from './jsm/loaders/RGBELoader.js';

            var  renderer, scene, camera, controls;
            var gui, guiExposure = null;

            var params = {
                exposure: 1.0,
                toneMapping: 'ACESFilmic'
            };

            init();


            async function init() {

                renderer = new THREE.WebGLRenderer( { antialias: true } );
                renderer.setPixelRatio( window.devicePixelRatio );
                renderer.setSize( window.innerWidth, window.innerHeight );
                document.body.appendChild( renderer.domElement );

                renderer.toneMapping =THREE.ACESFilmicToneMapping;
                renderer.toneMappingExposure = params.exposure;

                renderer.outputEncoding = THREE.sRGBEncoding;

                // Set CustomToneMapping to Uncharted2
                // source: http://filmicworlds.com/blog/filmic-tonemapping-operators/

                THREE.ShaderChunk.tonemapping_pars_fragment = THREE.ShaderChunk.tonemapping_pars_fragment.replace(
                    'vec3 CustomToneMapping( vec3 color ) { return color; }',
                    `#define Uncharted2Helper( x ) max( ( ( x * ( 0.15 * x + 0.10 * 0.50 ) + 0.20 * 0.02 ) / ( x * ( 0.15 * x + 0.50 ) + 0.20 * 0.30 ) ) - 0.02 / 0.30, vec3( 0.0 ) )
                    float toneMappingWhitePoint = 1.0;
                    vec3 CustomToneMapping( vec3 color ) {
                        color *= toneMappingExposure;
                        return saturate( Uncharted2Helper( color ) / Uncharted2Helper( vec3( toneMappingWhitePoint ) ) );
                    }`
                );

                scene = new THREE.Scene();

                camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 20 );
                camera.position.set( - 1.8, 0.6, 2.7 );

                controls = new OrbitControls( camera, renderer.domElement );
                controls.addEventListener( 'change', render ); // use if there is no animation loop
                controls.enableZoom = false;
                controls.enablePan = false;
                controls.target.set( 0, 0, - 0.2 );
                controls.update();

                var pmremGenerator = new THREE.PMREMGenerator( renderer );
                pmremGenerator.compileEquirectangularShader();

                var rgbeLoader = new RGBELoader()
                    .setDataType( THREE.UnsignedByteType )
                    .setPath( 'textures/equirectangular/' );


                var [ texture,  ] = await Promise.all( [
                    rgbeLoader.loadAsync( 'small_cathedral_4k.hdr' ),

                ] );

                // environment
                //displays correctly
                const cubeRenderOpts1 = {
                        format: THREE.RGBAFormat,
                        generateMipmaps: true,
                        magFilter: THREE.LinearFilter,
                        minFilter: THREE.LinearFilter 
                    }

                //displays correctly
                const cubeRenderOpts2 = {
                        format: THREE.RGBAFormat,
                        generateMipmaps: true,
                        magFilter: THREE.NearestFilter,
                        minFilter: THREE.LinearFilter 
                    }

                //cause of  banding artifact
                const cubeRenderOpts3 = {
                        format: THREE.RGBAFormat,
                        generateMipmaps: true,
                        magFilter: THREE.NearestFilter,
                        minFilter: THREE.LinearMipmapLinearFilter 
                    }

                //cause of  banding artifact
                const cubeRenderOpts4 = {
                        format: THREE.RGBAFormat,
                        generateMipmaps: true,
                        magFilter: THREE.LinearFilter,
                        minFilter: THREE.LinearMipMapLinearFilter 
                    }



                var envMap = new THREE.WebGLCubeRenderTarget( 4096, cubeRenderOpts2 ).fromEquirectangularTexture( renderer, texture );

                scene.background = envMap;
                scene.environment = envMap;

                texture.dispose();
                pmremGenerator.dispose();


                render();

                window.addEventListener( 'resize', onWindowResize, false );

            }


            function onWindowResize() {

                camera.aspect = window.innerWidth / window.innerHeight;

                camera.updateProjectionMatrix();

                renderer.setSize( window.innerWidth, window.innerHeight );

                render();

            }

            function render() {

                renderer.render( scene, camera );

            }

        </script>

    </body>
</html>

As I said, you will get artifacts applying linear filtering to RGBE-encoded textures.

The only reason linear filtering may work in your case is because you are setting the cube size to 4096, which is likely larger than the resolution of your monitor.

For rgbe-encoded textures, you should set

generateMipmaps: false
magFilter: THREE.NearestFilter
minFilter: THREE.NearestFilter

Closing. Let's track the support for equirectangular background textures in #9733.

The current workaround is the usage of WebGLCubeRenderTarget.fromEquirectangularTexture().

Was this page helpful?
0 / 5 - 0 ratings