javascriptthree.jslightingphong

THREE.js: Simulating MeshBasicMaterial while allowing colored lights


I am making a dungeon crawler type game using three.js. I was using MeshBasicMaterial to make everything "truebright" to make the dungeon consistently visible throughout. However, I wanted to add "bonus" lights coming through under doorways or slits in the wall to give atmosphere. But light doesn't display on BasicMaterial, so I switched over to Phong to test out the light on my floor. Now my floor is black! Most likely because there is no global light.

Is there any way to simulate the properties of MeshBasicMaterial while allowing different colored lights? The dungeon is closed on all four sides, so I imagine that putting an extremely large global light would cast shadows everywhere or override the colors on the ground.

Not the main focus of my question but additionally: how do I make it so the light is stopped by walls instead of just clipping through them? The walls are just 1x1x1 3d mesh cubes spawned by a mapping system.

With MeshBasicMaterial

With MeshPhongMaterial

With MeshPhongMaterial


Solution

  • Once you switch to MeshPhongMaterial, the material becomes shaded. You can set some parameters to being it closer to MeshBasicMaterial, but you're still going to get gradient lighting, which is really what you're looking for for your "bonus" lighting anyway. In the code below, I set the shininess property to 0, which eliminates the hard light effect of the Phong shading.

    To get the light to not bleed through your walls, you'll need to implement shadow casting. This is actually really simple in THREE.js, and there are an abundance of articles online describing how to do it, so I won't duplicate any of that here. But as you can see in my simple example, you need to set your meshes to cast and receive shadows (castShadows/receiveShadows, respectively), and set your light to cast them as well (castShadows).

    var renderer, scene, camera, controls, stats;
    
    var WIDTH = window.innerWidth,
      HEIGHT = window.innerHeight,
      FOV = 70,
      NEAR = 1,
      FAR = 1000;
    
    function populateScene() {
      var cfgeo = new THREE.PlaneBufferGeometry(100, 100),
        lwallgeo = new THREE.PlaneBufferGeometry(20, 20),
        rwallgeo = new THREE.PlaneBufferGeometry(50, 20),
        farwallgeo = new THREE.PlaneBufferGeometry(50, 20),
        bumpgeo = new THREE.PlaneBufferGeometry(10, 10);
    
      var mat = new THREE.MeshPhongMaterial({
        color: 0xcccccc,
        emissive: new THREE.Color(0x0c0c0c),
        shininess: 0,
        side: THREE.DoubleSide
      });
    
      var ceiling = new THREE.Mesh(cfgeo, mat),
        floor = new THREE.Mesh(cfgeo, mat),
        lwall = new THREE.Mesh(lwallgeo, mat),
        rwall = new THREE.Mesh(rwallgeo, mat),
        farwall = new THREE.Mesh(farwallgeo, mat),
        bump1 = new THREE.Mesh(bumpgeo, mat),
        bump2 = new THREE.Mesh(bumpgeo, mat);
      ceiling.castShadow = true;
      ceiling.receiveShadow = true;
      floor.castShadow = true;
      floor.receiveShadow = true;
      lwall.castShadow = true;
      lwall.receiveShadow = true;
      rwall.castShadow = true;
      rwall.receiveShadow = true;
      farwall.castShadow = true;
      farwall.receiveShadow = true;
      bump1.castShadow = true;
      bump1.receiveShadow = true;
      bump2.castShadow = true;
      bump2.receiveShadow = true;
    
      ceiling.position.y = 10;
      ceiling.rotation.x = Math.PI / 2;
      floor.position.y = -10;
      floor.rotation.x = Math.PI / -2;
      lwall.rotation.y = Math.PI / 2;
      lwall.position.x = -10;
      rwall.rotation.y = Math.PI / -2;
      rwall.position.x = 10;
      rwall.position.y = 2;
      farwall.position.z = -20;
      bump1.rotation.y = Math.PI / -2;
      bump2.rotation.y = Math.PI / -2;
      bump1.position.set(10, -10, -15);
      bump2.position.set(10, -10, 5);
    
      scene.add(ceiling);
      scene.add(floor);
      scene.add(lwall);
      scene.add(rwall);
      scene.add(farwall);
      scene.add(bump1);
      scene.add(bump2);
    
      var bonus = new THREE.SpotLight(0xcccc00, 0.5);
      bonus.position.set(15, -7, -5);
      bonus.castShadow = true;
      bonus.distance = 20;
      var tgt = new THREE.Object3D();
      tgt.position.set(0, -10, -10);
      bonus.target = tgt;
      scene.add(bonus);
      scene.add(tgt);
    }
    
    function init() {
      document.body.style.backgroundColor = "slateGray";
    
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true
      });
      renderer.shadowMap.enabled = true;
      renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    
      document.body.appendChild(renderer.domElement);
      document.body.style.overflow = "hidden";
      document.body.style.margin = "0";
      document.body.style.padding = "0";
    
      scene = new THREE.Scene();
    
      camera = new THREE.PerspectiveCamera(FOV, WIDTH / HEIGHT, NEAR, FAR);
      camera.position.z = 15;
      scene.add(camera);
    
      controls = new THREE.TrackballControls(camera, renderer.domElement);
      controls.dynamicDampingFactor = 0.5;
      controls.rotateSpeed = 3;
    
      var light = new THREE.PointLight(0xffffff, 1, Infinity);
      camera.add(light);
    
      stats = new Stats();
      stats.domElement.style.position = 'absolute';
      stats.domElement.style.top = '0';
      document.body.appendChild(stats.domElement);
    
      resize();
      window.onresize = resize;
    
      populateScene();
    
      animate();
    }
    
    function resize() {
      WIDTH = window.innerWidth;
      HEIGHT = window.innerHeight;
      if (renderer && camera && controls) {
        renderer.setSize(WIDTH, HEIGHT);
        camera.aspect = WIDTH / HEIGHT;
        camera.updateProjectionMatrix();
        controls.handleResize();
      }
    }
    
    function render() {
      renderer.render(scene, camera);
    }
    
    function animate() {
      requestAnimationFrame(animate);
      render();
      controls.update();
      stats.update();
    }
    
    function threeReady() {
      init();
    }
    
    (function() {
      function addScript(url, callback) {
        callback = callback || function() {};
        var script = document.createElement("script");
        script.addEventListener("load", callback);
        script.setAttribute("src", url);
        document.head.appendChild(script);
      }
    
      addScript("https://threejs.org/build/three.js", function() {
        addScript("https://threejs.org/examples/js/controls/TrackballControls.js", function() {
          addScript("https://threejs.org/examples/js/libs/stats.min.js", function() {
            threeReady();
          })
        })
      })
    })();