0

I'm trying to implement the Cook-Torrance shading algorithm in three.js I have a mostly working solution, however it doesn't show the effects of the ambient light. The sides of the cube not illuminated by the light are completely black. If I remove the "Beckmann term" then I can indeed see the ambient light effect:

Image of the cube with completely back faces when not illuminated

While, replacing Beckmann with the function that always return 0.0 I get:

Image of the cube with ambient term added to non-illuminated faces.


It seems like the cause of the wrong behaviour is the division in:

vec3 Specular = (Beckmann(NdotH) * G(NdotH, NdotV, VdotH, NdotL) * R_F(VdotH)) / ( NdotL* NdotV);

If I modify NdotL * NdotV to NdotV and change the computation for gl_FragColor to:

gl_FragColor = vec4(beta * NdotL * (1.0-s)*Kd + beta * s*Specular + ambient*Kd, 1.0);

Everything seems to work correctly.

What I don't understand is: why? This problem with the division isn't mentioned anywhere, and I'm not 100% sure that even the remaining division wont cause problems in other situations.


Here's the full MWE:

<html>
    <head>
        <title>Cook-Torrance BRDF computed by shader</title>
        <style>

        body {
            font-family: Monospace;
            background-color: #f0f0f0;
            margin: 0px;
            overflow: hidden;
        }

        canvas {
            width: 100%;
            height: 100%;
        }

    </style>
        <script src="lib/three.min.js"></script>
        <script src="lib/OrbitControls.js"></script>
    </head>
    <body>

        <script type="text/x-glsl" id="vertex">
        varying vec3 transformedNormal;
        varying vec3 pointPosition;
        varying vec3 lightVector;
        uniform vec3 pointLightPosition;

        void main()
        {
            transformedNormal = normalMatrix * normal;
            pointPosition = (modelViewMatrix * vec4( position, 1.0 )).xyz;
            vec4 lPosition = viewMatrix * vec4( pointLightPosition, 1.0 );
            lightVector = lPosition.xyz - pointPosition;
            gl_Position = projectionMatrix * vec4(pointPosition,1.0);
        }
        </script>

        <script type="text/x-glsl" id="ct-fragment">
            uniform vec3 lightPower;
            uniform vec3 ambient;
            uniform vec3 Kd; // surface diffuse color
            uniform vec3 Ks; // surface specular color: equal to R_F(0)
            uniform float m; // material roughness (average slope of microfacets)
            uniform float s; // percentage of incoming light which is specularly reflected

            varying vec3 transformedNormal;
            varying vec3 pointPosition;
            varying vec3 lightVector;

            #define PI 3.14159265

            float G(float NdotH, float NdotV, float VdotH, float NdotL)
            {
                float G1 = 2.0 * NdotH * NdotV / VdotH;
                float G2 = 2.0 * NdotH * NdotL / VdotH;
                return min( 1.0, min( G1, G2 ));
            }

            vec3 R_F(float VdotH) {
                return Ks + (1.0 - Ks)*pow(1.0-VdotH, 5.0);
            }

            float Beckmann(float NdotH){
                float A = 1.0 / (pow(m,2.0)+pow(NdotH,4.0)*PI);
                float B = exp( - pow( tan(acos(NdotH)) , 2.0) / pow(m,2.0));
                return A*B;
            }

            void main()
            {
                vec3  n                 = normalize( transformedNormal );
                vec3  v                 = normalize( -pointPosition );
                vec3  l                 = normalize(  lightVector );
                vec3  h                 = normalize( v+l );
                float  NdotH            = max(0.0, dot( n, h ));
                float  VdotH            = max(0.0, dot( v, h ));
                float  NdotV            = max(0.0, dot( n, v ));
                float  NdotL            = max(0.0, dot( n, l ));
                // specular BRDF
                vec3 Specular = (Beckmann(NdotH) * G(NdotH, NdotV, VdotH, NdotL) * R_F(VdotH)) / ( NdotL* NdotV);
                vec3 beta = lightPower / ( 4.0  * PI * pow( length(lightVector),2.0) );
                gl_FragColor = vec4(beta * NdotL * ((1.0-s)*Kd + s*Specular) + ambient*Kd, 1.0);
            }
        </script>


        <script>
            var scene = new THREE.Scene();
            var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
            camera.position = new THREE.Vector3(0,0,5);

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

            controls = new THREE.OrbitControls(camera, renderer.domElement);
            controls.target.set(0, 0, 0);

            var uniforms = {
                        Ks: { type: "v3", value: new THREE.Vector3() },
                        Kd: { type: "v3", value: new THREE.Vector3() },
                        ambient:    { type: "v3", value: new THREE.Vector3() },
                        pointLightPosition: { type: "v3", value: new THREE.Vector3() },
                        lightPower: { type: "v3", value: new THREE.Vector3() },
                        s: {type: "f", value: 0},
                        m: {type: "f", value: 0}
                    };

            var vs = document.getElementById("vertex").textContent;
            var fs = document.getElementById("ct-fragment").textContent;

            var material = new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: vs, fragmentShader: fs });

            var geometry = new THREE.CubeGeometry(1, 1, 1);
            var mesh = new THREE.Mesh(geometry, material);
            scene.add(mesh);

            light = new THREE.Mesh( new THREE.SphereGeometry( 1, 16, 16), new THREE.MeshBasicMaterial ({color: 0xffff00, wireframe:true}));
            light.position = new THREE.Vector3( 10.0, 10.0, 10.0 );
            scene.add( light );

            uniforms.Ks.value = new THREE.Vector3( 0.95, 0.93, 0.88 );
            uniforms.Kd.value = (new THREE.Vector3( 0.50754, 0.50754, 0.50754 ));
            uniforms.ambient.value = (new THREE.Vector3( 0.5, 0.5, 0.5 ));
            uniforms.pointLightPosition.value = new THREE.Vector3(light.position.x, light.position.y, light.position.z);
            uniforms.lightPower.value = new THREE.Vector3( 7000.0, 7000.0, 7000.0 );
            uniforms.s.value = 0.5;
            uniforms.m.value = 0.1;

            function animate() {

                requestAnimationFrame( animate );
                render();

            }

            function render() {
                controls.update();
                renderer.render(scene, camera);
            }

            animate();
        </script>
    </body>
</html>
Bakuriu
  • 98,325
  • 22
  • 197
  • 231
  • implementation found here: http://ruh.li/GraphicsCookTorrance.html – gaitat Jun 07 '14 at 14:06
  • Tip: do not instantiate a new `Vector3` when you want to change a value. Use `vector.set( x, y, z )` or `vector.copy( vector2 )`. – WestLangley Jun 07 '14 at 16:42
  • what happens if you clamp your specular to 0,1? – pailhead Jun 08 '14 at 22:26
  • @pailhead Just tried to put the `Specular[i] = max(0.0, Specular[i]);` for all indices and I don't see any change. The cube faces are still completely black. – Bakuriu Jun 09 '14 at 06:50
  • what does for all indecis mean? – pailhead Jun 09 '14 at 06:52
  • @pailhead I mean, since `Specular` is a `vec3` I'm clamping all its components. – Bakuriu Jun 09 '14 at 06:54
  • you are modifying the shader, why don't you try breaking up this so they are not so much inline, then put breakpoints, put the results in gl_fragcolor, see what might be wrong, use abs to isolate negative values – pailhead Jun 09 '14 at 06:56
  • If ambient*kD shows up on its own, then you've got something negative in the remainder of the equation. – pailhead Jun 09 '14 at 07:02

1 Answers1

1

The shading equation is a mathematical description of the Cook-Torrance shading model. Writing an actual shader is a different thing that should take into account the fact that not all operations between floats have the same properties of the real mathematical operations in the equation.

In this case diving by 0 causes problems. In fact the problem is that the definition of Specular is diving by 0, but when assigning to gl_FragColor I'm multiplying again by NdotL obtaining 0 * inf = NaN, and it seems like NaN is interpreted as a zero/negative number by the GPU (thus displaying black).


As a reference, the correct main() is:

void main()
{
    vec3  n = normalize( transformedNormal );
    vec3  v = normalize( -pointPosition );
    vec3  l = normalize(  lightVector );
    vec3  h = normalize( v+l );

    vec3 specular = vec(0.0, 0.0, 0.0);           
    float  NdotH = max(0.0, dot( n, h ));
    float  VdotH = max(0.0, dot( v, h ));
    float  NdotV = max(0.0, dot( n, v ));
    float  NdotL = max(0.0, dot( n, l ));
    if (NdotL > 0 && NdotV > 0) 
    {
        specular = (Beckmann(NdotH) * G(NdotH, NdotV, VdotH, NdotL) * R_F(VdotH)) / ( NdotL* NdotV);
    }
    vec3 beta = lightPower / ( 4.0  * PI * pow( length(lightVector),2.0) );
    gl_FragColor = vec4(beta * NdotL * ((1.0-s)*Kd + s*specular) + ambient*Kd, 1.0);
}
Bakuriu
  • 98,325
  • 22
  • 197
  • 231