Repository: iTowns/itowns (JavaScript)

Issue:
[REGRESSION] We can't change the opacity of the globe
Since https://github.com/iTowns/itowns/pull/2374/files#diff-2cbabdb42b34b51f54a3ad1eabf469e8990ea4073b43780ff5f2cc6709e08847

It seems that we can't edit the opactiy of the globe anymore






## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1. Use the following example code
2. Click on the button "Change Tile Layer Opacity"


## Expected Behavior

![Image](https://github.com/user-attachments/assets/2a10a5b7-fea8-42ef-bd7b-522d38cd9ae1)


## Actual Behavior

![Image](https://github.com/user-attachments/assets/fd352eba-8754-42ed-9931-666fa96009f2)

```html

<html>
<head>
    <title>Itowns - Globe</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="css/example.css">
    <link rel="stylesheet" type="text/css" href="css/LoadingScreen.css">
    <link rel="stylesheet" type="text/css" href="css/widgets.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
    <style>
        #removeElevationBtn {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 1000;
            padding: 10px 15px;
            background-color: #ff6b6b;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
        }
        #removeElevationBtn:hover {
            background-color: #ff5252;
        }
        #removeElevationBtn:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        #opacityBtn {
            position: absolute;
            top: 60px;
            left: 10px;
            z-index: 1000;
            padding: 10px 15px;
            background-color: #4caf50;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
        }
        #opacityBtn:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
<div id="viewerDiv"></div>
<button id="removeElevationBtn" onclick="removeElevationLayer()">Remove Elevation Layer</button>
<button id="opacityBtn" onclick="changeTileLayerOpacity()">Change Tile Layer Opacity</button>

<!-- Import iTowns source code -->
<script src="../dist/itowns.js"></script>
<script src="../dist/debug.js"></script>
<!-- Import iTowns Widgets plugin -->
<script src="../dist/itowns_widgets.js"></script>
<!-- Import iTowns LoadingScreen and GuiTools plugins -->
<script src="js/GUI/GuiTools.js"></script>
<script src="js/GUI/LoadingScreen.js"></script>

<script type="text/javascript">

    // ---------- CREATE A GlobeView FOR SUPPORTING DATA VISUALIZATION : ----------
    // Define camera initial position
    const placement = {
        coord: new itowns.Coordinates('EPSG:4326', 2.351323, 48.856712),
        range: 2500,
    }
    // `viewerDiv` will contain iTowns' rendering area (`<canvas>`)
    const viewerDiv = document.getElementById('viewerDiv');
    // Create a GlobeView
    const view = new itowns.GlobeView(viewerDiv, placement);
    // Setup loading screen and debug menu
    setupLoadingScreen(viewerDiv, view);
    const debugMenu = new GuiTools('menuDiv', view);

    // Store elevation layer ID for removal
    let elevationLayerId = null;
    // Track current opacity for cycling through values
    let currentOpacity = 1.0;

    // ---------- DISPLAY ORTHO-IMAGES : ----------
    // Add one imagery layer to the scene. This layer's properties are defined in a json file, but it could be
    // defined as a plain js object. See `Layer` documentation for more info.
    itowns.Fetcher.json('./layers/JSONLayers/Ortho.json').then(function _(config) {
        config.source = new itowns.WMTSSource(config.source);
        view.addLayer(
            new itowns.ColorLayer('Ortho', config),
        ).then(debugMenu.addLayerGUI.bind(debugMenu));
    });

    // ---------- DISPLAY A DIGITAL ELEVATION MODEL : ----------
    // Add two elevation layers, each with a different level of detail. Here again, each layer's properties are
    // defined in a json file.
    function addElevationLayerFromConfig(config) {
        config.source = new itowns.WMTSSource(config.source);
        const elevationLayer = new itowns.ElevationLayer(config.id, config);
        elevationLayerId = config.id; // Store the layer ID
        view.addLayer(elevationLayer).then(debugMenu.addLayerGUI.bind(debugMenu));
    }
    // itowns.Fetcher.json('./layers/JSONLayers/IGN_MNT_HIGHRES.json').then(addElevationLayerFromConfig);
    itowns.Fetcher.json('./layers/JSONLayers/WORLD_DTM.json').then(addElevationLayerFromConfig);

    // Function to remove elevation layer
    function removeElevationLayer() {
        if (elevationLayerId) {
            view.removeLayer(elevationLayerId, true);
            console.log(`Elevation layer ${elevationLayerId} removed`);
            // Disable the button after removal
            document.getElementById('removeElevationBtn').disabled = true;
            document.getElementById('removeElevationBtn').textContent = 'Elevation Layer Removed';
        } else {
            console.log('No elevation layer ID found');
        }
    }

    // Function to change tile layer opacity
    function changeTileLayerOpacity() {
        // Cycle through opacity values: 1.0 -> 0.7 -> 0.5 -> 0.3 -> 1.0
        const opacityValues = [1.0, 0.7, 0.5, 0.3];
        const currentIndex = opacityValues.indexOf(currentOpacity);
        const nextIndex = (currentIndex + 1) % opacityValues.length;
        currentOpacity = opacityValues[nextIndex];

        // Apply the opacity change
        const opacity = currentOpacity;
        view.tileLayer.opacity = opacity;




            view.notifyChange(view.camera3D);

        console.log(`Tile layer opacity changed to: ${opacity}`);
        document.getElementById('opacityBtn').textContent = `Opacity: ${Math.round(opacity * 100)}%`;
    }

    // ---------- ADD SOME WIDGETS : ----------
    // ADD A SCALE :
    const scale = new itowns_widgets.Scale(view, { position: 'bottom-right', translate: { x: -80 } });
    // ADD A MINIMAP :
    const minimap = new itowns_widgets.Minimap(
        view,
        new itowns.ColorLayer('minimap', {
            source: new itowns.VectorTilesSource({
                style: "https://data.geopf.fr/annexes/ressources/vectorTiles/styles/PLAN.IGN/standard.json",
                // We don't display mountains and plot related data to ease visualisation
                filter: (layer) => !layer['source-layer'].includes('oro_')
                    && !layer['source-layer'].includes('parcellaire'),
            }),
            addLabelLayer: { performance: true },
        }),
        { cursor: '+' },
    );
    // ADD NAVIGATION TOOLS :
    const navigation = new itowns_widgets.Navigation(view, {
        position: 'bottom-right',
        translate: { y: -40 },
    });
    // ADD A SEARCH BAR :
    // You can find more precise explanation on searchbar options in the doc
    // (http://www.itowns-project.org/itowns/docs/#api/Widgets/Searchbar) and in the searchbar example
    // (https://www.itowns-project.org/itowns/examples/#widgets_searchbar)
    // Define options for geocoding service that should be used by the searchbar.
    const geocodingOptions = {
        url: new URL(
            'https://data.geopf.fr/geocodage/completion?' +
            'text=&type=StreetAddress,PositionOfInterest',
        ),
        parser: (response) => {
            const map = new Map();
            response.results.forEach(location => {
                map.set(location.fulltext, new itowns.Coordinates('EPSG:4326', location.x, location.y));
            });
            return map;
        },
        onSelected: (coordinates) => {
            view.controls.lookAtCoordinate({ coord: coordinates, range: 20000, tilt: 45, heading: 0 });
        },
    }
    // Create the searchbar
    const searchbar = new itowns_widgets.Searchbar(view, geocodingOptions, {
        maxSuggestionNumber: 15,
        placeholder: 'Search a location in France',
        position: 'top-right',
    });

    // ---------- DISPLAY ATMOSPHERIC LIGHTING : ----------
    const atmosphere = view.getLayerById('atmosphere');
    atmosphere.setRealisticOn(!view.isDebugMode);

    // ---------- DEBUG TOOLS : ----------
    // Toggle atmospheric lighting on/off.
    const cRL = debugMenu.addGUI('RealisticLighting', !view.isDebugMode, function (v) {
        atmosphere.setRealisticOn(v);
        view.notifyChange(atmosphere);
    });
    debug.createTileDebugUI(debugMenu.gui, view);
</script>
</body>
</html>


```


Discussion: PR #2562 
Should fix the problem
@ftoromanoff 

After this https://github.com/iTowns/itowns/pull/2562

The render of point cloud looks a bit laggy:

![Image](https://github.com/user-attachments/assets/700602e7-b5ed-4e30-89b8-45ab78b2fe6c)

Can be tried with the 3dtiles_loader example (Sete)

Maybe the collision with the ground is provoking this 🤔 
@AnthonyGlt Can you try #2577 ?



Fix (patch):
diff --git a/packages/Main/src/Core/Prefab/Globe/Atmosphere.js b/packages/Main/src/Core/Prefab/Globe/Atmosphere.js
index 3ff24ae6bb..9716947ef4 100644
--- a/packages/Main/src/Core/Prefab/Globe/Atmosphere.js
+++ b/packages/Main/src/Core/Prefab/Globe/Atmosphere.js
@@ -71,7 +71,6 @@ class Atmosphere extends GeometryLayer {
         const sphereGeometry = new THREE.SphereGeometry(1, 64, 64);
         const basicAtmosphereOut = new THREE.Mesh(sphereGeometry, material);
         basicAtmosphereOut.scale.copy(ellipsoidSizes).multiplyScalar(1.14);
-        basicAtmosphereOut.renderOrder = 1;
 
         this.basicAtmosphere = new THREE.Object3D();
         this.realisticAtmosphere = new THREE.Object3D();
@@ -102,7 +101,6 @@ class Atmosphere extends GeometryLayer {
 
         const basicAtmosphereIn = new THREE.Mesh(sphereGeometry, materialAtmoIn);
         basicAtmosphereIn.scale.copy(ellipsoidSizes).multiplyScalar(1.002);
-        basicAtmosphereIn.renderOrder = 2;
 
         this.basicAtmosphere.add(basicAtmosphereIn);
         this.realisticLightingPosition = { x: -0.5, y: 0.0, z: 1.0 };
@@ -207,7 +205,6 @@ class Atmosphere extends GeometryLayer {
             depthWrite: false,
         });
         const ground = new THREE.Mesh(geometryAtmosphereIn, materialAtmosphereIn);
-        ground.renderOrder = 2;
 
         const geometryAtmosphereOut = new THREE.SphereGeometry(atmosphere.outerRadius, 196, 196);
         const materialAtmosphereOut = new THREE.ShaderMaterial({
@@ -218,10 +215,8 @@ class Atmosphere extends GeometryLayer {
             side: THREE.BackSide,
         });
         const sky = new THREE.Mesh(geometryAtmosphereOut, materialAtmosphereOut);
-        sky.renderOrder = 1;
 
         const skyDome = new Sky();
-        skyDome.renderOrder = 1;
         skyDome.frustumCulled = false;
 
         this.realisticAtmosphere.add(ground);
diff --git a/packages/Main/src/Layer/OGC3DTilesLayer.js b/packages/Main/src/Layer/OGC3DTilesLayer.js
index 8dfce4586c..3fab39e520 100644
--- a/packages/Main/src/Layer/OGC3DTilesLayer.js
+++ b/packages/Main/src/Layer/OGC3DTilesLayer.js
@@ -16,7 +16,6 @@ import iGLTFLoader from 'Parser/iGLTFLoader';
 import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
 // eslint-disable-next-line import/extensions, import/no-unresolved
 import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
-import ReferLayerProperties from 'Layer/ReferencingLayerProperties';
 import PointsMaterial, {
     PNTS_MODE,
     PNTS_SHAPE,
@@ -146,6 +145,112 @@ export function enableMeshoptDecoder(MeshOptDecoder) {
     itownsGLTFLoader.setMeshoptDecoder(MeshOptDecoder);
 }
 
+const noSetter = () => {
+    console.warn('[OGC3DTilesLayer] Material property cannot be set from the material, use layer properties instead');
+};
+
+/**
+ * Patches material properties to automatically update the material (uniforms
+ * and shader) when the layer properties are updated. Note that:
+ * - The transparent property is set according to the opacity. This leads
+ *   to the recompilation of the material if not cached.
+ * - The material properties cannot be set from within the material, so the
+ *   setters are not implemented and shall not be used.
+ * @param {Material} material A three.js material
+ * @param {OGC3DTilesLayer} layer An OGC3DTilesLayer
+ */
+function referMaterialProperties(material, layer) {
+    Object.defineProperty(material, 'opacity', {
+        get: () => layer.opacity,
+        set: noSetter,
+    });
+
+    let _transparent = material.transparent;
+    Object.defineProperty(material, 'transparent', {
+        get: () => {
+            const transparent = material.opacity < 1.0;
+            if (transparent != _transparent) {
+                material.needsUpdate = true;
+                _transparent = transparent;
+            }
+            return transparent;
+        },
+        set: noSetter,
+    });
+
+    Object.defineProperty(material, 'wireframe', {
+        get: () => layer.wireframe,
+        set: noSetter,
+    });
+}
+
+/**
+ * Patches material properties to automatically update the material (uniforms
+ * and shader) when the layer properties are updated. Note that:
+ * - The transparent property is set according to the opacity **and** the
+ *   presence of non-opaque pixels in the classification texture. This leads
+ *   to the recompilation of the material if not cached.
+ * - The material properties cannot be set from within the material, so the
+ *   setters are not implemented and shall not be used.
+ * @param {PointsMaterial} material An itowns PointsMaterial
+ * @param {OGC3DTilesLayer} layer An OGC3DTilesLayer
+ */
+function referPointsMaterialProperties(material, layer) {
+    let _transparent = material.transparent;
+    Object.defineProperty(material, 'transparent', {
+        get: () => {
+            const transparent = material.opacity < 1.0
+                || material.classificationTexture.userData.transparent;
+            if (transparent != _transparent) {
+                material.needsUpdate = true;
+                _transparent = transparent;
+            }
+            return transparent;
+        },
+        set: noSetter,
+    });
+
+    Object.defineProperty(material, 'wireframe', {
+        get: () => layer.wireframe,
+        set: noSetter,
+    });
+
+    Object.defineProperty(material.uniforms.opacity, 'value', {
+        get: () => layer.opacity,
+        set: noSetter,
+    });
+
+    Object.defineProperty(material.uniforms.mode, 'value', {
+        get: () => layer.pntsMode,
+        set: noSetter,
+    });
+
+    Object.defineProperty(material.uniforms.shape, 'value', {
+        get: () => layer.pntsShape,
+        set: noSetter,
+    });
+
+    Object.defineProperty(material.uniforms.sizeMode, 'value', {
+        get: () => layer.pntsSizeMode,
+        set: noSetter,
+    });
+
+    Object.defineProperty(material.uniforms.minAttenuatedSize, 'value', {
+        get: () => layer.pntsMinAttenuatedSize,
+        set: noSetter,
+    });
+
+    Object.defineProperty(material.uniforms.maxAttenuatedSize, 'value', {
+        get: () => layer.pntsMaxAttenuatedSize,
+        set: noSetter,
+    });
+
+    Object.defineProperty(material.uniforms.scale, 'value', {
+        get: () => layer.scale,
+        set: noSetter,
+    });
+}
+
 async function getMeshFeatures(meshFeatures, options) {
     const { faceIndex, barycoord } = options;
 
@@ -429,6 +534,10 @@ class OGC3DTilesLayer extends GeometryLayer {
      * @private
      */
     _assignFinalMaterial(model) {
+        if (!model.isMesh && !model.isPoints) {
+            return;
+        }
+
         let material = model.material;
 
         if (model.isPoints) {
@@ -442,12 +551,9 @@ class OGC3DTilesLayer extends GeometryLayer {
             });
             pointsMaterial.copy(material);
             material = pointsMaterial;
-            material.depthWrite = false;
-        }
-
-        if (material) {
-            material.transparent = true;
-            ReferLayerProperties(material, this);
+            referPointsMaterialProperties(material, this);
+        } else {
+            referMaterialProperties(material, this);
         }
 
         model.material = material;
diff --git a/packages/Main/src/Layer/PointCloudLayer.js b/packages/Main/src/Layer/PointCloudLayer.js
index 7903569daf..1d99e55bea 100644
--- a/packages/Main/src/Layer/PointCloudLayer.js
+++ b/packages/Main/src/Layer/PointCloudLayer.js
@@ -238,7 +238,6 @@ class PointCloudLayer extends GeometryLayer {
         if (this.material) {
             this.material.visible = this.visible;
             this.material.opacity = this.opacity;
-            this.material.depthWrite = false;
             this.material.size = this.pointSize;
             this.material.scale = context.camera.preSSE;
             if (this.material.updateUniforms) {
diff --git a/packages/Main/src/Layer/ReferencingLayerProperties.js b/packages/Main/src/Layer/ReferencingLayerProperties.js
index cf4c48c724..5d9f1a648e 100644
--- a/packages/Main/src/Layer/ReferencingLayerProperties.js
+++ b/packages/Main/src/Layer/ReferencingLayerProperties.js
@@ -2,6 +2,7 @@
 // next step is move these properties to Style class
 function ReferLayerProperties(material, layer) {
     if (layer && layer.isGeometryLayer) {
+        let transparent = material.transparent;
         material.layer = layer;
 
         if (material.uniforms && material.uniforms.opacity != undefined) {
@@ -53,6 +54,16 @@ function ReferLayerProperties(material, layer) {
         Object.defineProperty(material, 'wireframe', {
             get: () => material.layer.wireframe,
         });
+
+        Object.defineProperty(material, 'transparent', {
+            get: () => {
+                if (transparent != material.layer.opacity < 1.0) {
+                    material.needsUpdate = true;
+                    transparent = material.layer.opacity < 1.0;
+                }
+                return transparent;
+            },
+        });
     }
 
     return material;
diff --git a/packages/Main/src/Layer/TiledGeometryLayer.js b/packages/Main/src/Layer/TiledGeometryLayer.js
index 71c2cc4a58..7793e5fc68 100644
--- a/packages/Main/src/Layer/TiledGeometryLayer.js
+++ b/packages/Main/src/Layer/TiledGeometryLayer.js
@@ -136,7 +136,6 @@ class TiledGeometryLayer extends GeometryLayer {
 
         this.tileMatrixSets = tileMatrixSets;
 
-        materialOptions.transparent = materialOptions.transparent ?? true;
         this.materialOptions = materialOptions;
 
         /*
diff --git a/packages/Main/src/Renderer/PointsMaterial.js b/packages/Main/src/Renderer/PointsMaterial.js
index dff0933022..bc5aee9850 100644
--- a/packages/Main/src/Renderer/PointsMaterial.js
+++ b/packages/Main/src/Renderer/PointsMaterial.js
@@ -117,6 +117,7 @@ function recomputeTexture(scheme, texture, nbClass) {
     const width = texture.image.width;
     if (!nbClass) { nbClass = Object.keys(scheme).length; }
 
+    texture.userData.transparent = false;
     for (let i = 0; i < width; i++) {
         let color;
         let opacity;
@@ -140,6 +141,10 @@ function recomputeTexture(scheme, texture, nbClass) {
         data[j + 1] = parseInt(255 * color.g, 10);
         data[j + 2] = parseInt(255 * color.b, 10);
         data[j + 3] = parseInt(255 * opacity, 10);
+
+        if (opacity < 1.0) {
+            texture.userData.transparent = true;
+        }
     }
     texture.needsUpdate = true;
 }
diff --git a/packages/Main/src/Renderer/RenderMode.js b/packages/Main/src/Renderer/RenderMode.js
index 07d2d5fd0e..926f9fb49a 100644
--- a/packages/Main/src/Renderer/RenderMode.js
+++ b/packages/Main/src/Renderer/RenderMode.js
@@ -14,7 +14,6 @@ function push(object3d, mode) {
         const material = node.material;
         if (material) {
             material.mode = m;
-            material.transparent = m === MODES.FINAL;
         }
     });