three.jsglslhdrimages

Threejs reflection and glossiness issues with ShaderMaterial


I'm trying to develop my own custom glsl shader for threejs but I'm stack on the glossy reflections with HDR images. It works fine with with LDR images, but not with HDR.

I started by using this example in order to generate the mipmaps.

I then used a browser extension to get the compiled code from the default MeshStandardMaterial and I isolated the bits I needed which were related to the environment maps and I wrote my shader (I've added both radiance and irradiance functions in the shader), but I'm probably missing something as this doesn't seem to work for me.

In the image bellow, you can see that in version 1) which has a plain HDRI (with no mipmaps) I had to change the DataType to "THREE.FloatType". Even though this is still looking wrong (not glossy and very dark), it is the closest I managed to get. Version 2) uses the default "THREE.UnsignedDataType" but the image looks completely wrong. Version 3) which is the version that includes mipmaps, just errors.

enter image description here

Here's a link to download all the files or just have a look in the code below.

<html>
    <head>
        <script src="./js/three.min.js"></script>
        <script src="./js/RGBELoader.js"></script>
        <script src="./js/HDRCubeTextureLoader.js"></script>
        <script src="./js/PMREMCubeUVPacker.js"></script>
        <script src="./js/PMREMGenerator.js"></script>
        <style>
            html, body{ margin:0px; padding: 0px;}
        </style>
    </head>

    <body>
        <!-- Vertex and Fragment shaders-->
        <script id="VS" type="x-shader/x-vertex">
            varying vec3 vNormal;
            varying vec3 vViewPosition;

            void main() {
                vNormal = normal;
                vViewPosition = - (modelViewMatrix * vec4( position, 1.0 )).xyz;

                gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
            }
        </script>
        <script id="FS" type="x-shader/x-fragment">
            varying vec3 vViewPosition;
            varying vec3 vNormal;

            uniform int maxMipLevel;
            uniform samplerCube envMap;
            uniform float envMapIntensity;
            uniform float flipEnvMap;

            uniform float roughness;


            float pow2( const in float x ) {
                return x*x;
            }
            float GGXRoughnessToBlinnExponent( const in float ggxRoughness ) {
                return ( 2.0 / pow2( ggxRoughness + 0.0001 ) - 2.0 );
            }
            float getSpecularMIPLevel( const in float blinnShininessExponent, const in int maxMIPLevel ) {
                float maxMIPLevelScalar = float( maxMIPLevel );
                float desiredMIPLevel = maxMIPLevelScalar + 0.79248 - 0.5 * log2( pow2( blinnShininessExponent ) + 1.0 );
                return clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );
            }
            vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {
                return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );
            }
            vec3 getLightProbeIndirectRadiance( const in vec3 viewDir, const in vec3 normal, const in float blinnShininessExponent, const in int maxMIPLevel ) {
                vec3 reflectVec = reflect( -viewDir, normal );
                reflectVec = inverseTransformDirection( reflectVec, viewMatrix );
                float specularMIPLevel = getSpecularMIPLevel( blinnShininessExponent, maxMIPLevel );

                vec3 queryReflectVec = vec3( flipEnvMap * reflectVec.x, reflectVec.yz );
                vec4 envMapColor = textureCube( envMap, queryReflectVec, specularMIPLevel );
                envMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;

                return envMapColor.rgb * envMapIntensity * .75;
            }
            vec3 getLightProbeIndirectIrradiance( const in vec3 normal, const in int maxMIPLevel ) {
                vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );
                vec3 queryVec = vec3( flipEnvMap * worldNormal.x, worldNormal.yz );
                vec4 envMapColor = textureCube( envMap, queryVec, float( maxMIPLevel ) );

                return PI * envMapColor.rgb * envMapIntensity;
            }


            void main() {
                vec3 irradiance = getLightProbeIndirectIrradiance(normalize(vNormal), maxMipLevel );
                vec3 radiance = getLightProbeIndirectRadiance( normalize( vViewPosition ), normalize(vNormal), GGXRoughnessToBlinnExponent( roughness ), maxMipLevel );

                gl_FragColor = vec4( radiance, 1.0 );
            }

        </script>

        <!-- THREE JS code-->
        <script>
            var CubeTextureLoader = new THREE.CubeTextureLoader();
            var HDRCubeTextureLoader = new THREE.HDRCubeTextureLoader();

            // renderer
            var renderer = new THREE.WebGLRenderer({antialias:true});
            renderer.setClearColor( 0xaaaaaa );
            renderer.setSize( window.innerWidth, window.innerHeight );
            document.body.appendChild( renderer.domElement );

            // scene
            scene = new THREE.Scene();

            // camera
            camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 1000 );
            camera.position.set(0, 0, 40);

            // load HDRIs and Generate mipmaps
            // Code from: https://threejs.org/examples/#webgl_materials_envmaps_hdr
            var hdrCubeRenderTarget;
            var hdrUrls = [ 'px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr' ];
            var hdrCubeMap = new THREE.HDRCubeTextureLoader()
            .setPath( './pisaHDR/' )
            .setDataType( THREE.UnsignedByteType )
            // .setDataType( THREE.FloatType )
            .load( hdrUrls, function () {
                var pmremGenerator = new THREE.PMREMGenerator( hdrCubeMap );
                pmremGenerator.update( renderer );
                var pmremCubeUVPacker = new THREE.PMREMCubeUVPacker( pmremGenerator.cubeLods );
                pmremCubeUVPacker.update( renderer );
                hdrCubeRenderTarget = pmremCubeUVPacker.CubeUVRenderTarget;
                hdrCubeMap.magFilter = THREE.LinearFilter;
                hdrCubeMap.needsUpdate = true;
                pmremGenerator.dispose();
                pmremCubeUVPacker.dispose();
            } );

            // materials
            var stdMtl = new THREE.MeshStandardMaterial( { color: 0xffffff, roughness: 0.5, metalness: 1.0 } );
            var cusMtl = new THREE.ShaderMaterial( {
                defines: {
                    PI: 3.14159265359
                },
                uniforms: {
                    roughness: 0.5,
                    envMapIntensity: { value:1.0 },
                    flipEnvMap: { value: -1.0 },
                    envMap: { value:null }
                },
                vertexShader: document.getElementById('VS').text,
                fragmentShader: document.getElementById('FS').text
            } );

            // geometries
            var sphereGeometry = new THREE.SphereGeometry( 5, 32, 32 );
            var stdSphereMesh = new THREE.Mesh( sphereGeometry, stdMtl );
            var cusSphereMesh = new THREE.Mesh( sphereGeometry, cusMtl );
            stdSphereMesh.position.set(-7.5, 0, 0);
            cusSphereMesh.position.set(7.5, 0, 0);

            scene.add( stdSphereMesh );
            scene.add( cusSphereMesh );

            // render scene
            function render() {
                var newEnvMap = hdrCubeRenderTarget ? hdrCubeRenderTarget.texture : null;
                if ( newEnvMap && newEnvMap !== stdSphereMesh.material.envMap ) {
                    stdSphereMesh.material.envMap = newEnvMap;
                    stdSphereMesh.material.needsUpdate = true;

                    cusSphereMesh.material.uniforms.envMap.value = newEnvMap; // This isErroring
                    // cusSphereMesh.material.uniforms.envMap.value = hdrCubeMap; // Result show HDRI but with wrong gamma and no mipmaps
                    cusSphereMesh.material.needsUpdate = true;
                }

                requestAnimationFrame( render );
                renderer.render( scene, camera );
            }
            render();

        </script>

    </body>
</html>

Solution

  • You have to change your envMapTexelToLinear() function to RGBEToLinear() in the fragment shader.

    This is something the WebGLProgram usually does automatically when compiling shaders and it finds envMap.encoding = THREE.RGBEEncoding. However, since you're using a custom shader, and not a default material, you have to write this step manually.

    Here's a quick example with an HDRI image. On the right I'm reading the texels in linear space, and it yields the funky oversaturated colors. On the left, I'm converting the texels from RGBE to Linear, and it's nicely mapped to its expected output: enter image description here