TiagoRibeiro
TiagoRibeiro

Reputation: 316

why is three.js cast shadow not working on a 3D model

I have 3D models as such:

model

I want to add a cast shadow similar to this:

shadow

And I have the following piece of code responsible for the model:

var ambLight = new THREE.AmbientLight( 0x404040 );
this.scene.add(ambLight)

var loader = new THREE.GLTFLoader();
loader.load(path,function (gltf) {
 gltf.scene.traverse( function( model ) {
  if (model.isMesh){ 
   model.castShadow = true; 
  }
 });
 this.scene.add(gltf.scene);
}

I added the castSHadow part as seen in this StackOverflow post.

I've tried model.castShadow = true and I've tried removing the if condition and just leave the castShadow but that doesn't work either. Am I missing a step? The full custom layer code is here if it helps.

Upvotes: 3

Views: 6968

Answers (2)

jscastro
jscastro

Reputation: 3780

Based on your code, you're trying to add a shadow on top of Mapbox.

For that, apart from the suggestions from @Mugen87, you'll need to create a surface to receive the shadow, and place it exactly below the model you're loading, considering also the size of the object you're loading to avoid the shadow goes out of the plane surface... and then you'll get this.

enter image description here

Relevant code in this fiddle I have created. I slightly changed the light and I added a light helper for clarity.

var customLayer = {
  id: '3d-model',
  type: 'custom',
  renderingMode: '3d',
  onAdd: function(map, gl) {
    this.camera = new THREE.Camera();
    this.scene = new THREE.Scene();

    const dirLight = new THREE.DirectionalLight(0xffffff, 1);
    dirLight.position.set(0, 70, 100);
    let d = 1000;
    let r = 2;
    let mapSize = 8192;
    dirLight.castShadow = true;
    dirLight.shadow.radius = r;
    dirLight.shadow.mapSize.width = mapSize;
    dirLight.shadow.mapSize.height = mapSize;
    dirLight.shadow.camera.top = dirLight.shadow.camera.right = d;
    dirLight.shadow.camera.bottom = dirLight.shadow.camera.left = -d;
    dirLight.shadow.camera.near = 1;
    dirLight.shadow.camera.far = 400000000;
    //dirLight.shadow.camera.visible = true;

    this.scene.add(dirLight);
    this.scene.add(new THREE.DirectionalLightHelper(dirLight, 10));


    // use the three.js GLTF loader to add the 3D model to the three.js scene
    var loader = new THREE.GLTFLoader();
    loader.load(
      'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf',
      function(gltf) {
        gltf.scene.traverse(function(model) {
          if (model.isMesh) {
            model.castShadow = true;
          }
        });
        this.scene.add(gltf.scene);
        // we add the shadow plane automatically 
        const s = new THREE.Box3().setFromObject(gltf.scene).getSize(new THREE.Vector3(0, 0, 0));
        const sizes = [s.x, s.y, s.z];
        const planeSize = Math.max(...sizes) * 10;
        const planeGeo = new THREE.PlaneBufferGeometry(planeSize, planeSize);
        const planeMat = new THREE.ShadowMaterial();
        planeMat.opacity = 0.5;
        let plane = new THREE.Mesh(planeGeo, planeMat);
        plane.rotateX(-Math.PI / 2);
        plane.receiveShadow = true;
        this.scene.add(plane);
      }.bind(this)
    );
    this.map = map;

    // use the Mapbox GL JS map canvas for three.js
    this.renderer = new THREE.WebGLRenderer({
      canvas: map.getCanvas(),
      context: gl,
      antialias: true
    });

    this.renderer.autoClear = false;
    this.renderer.shadowMap.enabled = true;

  },
  render: function(gl, matrix) {
    var rotationX = new THREE.Matrix4().makeRotationAxis(
      new THREE.Vector3(1, 0, 0),
      modelTransform.rotateX
    );
    var rotationY = new THREE.Matrix4().makeRotationAxis(
      new THREE.Vector3(0, 1, 0),
      modelTransform.rotateY
    );
    var rotationZ = new THREE.Matrix4().makeRotationAxis(
      new THREE.Vector3(0, 0, 1),
      modelTransform.rotateZ
    );

    var m = new THREE.Matrix4().fromArray(matrix);
    var l = new THREE.Matrix4()
      .makeTranslation(
        modelTransform.translateX,
        modelTransform.translateY,
        modelTransform.translateZ
      )
      .scale(
        new THREE.Vector3(
          modelTransform.scale,
          -modelTransform.scale,
          modelTransform.scale
        )
      )
      .multiply(rotationX)
      .multiply(rotationY)
      .multiply(rotationZ);

    this.camera.projectionMatrix = m.multiply(l);
    this.renderer.state.reset();
    this.renderer.render(this.scene, this.camera);

    this.map.triggerRepaint();
  }
};

Upvotes: 5

Mugen87
Mugen87

Reputation: 31026

  • You only have an instance of AmbientLight in your scene which is no shadow-casting light.
  • 3D objects can only receive shadow if they set Object3D.receiveShadow to true and if the material is not unlit. Meaning MeshBasicMaterial would not work as the ground's material.
  • You have to globally enable shadows via: renderer.shadowMap.enabled = true;

I suggest you have a closer look to the shadow setup of this official example.

Upvotes: 7

Related Questions