danvk
danvk

Reputation: 16903

Unable to cast a shadow with THREE.js and Mapbox GL

I'm trying to add a THREE.js scene into a Mapbox GL visualization following this example. I've added a sphere and a ground plane and a DirectionalLight. Now I'm trying to get the light to cast a shadow on the ground plane. Adding a DirectionalLightHelper and a CameraHelper for the light's shadow camera, everything looks pretty reasonable to me:

A DirectionLight not casting a shadow

I'd expect to see a shadow for the sphere on the plane.

Full code here, but here are the highlights:

class SpriteCustomLayer {
  type = 'custom';
  renderingMode = '3d';

  constructor(id) {
    this.id = id;
    this.gui = new dat.GUI();
    THREE.Object3D.DefaultUp.set(0, 0, 1);
  }

  async onAdd(map, gl) {
    this.camera = new THREE.Camera();

    const centerLngLat = map.getCenter();
    this.center = MercatorCoordinate.fromLngLat(centerLngLat, 0);
    const {x, y, z} = this.center;
    this.cameraTransform = new THREE.Matrix4()
      .makeTranslation(x, y, z)
      .scale(new THREE.Vector3(1, -1, 1));

    this.map = map;
    this.scene = this.makeScene();

    this.renderer = new THREE.WebGLRenderer({
      canvas: map.getCanvas(),
      context: gl,
      antialias: true,
    });
    this.renderer.shadowMap.enabled = true;

    this.renderer.autoClear = false;
  }

  makeScene() {
    const scene = new THREE.Scene();
    scene.add(new THREE.AmbientLight(0xffffff, 0.25));

    const s = this.center.meterInMercatorCoordinateUnits();

    const light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(0.000002360847837325531, 0.000004566603480958114, 0.00000725142167844218);
    light.target.position.set(0, 0, 0);

    light.castShadow = true;
    light.shadow.mapSize.width = 1024;
    light.shadow.mapSize.height = 1024;

    light.shadow.camera.left = -0.000002383416166278454 * 2;
    light.shadow.camera.right = 0.000002383416166278454 * 2;
    light.shadow.camera.bottom = -0.000002383416166278454 * 2;
    light.shadow.camera.top = 0.000002383416166278454 * 2;
    light.shadow.camera.near = 0.0000012388642793465356;
    light.shadow.camera.far *= s;

    scene.add(light);
    this.light = light;

    {
      const planeSize = 500;
      const loader = new THREE.TextureLoader();
      const texture = loader.load('/checker.png');
      texture.wrapS = THREE.RepeatWrapping;
      texture.wrapT = THREE.RepeatWrapping;
      texture.magFilter = THREE.NearestFilter;
      const repeats = 10;
      texture.repeat.set(repeats, repeats);

      const planeGeo = new THREE.PlaneBufferGeometry(planeSize, planeSize);
      const planeMat = new THREE.MeshPhongMaterial({
        map: texture,
        side: THREE.DoubleSide,
      });

      const plane = new THREE.Mesh(planeGeo, planeMat);
      plane.scale.setScalar(s);
      plane.receiveShadow = true;
      scene.add(plane);
    }

    {
      const sphereRadius = 5e-7;
      const sphereGeo = new THREE.SphereBufferGeometry(sphereRadius, 32, 32);
      const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
      const mesh = new THREE.Mesh(sphereGeo, sphereMat);

      mesh.position.set(0, 0, 5e-6);
      mesh.castShadow = true;
      mesh.receiveShadow = false;
      sphereMat.side = THREE.DoubleSide;
      scene.add(mesh);
    }

    return scene;
  }

  render(gl, matrix) {
    this.camera.projectionMatrix = new THREE.Matrix4()
      .fromArray(matrix)
      .multiply(this.cameraTransform);
    this.renderer.state.reset();
    this.renderer.render(this.scene, this.camera);
    this.map.triggerRepaint();
  }
}

Mapbox GL JS uses a coordinate system where the entire world is in [0, 1] so the coordinates are pretty tiny. It also uses x/y for lat/lng and z for up, which is different than usual Three.js coordinates.

How can I get the shadow to appear? I'm using Three.js r109 and Mapbox GL JS 1.4.0. I've tried replacing the PlaneBufferGeometry with a thin BoxGeometry to no avail.

Upvotes: 1

Views: 1356

Answers (1)

TheJim01
TheJim01

Reputation: 8866

EDIT

Forget everything I said in my old answer.

The example below scales things WAY down and the shadow remains.

The kicker was here:

shadowLight.shadow.camera.near *= (scaleDown) ? 0.1 : 10;
shadowLight.shadow.camera.far *= (scaleDown) ? 0.1 : 10;
shadowLight.shadow.camera.updateProjectionMatrix(); // <========= !!!!!

I was updating the scale, but wasn't updating the near/far of the shadow camera. Then, once I was, I was forgetting to update that camera's projection matrix. With all the pieces back together, it seems to be working well.

Try adding a call to update the shadow-casting light's camera's projection matrix after you configure the values.

If it still doesn't work, maybe you can use my example to figure out what's going on in your code.

If MY example doesn't work for you, then it might be your hardware doesn't support the level of precision you need.

// just some random colors to show it's actually rendering
const colors = [
  0xff0000, // 1e+1
  0x00ff00, // 1e+0
  0x0000ff, // 1e-1
  0xffff00, // 1e-2
  0xff00ff, // 1e-3
  0x00ffff, // 1e-4
  0xabcdef, // 1e-5
  0xfedcba, // 1e-6
  0x883300, // 1e-7
  0x008833, // 1e-8
  0x330088, // 1e-9
  0x338800 // 1e-10
];
const renderer = new THREE.WebGLRenderer({
  antialias: true
});
renderer.shadowMap.enabled = true; // turn on shadow mapping
renderer.setClearColor(0xcccccc);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(28, 1, 1, 1000)
camera.position.set(25, 10, 15);
camera.lookAt(0, 0, 0);

const camLight = new THREE.PointLight(0xffffff, 1);
camera.add(camLight);

const floor = new THREE.Mesh(
  new THREE.PlaneBufferGeometry(50, 50),
  new THREE.MeshPhongMaterial({
    color: "gray"
  })
);
floor.receiveShadow = true;
floor.rotation.set(Math.PI / -2, 0, 0);
floor.position.set(0, -1, 0);

const sphere = new THREE.Mesh(
  new THREE.SphereBufferGeometry(2, 16, 32),
  new THREE.MeshPhongMaterial({
    color: colors[0]
  })
);
sphere.castShadow = true;
sphere.position.set(0, 1, 0);

const shadowLight = new THREE.PointLight(0xffffff, 1);
shadowLight.castShadow = true;
shadowLight.position.set(-10, 10, 5);

const group = new THREE.Group();
group.add(floor);
group.add(sphere);
group.add(shadowLight);
group.add(camera);

scene.add(group);

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

function resize() {
  const W = window.innerWidth;
  const H = window.innerHeight;
  renderer.setSize(W, H);
  camera.aspect = W / H;
  camera.updateProjectionMatrix();
}
window.onresize = resize;

resize();
render();
let scaler = 10;
let scaleLevel = 10;
let scaleLevelOutput = document.getElementById("scaleLevel");
let scaleDown = true;
let colorIndex = 0;
setInterval(() => {
  colorIndex += (scaleDown) ? 1 : -1;
  scaleLevel *= (scaleDown) ? 0.1 : 10;
  shadowLight.shadow.camera.near *= (scaleDown) ? 0.1 : 10;
  shadowLight.shadow.camera.far *= (scaleDown) ? 0.1 : 10;
  shadowLight.shadow.camera.updateProjectionMatrix();

  if (scaleLevel < 1e-9 && scaleDown) {
    scaleDown = false;
  }
  if (scaleLevel >= 10 && !scaleDown) {
    scaleDown = true;
  }

  scaleLevelOutput.innerText = `SCALE LEVEL: ${scaleLevel.toExponential()}`;

  group.scale.set(scaleLevel, scaleLevel, scaleLevel);

  sphere.material.color.setHex(colors[colorIndex]);
  sphere.material.needsUpdate = true;

  render();
}, 1000);
body {
  margin: 0;
  overflow: hidden;
}

#scaleLevel {
  font-family: monospace;
  font-size: 2em;
  position: absolute;
  top: 0;
  left: 0;
  font-weight: bold;
  margin: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/109/three.min.js"></script>
<div id="scaleLevel">SCALE LEVEL: 1e+1</div>

Upvotes: 1

Related Questions