Change from Three.js v122 to v123 hides the shadows on a custom layer for Mapbox

Viewed 424

I'm adding a custom layer to mapbox using three.js, using the official example from mapbox. I found that the shadows that worked perfectly in v122 are not working in v123. After reading carefully the release changelog for v123 and and the migration guide from v122, I cannot find any related commit or change that is making the shadows disappear. I also tested with the latest build available of three.js and it happens the same, but I found the change happens between these two releases. I have tested with different materials apart from ShadowMaterial but same result.

Exactly the same code, just changing the package version from:

https://unpkg.com/three@0.122.0/examples/js/loaders/GLTFLoader.js
https://unpkg.com/three@0.122.0/build/three.min.js

to

https://unpkg.com/three@0.123.0/examples/js/loaders/GLTFLoader.js
https://unpkg.com/three@0.123.0/build/three.min.js

From this:
enter image description here Here is the Fiddle v122

To this...
enter image description here Here is the Fiddle v123

Code is exactly the same:

mapboxgl.accessToken = 'pk.eyJ1IjoianNjYXN0cm8iLCJhIjoiY2s2YzB6Z25kMDVhejNrbXNpcmtjNGtpbiJ9.28ynPf1Y5Q8EyB_moOHylw';
var map = (window.map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/light-v10',
  zoom: 18,
  center: [148.9819, -35.3981],
  pitch: 60,
  bearing: 45,
  antialias: true // create the gl context with MSAA antialiasing, so custom layers are antialiased
}));

// parameters to ensure the model is georeferenced correctly on the map
var modelOrigin = [148.9819, -35.39847];
var modelAltitude = 0;
var modelRotate = [Math.PI / 2, 0, 0];

var modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(
  modelOrigin,
  modelAltitude
);

// transformation parameters to position, rotate and scale the 3D model onto the map
var modelTransform = {
  translateX: modelAsMercatorCoordinate.x,
  translateY: modelAsMercatorCoordinate.y,
  translateZ: modelAsMercatorCoordinate.z,
  rotateX: modelRotate[0],
  rotateY: modelRotate[1],
  rotateZ: modelRotate[2],
  /* Since our 3D model is in real world meters, a scale transform needs to be
   * applied since the CustomLayerInterface expects units in MercatorCoordinates.
   */
  scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits()
};

var THREE = window.THREE;

// configuration of the custom layer for a 3D model per the CustomLayerInterface
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 = 100;
    let r = 2;
    let mapSize = 1024;
    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 = 400;
    dirLight.shadow.camera.visible = true;

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

    // 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.MeshStandardMaterial({ color: 0xffffff, side: THREE.DoubleSide});
        const planeMat = new THREE.ShadowMaterial();
        planeMat.opacity = 0.5;
        let plane = new THREE.Mesh(planeGeo, planeMat);
        plane.rotateX(-Math.PI / 2);
        //plane.layers.enable(1); plane.layers.disable(0); // it makes the object invisible for the raycaster
        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();
  }
};

map.on('style.load', function() {
  map.addLayer(customLayer, 'waterway-label');
});

It seems like a bug, but honestly I'd love is something I'm missing or I didn't realize. I even checked if with the files from different CDNs happens the same, and yes, it happens the same. Hoping one of the Three.js contributors or other devs can help me with this as I'm completely blocked with this and stopping me to migrate

Thanks in advance for any pointer!

2 Answers

A new implementation of WebGLState.reset() will be available with r126. It does not only reset engine internal state flags but also calls WebGL commands to reset the actual WebGL state. This approach should solve the reported issue.

Link to the PR at GitHub: https://github.com/mrdoob/three.js/pull/21281

I included this suggestion in the comments for the pull request on github https://github.com/mrdoob/three.js/pull/20732, but I'll add it here as well incase someone only finds this Stack Overflow question when searching.

I ran into a similar issue using another library that was sharing the WebGL context. What I found was that the other library (imgui-js) was setting gl.BLEND to true and then, with the change in the mentioned pull request, WebGLState.Reset() is now setting currentBlendingEnabled to null.

This caused a lot of textures in my scene to be displayed incorrectly because when setBlending is subsequently called on the WebGLState, it assumes that if the desired blending method is NoBlending and currentBlendingEnabled is null, that gl.BLEND is already disabled:

function setBlending(blending, blendEquation, blendSrc, blendDst, blendEquationAlpha, blendSrcAlpha, blendDstAlpha, premultipliedAlpha) {
    if (blending === NoBlending) {
       if ( currentBlendingEnabled ) {
          disable(3042);

However, with reset nulling the currentBlendingEnabled value each frame but not setting gl.BLEND to false, I believe this assumption is no longer correct. Looking closer, even after removing the external library I was using that sets the gl.BLEND value to true, I found that the change in the pull request was having a negative impacting some of the textures in my scene. In my case I found that updating the setBlending function to honor NoBlending requests, including when currentBlendingEnabled is null, seems to have remedied the situation. Maybe that will work in your case too?

function setBlending(blending, blendEquation, blendSrc, blendDst, blendEquationAlpha, blendSrcAlpha, blendDstAlpha, premultipliedAlpha) {
    if (blending === NoBlending) {
       if (currentBlendingEnabled !== false) {
          disable(3042);
Related