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

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

עבודה עם SesiumJS

SesiumJS היא ספריית JavaScript בקוד פתוח להדמיה בתלת-ממד באינטרנט. למידע נוסף על השימוש ב-CesiumJS, ראו מידע על SesiumJS.

פקדי משתמש

למעבד המשבצות CisiumJS יש קבוצה סטנדרטית של בקרות משתמש.

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

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

יש כמה גישות כדי לקצר את זמני הטעינה בתלת-ממד של CisiumJS. למשל:

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

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

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

  • הפעלת דילוג על רמות פירוט. למידע נוסף, ראו בעיה עם צסיום.

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

מדדי רינדור

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

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

דוגמאות לרינדור CisiumJS

אפשר להשתמש בכלי לרינדור CisiumJS עם משבצות תלת-ממד של Map Tiles API, פשוט על-ידי הוספת כתובת ה-URL של קבוצת המשבצות ברמה הבסיסית.

דוגמה פשוטה

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

<!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. תוכלו להשתמש בווידג'ט של ההשלמה האוטומטית כדי לעוף לאזור התצוגה של 'מקומות'. בדוגמה הזו אנחנו משתמשים ב-Place השלמה אוטומטית API, שמופעל על ידי ביצוע ההוראות האלה, וב-API של מפות Google ב-JavaScript שמופעל על ידי ביצוע ההוראות האלה.

<!DOCTYPE html>
<head>
 <meta charset="utf-8" />
 <title>CesiumJS 3D Tiles Places API 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 ו-Height 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>

מסלול המצלמה

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

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

// 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);
});

מידע נוסף על שליטה במצלמה זמין במאמר שליטה במצלמה

עבודה עם Cisium for Unreal

כדי להשתמש ב-Ccesium לפלאגין Unreal עם 3D Tiles API, תצטרכו לבצע את השלבים הבאים.

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

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

  3. התחברות לממשק ה-API של משבצות תלת-ממדיות ב-Google Photoreal.

    1. כדי לפתוח את חלון צסיום, בוחרים באפשרות צסיום > צסיום בתפריט.

    2. בוחרים באפשרות ערכת משבצות ריקה של משבצות תלת-ממדיות.

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

    4. משנים את המקור מ-From Cisium Ion ל-מכתובת אתר.

    5. הגדר את כתובת האתר ככתובת האתר של אריחי Google 3D.

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

עבודה עם Cisium ל-Unity

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

  1. יצירת פרויקט Unity חדש.

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

    • שם: צסיום

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

    • היקפים: com.cesium.unity

  3. התקנה של חבילת Cesium ל-Unity.

  4. התחברות לממשק ה-API של משבצות תלת-ממדיות ב-Google Photoreal.

    1. כדי לפתוח את חלון צסיום, בוחרים באפשרות צסיום > צסיום בתפריט.

    2. לוחצים על קבוצת משבצות של משבצות תלת-ממדיות ריקות.

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

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

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

עבודה עם deck.gl

deck.gl, שמופעל על ידי WebGL, הוא מסגרת JavaScript בקוד פתוח, שמיועדת להמחשת נתונים בהיקף רחב ובקנה מידה גדול.

שיוך (Attribution)

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

דוגמאות לכלי לרינדור Dec.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 פוטוריאליסטיות

במקרים אחרים, הנתונים בדו-ממד מוצגים על משטח תלת-ממדי באמצעות ה-display.gl TerrainExtension. לדוגמה, אפשר להציג את ה-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>