עבודה עם רינדור אריחים בתלת-ממד

מפתחים באזור הכלכלי האירופי (EEA)

קטעי מפה ריאליסטיים בתלת-ממד הם בפורמט glTF סטנדרטי של OGC, כלומר אתם יכולים להשתמש בכל כלי רינדור שתומך במפרט של קטעי מפה בתלת-ממד של OGC כדי ליצור את התצוגות החזותיות בתלת-ממד. לדוגמה, Cesium היא ספריית קוד פתוח בסיסית לעיבוד של תצוגות תלת-ממדיות.

עבודה עם CesiumJS

‫CesiumJS היא ספריית JavaScript בקוד פתוח להדמיה תלת-ממדית באינטרנט. מידע נוסף על השימוש ב-CesiumJS זמין במאמר Learn CesiumJS.

פקדי משתמש

לכלי לעיבוד אריחים של CesiumJS יש קבוצה סטנדרטית של אמצעי בקרה למשתמשים.

פעולה תיאור
הזזת התצוגה לחיצה שמאלית וגרירה
שינוי מרחק התצוגה לוחצים לחיצה ימנית וגוררים, או גוללים עם גלגל העכבר
סיבוב התצוגה ‫Ctrl + לחיצה על הכפתור השמאלי/הימני וגרירה, או לחיצה על הכפתור האמצעי וגרירה

שיטות מומלצות

יש כמה דרכים לקצר את זמני הטעינה של CesiumJS 3D. לדוגמה:

  • כדי להפעיל בקשות בו-זמניות, מוסיפים את ההצהרה הבאה לקוד ה-HTML של העיבוד:

    Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = <REQUEST_COUNT>
    

    ככל שREQUEST_COUNT גבוה יותר, כך המשבצות נטענות מהר יותר. עם זאת, כשמבצעים טעינה בדפדפן Chrome עם REQUEST_COUNT גדול מ-10 והמטמון מושבת, יכול להיות שתיתקלו בבעיה מוכרת ב-Chrome. ברוב התרחישים לדוגמה, מומלץ להשתמש ב-REQUEST_COUNT של 18 כדי לקבל ביצועים אופטימליים.

  • אפשר לדלג על רמות פירוט. למידע נוסף, ראו הבעיה הזו ב-Cesium.

כדי לוודא ששיוך הנתונים מוצג בצורה תקינה, צריך להפעיל את showCreditsOnScreen: true. מידע נוסף מופיע במאמר כללי מדיניות.

מדדי רינדור

כדי למצוא את קצב הפריימים, בודקים כמה פעמים בשנייה מתבצעת קריאה ל-method‏ requestAnimationFrame.

כדי לראות איך זמן האחזור של הפריימים מחושב, אפשר לעיין במחלקה PerformanceDisplay.

דוגמאות לכלי רינדור של CesiumJS

כדי להשתמש בכלי הרינדור של CesiumJS עם אריחי תלת-ממד של Map Tiles API, פשוט צריך לספק את כתובת ה-URL של קבוצת האריחים ברמה הבסיסית (root).

דוגמה פשוטה

בדוגמה הבאה, המרנדר של CesiumJS מאותחל ואז נטען מערך המשבצות ברמה הבסיסית (root).

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <title>CesiumJS 3D Tiles Simple Demo</title>
  <script src="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Cesium.js"></script>
  <link href="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>
<body>
  <div id="cesiumContainer"></div>
  <script>

    // Enable simultaneous requests.
    Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = 18;

    // Create the viewer.
    const viewer = new Cesium.Viewer('cesiumContainer', {
      imageryProvider: false,
      baseLayerPicker: false,
      geocoder: false,
      globe: false,
      // https://cesium.com/blog/2018/01/24/cesium-scene-rendering-performance/#enabling-request-render-mode
      requestRenderMode: true,
    });

    // Add 3D Tiles tileset.
    const tileset = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({
      url: "https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY",
      // This property is needed to appropriately display attributions
      // as required.
      showCreditsOnScreen: true,
    }));
  </script>
</body>

למידע על requestRenderMode, ראו הפעלת מצב רינדור לפי בקשה.

דף ה-HTML מוצג כמו שמופיע כאן.

שילוב של Places API (חדש)

אתם יכולים להשתמש ב-CesiumJS עם Places API (החדש) כדי לאחזר מידע נוסף. אתם יכולים להשתמש בווידג'ט ההשלמה האוטומטית כדי לעבור אל אזור התצוגה של המקומות. בדוגמה הזו משתמשים ב-Places Autocomplete API (השלמה אוטומטית למקומות), שמופעל לפי ההוראות האלה, וב-Maps JavaScript API, שמופעל לפי ההוראות האלה.

<!DOCTYPE html>
<head>
 <meta charset="utf-8" />
 <title>CesiumJS 3D Tiles Places API (New) Integration Demo</title>
 <script src="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Cesium.js"></script>
 <link href="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>
<body>
 <label for="pacViewPlace">Go to a place: </label>
 <input
   type="text"
   id="pacViewPlace"
   name="pacViewPlace"
   placeholder="Enter a location..."
   style="width: 300px"
 />
 <div id="cesiumContainer"></div>
 <script>
   // Enable simultaneous requests.
   Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = 18;

   // Create the viewer.
   const viewer = new Cesium.Viewer("cesiumContainer", {
     imageryProvider: false,
     baseLayerPicker: false,
     requestRenderMode: true,
     geocoder: false,
     globe: false,
   });

   // Add 3D Tiles tileset.
   const tileset = viewer.scene.primitives.add(
     new Cesium.Cesium3DTileset({
       url: "https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY",
       // This property is required to display attributions as required.
       showCreditsOnScreen: true,
     })
   );

   const zoomToViewport = (viewport) => {
     viewer.entities.add({
       polyline: {
         positions: Cesium.Cartesian3.fromDegreesArray([
           viewport.getNorthEast().lng(), viewport.getNorthEast().lat(),
           viewport.getSouthWest().lng(), viewport.getNorthEast().lat(),
           viewport.getSouthWest().lng(), viewport.getSouthWest().lat(),
           viewport.getNorthEast().lng(), viewport.getSouthWest().lat(),
           viewport.getNorthEast().lng(), viewport.getNorthEast().lat(),
         ]),
         width: 10,
         clampToGround: true,
         material: Cesium.Color.RED,
       },
     });
     viewer.flyTo(viewer.entities);
   };

   function initAutocomplete() {
     const autocomplete = new google.maps.places.Autocomplete(
       document.getElementById("pacViewPlace"),
       {
         fields: [
           "geometry",
           "name",
         ],
       }
     );
     autocomplete.addListener("place_changed", () => {
       viewer.entities.removeAll();
       const place = autocomplete.getPlace();
       if (!place.geometry || !place.geometry.viewport) {
         window.alert("No viewport for input: " + place.name);
         return;
       }
       zoomToViewport(place.geometry.viewport);
     });
   }
 </script>
 <script
   async=""
   src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initAutocomplete"
 ></script>
</body>

מבט מרחפן מסתובב

אתם יכולים לשלוט במצלמה כדי להוסיף אנימציה לקבוצת המשבצות. בשילוב עם Places API (החדש) ו-Elevation API, האנימציה הזו מדמה טיסה אינטראקטיבית של רחפן מעל כל נקודת עניין.

דוגמת הקוד הזו מציגה את המקום שבחרתם בווידג'ט ההשלמה האוטומטית ממעוף הציפור.

<!DOCTYPE html>
<head>
  <meta charset="utf-8" />
  <title>CesiumJS 3D Tiles Rotating Drone View Demo</title>
  <script src="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Cesium.js"></script>
  <link href="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
</head>
<body>
  <label for="pacViewPlace">Go to a place: </label>
  <input type="text" id="pacViewPlace" name="pacViewPlace" placeholder="Enter a location..." style="width: 300px" />
  <div id="cesiumContainer"></div>
  <script>
    // Enable simultaneous requests.
    Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = 18;

    // Create the viewer and remove unneeded options.
    const viewer = new Cesium.Viewer("cesiumContainer", {
      imageryProvider: false,
      baseLayerPicker: false,
      homeButton: false,
      fullscreenButton: false,
      navigationHelpButton: false,
      vrButton: false,
      sceneModePicker: false,
      geocoder: false,
      globe: false,
      infobox: false,
      selectionIndicator: false,
      timeline: false,
      projectionPicker: false,
      clockViewModel: null,
      animation: false,
      requestRenderMode: true,
    });

    // Add 3D Tile set.
    const tileset = viewer.scene.primitives.add(
      new Cesium.Cesium3DTileset({
        url: "https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY",
        // This property is required to display attributions.
        showCreditsOnScreen: true,
      })
    );

    // Point the camera at a location and elevation, at a viewport-appropriate distance.
    function pointCameraAt(location, viewport, elevation) {
      const distance = Cesium.Cartesian3.distance(
        Cesium.Cartesian3.fromDegrees(
          viewport.getSouthWest().lng(), viewport.getSouthWest().lat(), elevation),
        Cesium.Cartesian3.fromDegrees(
          viewport.getNorthEast().lng(), viewport.getNorthEast().lat(), elevation)
      ) / 2;
      const target = new Cesium.Cartesian3.fromDegrees(location.lng(), location.lat(), elevation);
      const pitch = -Math.PI / 4;
      const heading = 0;
      viewer.camera.lookAt(target, new Cesium.HeadingPitchRange(heading, pitch, distance));
    }

    // Rotate the camera around a location and elevation, at a viewport-appropriate distance.
    let unsubscribe = null;
    function rotateCameraAround(location, viewport, elevation) {
      if(unsubscribe) unsubscribe();
      pointCameraAt(location, viewport, elevation);
      unsubscribe = viewer.clock.onTick.addEventListener(() => {
        viewer.camera.rotate(Cesium.Cartesian3.UNIT_Z);
      });
    }

    function initAutocomplete() {
      const autocomplete = new google.maps.places.Autocomplete(
        document.getElementById("pacViewPlace"), {
          fields: [
            "geometry",
            "name",
          ],
        }
      );

      autocomplete.addListener("place_changed", async () => {
        const place = autocomplete.getPlace();

        if (!(place.geometry && place.geometry.viewport && place.geometry.location)) {
          window.alert(`Insufficient geometry data for place: ${place.name}`);
          return;
        }
        // Get place elevation using the ElevationService.
        const elevatorService = new google.maps.ElevationService();
        const elevationResponse =  await elevatorService.getElevationForLocations({
          locations: [place.geometry.location],
        });

        if(!(elevationResponse.results && elevationResponse.results.length)){
          window.alert(`Insufficient elevation data for place: ${place.name}`);
          return;
        }
        const elevation = elevationResponse.results[0].elevation || 10;

        rotateCameraAround(
          place.geometry.location,
          place.geometry.viewport,
          elevation
        );
      });
    }
  </script>
  <script async src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initAutocomplete"></script>
</body>

שרטוט קווים פוליגוניים ותוויות

דוגמת הקוד הזו ממחישה איך להוסיף קווים פוליגוניים ותוויות למפה. אפשר להוסיף קווים פוליגוניים למפה כדי להציג מסלולי נסיעה והליכה, להציג גבולות של נכסים או לחשב את משך הנסיעה וההליכה. אפשר גם לקבל מאפיינים בלי לרנדר את הסצנה בפועל.

אתם יכולים להציע למשתמשים סיור מודרך בשכונה, או להציג נכסים סמוכים שמוצעים כרגע למכירה, ואז להוסיף לסצנה אובייקטים תלת-ממדיים כמו שלטי חוצות.

אתם יכולים לסכם סיור, לפרט את הנכסים שראיתם ולהציג את הפרטים האלה באובייקטים וירטואליים.

<!DOCTYPE html>
<head>
  <meta charset="utf-8" />
  <title>CesiumJS 3D Tiles Polyline and Label Demo</title>
  <script src="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Cesium.js"></script>
  <link
    href="https://ajax.googleapis.com/ajax/libs/cesiumjs/1.105/Build/Cesium/Widgets/widgets.css"
    rel="stylesheet"
  />
</head>
<body>
  <div id="cesiumContainer"></div>
  <script>
    // Enable simultaneous requests.
    Cesium.RequestScheduler.requestsByServer["tile.googleapis.com:443"] = 18;

    // Create the viewer.
    const viewer = new Cesium.Viewer("cesiumContainer", {
      imageryProvider: false,
      baseLayerPicker: false,
      requestRenderMode: true,
      geocoder: false,
      globe: false,
    });

    // Add 3D Tiles tileset.
    const tileset = viewer.scene.primitives.add(
      new Cesium.Cesium3DTileset({
        url: "https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY",

        // This property is required to display attributions as required.
        showCreditsOnScreen: true,
      })
    );

    // Draws a circle at the position, and a line from the previous position.
    const drawPointAndLine = (position, prevPosition) => {
      viewer.entities.removeAll();
      if (prevPosition) {
        viewer.entities.add({
          polyline: {
            positions: [prevPosition, position],
            width: 3,
            material: Cesium.Color.WHITE,
            clampToGround: true,
            classificationType: Cesium.ClassificationType.CESIUM_3D_TILE,
          },
        });
      }
      viewer.entities.add({
        position: position,
        ellipsoid: {
          radii: new Cesium.Cartesian3(1, 1, 1),
          material: Cesium.Color.RED,
        },
      });
    };

    // Compute, draw, and display the position's height relative to the previous position.
    var prevPosition;
    const processHeights = (newPosition) => {
      drawPointAndLine(newPosition, prevPosition);

      const newHeight = Cesium.Cartographic.fromCartesian(newPosition).height;
      let labelText = "Current altitude (meters above sea level):\n\t" + newHeight;
      if (prevPosition) {
        const prevHeight =
          Cesium.Cartographic.fromCartesian(prevPosition).height;
        labelText += "\nHeight from previous point (meters):\n\t" + Math.abs(newHeight - prevHeight);
      }
      viewer.entities.add({
        position: newPosition,
        label: {
          text: labelText,
          disableDepthTestDistance: Number.POSITIVE_INFINITY,
          pixelOffset: new Cesium.Cartesian2(0, -10),
          showBackground: true,
          verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
        }
      });

      prevPosition = newPosition;
    };

    const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
    handler.setInputAction(function (event) {
      const earthPosition = viewer.scene.pickPosition(event.position);
      if (Cesium.defined(earthPosition)) {
        processHeights(earthPosition);
      }
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);
  </script>
</body>

מסלול המצלמה

ב-Cesium, אתם יכולים להקיף עם המצלמה נקודת עניין, בלי להתנגש בבניינים. אפשר גם להגדיר שהמבנים יהיו שקופים כשהמצלמה עוברת דרכם.

קודם נועלים את המצלמה על נקודה מסוימת, ואז אפשר ליצור מסלול הקפה של המצלמה כדי להציג את הנכס. כדי לעשות את זה, משתמשים בפונקציה lookAtTransform של המצלמה עם רכיב event listener, כמו בדוגמת הקוד הבאה.

// Lock the camera onto a point.
const center = Cesium.Cartesian3.fromRadians(
  2.4213211833389243,
  0.6171926869414084,
  3626.0426275055174
);

const transform = Cesium.Transforms.eastNorthUpToFixedFrame(center);

viewer.scene.camera.lookAtTransform(
  transform,
  new Cesium.HeadingPitchRange(0, -Math.PI / 8, 2900)
);

// Orbit around this point.
viewer.clock.onTick.addEventListener(function (clock) {
  viewer.scene.camera.rotateRight(0.005);
});

איך שולטים במצלמה

עבודה עם Cesium for Unreal

כדי להשתמש בפלאגין Cesium for Unreal עם 3D Tiles API, פועלים לפי השלבים הבאים.

  1. מתקינים את הפלאגין Cesium for Unreal.

  2. יוצרים פרויקט חדש ב-Unreal.

  3. מתחברים ל-Google Photorealistic 3D Tiles API.

    1. פותחים את החלון של Cesium על ידי בחירה באפשרות Cesium > Cesium בתפריט.

    2. בוחרים באפשרות Blank 3D Tiles Tileset (קבוצת משבצות ריקה של משבצות תלת-ממדיות).

    3. ב-World Outliner, פותחים את החלונית Details על ידי בחירה ב-Cesium3DTileset.

    4. משנים את Source (מקור) מ-From Cesium Ion (מ-Cesium Ion) ל-From URL (מכתובת URL).

    5. מגדירים את כתובת ה-URL של Google 3D Tiles.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. מפעילים את האפשרות הצגת קרדיטים במסך כדי להציג את הקרדיטים בצורה תקינה.
  4. העולם נטען. כדי לעבור לכל קו רוחב וקו אורך, בוחרים את הפריט CesiumGeoreference בחלונית Outliner, ואז עורכים את Origin Latitude/Longitude/Height בחלונית Details.

עבודה עם Cesium ל-Unity

כדי להשתמש במשבצות פוטו-ריאליסטיות עם Cesium ל-Unity, פועלים לפי השלבים הבאים.

  1. יוצרים פרויקט חדש ב-Unity.

  2. מוסיפים Scoped Registry חדש בקטע Package Manager (דרך Editor ‏> Project Settings).

    • שם: צזיום

    • כתובת URL: ‏https://unity.pkg.cesium.com

    • היקף: com.cesium.unity

  3. מתקינים את החבילה Cesium for Unity.

  4. מתחברים ל-Google Photorealistic 3D Tiles API.

    1. פותחים את החלון של Cesium על ידי בחירה באפשרות Cesium > Cesium בתפריט.

    2. לוחצים על Blank 3D Tiles Tileset.

    3. בחלונית שמימין, באפשרות Tileset Source בקטע Source, בוחרים באפשרות From URL (במקום From Cesium Ion).

    4. מגדירים את כתובת ה-URL לכתובת ה-URL של Google 3D Tiles.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. מפעילים את האפשרות הצגת קרדיטים במסך כדי להציג את הקרדיטים בצורה תקינה.
  5. העולם נטען. כדי לעבור לכל קו רוחב וקו אורך, בוחרים את הפריט CesiumGeoreference ב-Scene Hierarchy, ואז עורכים את קו הרוחב, קו האורך והגובה של נקודת המוצא ב-Inspector.

עבודה עם deck.gl

‫deck.gl, שמופעל על ידי WebGL, הוא JavaScript framework בקוד פתוח להמחשות נתונים בקנה מידה גדול ובביצועים גבוהים.

שיוך (Attribution)

כדי לוודא ששיוך הנתונים מוצג בצורה תקינה, צריך לחלץ את השדה copyright מ-gltf של המשבצות asset, ואז להציג אותו בתצוגה שעברה רינדור. מידע נוסף זמין במאמר הצגת שיוך נתונים.

דוגמאות למרנדר deck.gl

דוגמה פשוטה

בדוגמה הבאה, המערכת מאתחלת את כלי הרינדור deck.gl ואז טוענת מקום בתלת-ממד. בקוד, חשוב להחליף את הערך YOUR_API_KEY במפתח ה-API בפועל.

<!DOCTYPE html>
<html>
 <head>
   <title>deck.gl Photorealistic 3D Tiles example</title>
   <script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
   <style>
     body { margin: 0; padding: 0;}
     #map { position: absolute; top: 0;bottom: 0;width: 100%;}
     #credits { position: absolute; bottom: 0; right: 0; padding: 2px; font-size: 15px; color: white;
        text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;}
   </style>
 </head>

 <body>
   <div id="map"></div>
   <div id="credits"></div>
   <script>
     const GOOGLE_API_KEY = YOUR_API_KEY;
     const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
     const creditsElement = document.getElementById('credits');
     new deck.DeckGL({
       container: 'map',
       initialViewState: {
         latitude: 50.0890,
         longitude: 14.4196,
         zoom: 16,
         bearing: 90,
         pitch: 60,
         height: 200
       },
       controller: {minZoom: 8},
       layers: [
         new deck.Tile3DLayer({
           id: 'google-3d-tiles',
           data: TILESET_URL,
           loadOptions: {
            fetch: {
              headers: {
                'X-GOOG-API-KEY': GOOGLE_API_KEY
              }
            }
          },
           onTilesetLoad: tileset3d => {
             tileset3d.options.onTraversalComplete = selectedTiles => {
               const credits = new Set();
               selectedTiles.forEach(tile => {
                 const {copyright} = tile.content.gltf.asset;
                 copyright.split(';').forEach(credits.add, credits);
                 creditsElement.innerHTML = [...credits].join('; ');
               });
               return selectedTiles;
             }
           }
         })
       ]
     });
   </script>
 </body>
</html>

הצגת שכבות דו-ממדיות מעל קטעי מפה ריאליסטיים בתלת-ממד של Google

התוסף TerrainExtension של deck.gl מעבד נתונים דו-ממדיים על משטח תלת-ממדי. לדוגמה, אפשר להציג את ה-GeoJSON של שטח הבניין מעל הגיאומטריה של קטעי המפה הריאליסטיים בתלת-ממד.

בדוגמה הבאה, שכבת מבנים מוצגת עם פוליגונים שמותאמים למשטח של קטעי המפה הריאליסטיים בתלת-ממד.

<!DOCTYPE html>
<html>
 <head>
   <title>Google 3D tiles example</title>
   <script src="https://unpkg.com/deck.gl@latest/dist.min.js"></script>
   <style>
     body { margin: 0; padding: 0;}
     #map { position: absolute; top: 0;bottom: 0;width: 100%;}
     #credits { position: absolute; bottom: 0; right: 0; padding: 2px; font-size: 15px; color: white;
        text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;}
   </style>
 </head>

 <body>
   <div id="map"></div>
   <div id="credits"></div>
   <script>
     const GOOGLE_API_KEY = YOUR_API_KEY;
     const TILESET_URL = `https://tile.googleapis.com/v1/3dtiles/root.json`;
     const BUILDINGS_URL = 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/google-3d-tiles/buildings.geojson'
     const creditsElement = document.getElementById('credits');
     const deckgl = new deck.DeckGL({
       container: 'map',
       initialViewState: {
         latitude: 50.0890,
         longitude: 14.4196,
         zoom: 16,
         bearing: 90,
         pitch: 60,
         height: 200
       },
       controller: true,
       layers: [
         new deck.Tile3DLayer({
           id: 'google-3d-tiles',
           data: TILESET_URL,
           loadOptions: {
            fetch: {
              headers: {
                'X-GOOG-API-KEY': GOOGLE_API_KEY
              }
            }
          },
          onTilesetLoad: tileset3d => {
             tileset3d.options.onTraversalComplete = selectedTiles => {
               const credits = new Set();
               selectedTiles.forEach(tile => {
                 const {copyright} = tile.content.gltf.asset;
                 copyright.split(';').forEach(credits.add, credits);
                 creditsElement.innerHTML = [...credits].join('; ');
               });
               return selectedTiles;
             }
           },
           operation: 'terrain+draw'
         }),
         new deck.GeoJsonLayer({
           id: 'buildings',
           // This dataset is created by CARTO, using other Open Datasets available. More info at: https://3dtiles.carto.com/#about.
           data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/examples/google-3d-tiles/buildings.geojson',
           stroked: false,
           filled: true,
           getFillColor: ({properties}) => {
             const {tpp} = properties;
             // quantiles break
             if (tpp < 0.6249)
               return [254, 246, 181]
             else if (tpp < 0.6780)
               return [255, 194, 133]
             else if (tpp < 0.8594)
               return [250, 138, 118]
             return [225, 83, 131]
           },
           opacity: 0.2,
           extensions: [new deck._TerrainExtension()]
         })
       ]
     });
   </script>
 </body>
</html>