Search code examples
javascriptgraphicsthree.jsshaderfragment-shader

How to apply HemisphereLight to ShaderMaterial?


I want to add HemisphereLight to light an object which is built with ShaderMaterial. But when I simply add the light to the screen, the object doesn't seem to be affected. In fact, I've tried with all the other lights and no one light is affecting the object. What do I have to set to make the object affected by the lights?

<script src="https://threejs.org/build/three.js"></script>

<script type="module">

var clock = new THREE.Clock()

function main() {

    var renderer = new THREE.WebGLRenderer();

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

    var scene = new THREE.Scene();
    var camera = new THREE.PerspectiveCamera( 200, window.innerWidth/window.innerHeight, 0.1, 1000 );

    var renderer = new THREE.WebGLRenderer();
    renderer.setSize( window.innerWidth, window.innerHeight );
    document.body.appendChild( renderer.domElement );

    var vShader = `
    precision mediump float;
    uniform float uTime;
    void main() {
      float z = 0.1*cos(uTime)   * cos(position.y*40. - uTime) * position.y*sin(log(position.y))*0.5 + 0.5;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position.x, position.y, z, 1.0);
    }
  `
  var fShader = `
  precision mediump float;
    uniform vec2 uRes;
    void main() {
    vec2 uv = 1.5*gl_FragCoord.xy/uRes;
      gl_FragColor = vec4(uv.x, uv.y, 0.0, 1.0);
    }
//   `
        var planeG = new THREE.PlaneGeometry(25, 60, 20, 20);

        var planeM = new THREE.ShaderMaterial({
            uniforms: {
              //ambientLightColor: 0xffffff,
              uTime: {
                type:"f",
                value:0.0,
              },
              uRes: {
                type:"f",
                value:new THREE.Vector2(window.innerWidth, window.innerHeight),
              }
            },
            // wireframe:true,
            //lights:true,
            vertexShader:vShader,
            fragmentShader:fShader,
            side:THREE.DoubleSide,
        });
  
        var plane = new THREE.Mesh( planeG, planeM );
        var light = new THREE.HemisphereLight(0xffffff, 0x000000, 2);
        //var light = new THREE.AmbientLight(0xFFFFFF, 1.0)
        
  
        scene.add(light);
        scene.add(plane);
  
        plane.rotateZ(Math.PI/2)
        camera.position.z = 4;
  
        scene.background = new THREE.Color(0xfffffff);
    var render = function (time) {
          requestAnimationFrame( render );   
          plane.material.uniforms.uTime.value = time/100;
            renderer.render(scene, camera);
    };

    render();
};

main()
    </script>


Solution

  • ShaderMaterial does not respond to lights because you're manually telling the shader what color to output when you assign values to gl_FragColor in the fragment shader. You're overriding the default behavior of materials, so all things like, position, rotation, color are up to you to control.

    If you want a hemisphere light to affect your shader, then you'd have to manually write those calculations in GLSL, which is a pretty complex process. You'd have to feed the colors of the light as uniforms, then you'll have to figure out which triangles are facing up and down so they "reflect" those light colors accordingly.

    Three.js has all its material shaders broken down throughout several files in "ShaderChunks", so it's pretty difficult to track down exactly where each step takes place.