Làm việc với trình kết xuất Thẻ 3D

Thẻ thông tin 3D ảnh thực tế có định dạng glTF chuẩn OGC, tức là bạn có thể sử dụng bất kỳ trình kết xuất nào hỗ trợ thông số kỹ thuật của Thẻ thông tin OGC 3D để tạo hình ảnh 3D trực quan. Ví dụ: Cesium là một thư viện nguồn mở cơ bản để kết xuất hình ảnh 3D.

Làm việc với CesiumJS

CesiumJS là một thư viện JavaScript nguồn mở để trực quan hoá 3D trên web. Để biết thêm thông tin về cách sử dụng CesiumJS, hãy xem bài viết Tìm hiểu về CesiumJS.

Quyền kiểm soát của người dùng

Trình kết xuất thẻ thông tin CesiumJS có một bộ tiêu chuẩn gồm các biện pháp kiểm soát dành cho người dùng.

Hành động Nội dung mô tả
Chế độ xem kéo Nhấp và kéo trái
Chế độ xem thu phóng Nhấp và kéo chuột phải hoặc cuộn con lăn chuột
Xoay chế độ xem Ctrl + nhấp và kéo trái/phải hoặc nhấp và kéo giữa

Các phương pháp hay nhất

Có một số cách tiếp cận mà bạn có thể thực hiện để giảm thời gian tải 3D CesiumJS. Ví dụ:

  • Bật các yêu cầu đồng thời bằng cách thêm câu lệnh sau vào HTML hiển thị của bạn:

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

    REQUEST_COUNT càng cao, các thẻ thông tin càng tải nhanh hơn. Tuy nhiên, khi tải trong trình duyệt Chrome có REQUEST_COUNT lớn hơn 10 và tắt bộ nhớ đệm, bạn có thể gặp phải sự cố Chrome đã biết. Đối với hầu hết các trường hợp sử dụng, bạn nên sử dụng REQUEST_COUNT là 18 để có hiệu suất tối ưu.

  • Bật mức độ chi tiết bỏ qua. Để biết thêm thông tin, hãy xem bài viết Vấn đề về xesi.

Đảm bảo bạn hiển thị chính xác các thuộc tính dữ liệu bằng cách bật showCreditsOnScreen: true. Để biết thêm thông tin, hãy xem phần Chính sách.

Chỉ số hiển thị

Để tìm tốc độ khung hình, hãy xem số lần phương thức requestAnimationFrame được gọi mỗi giây.

Để biết cách tính độ trễ khung hình, hãy xem lớp PerformanceDisplay.

Ví dụ về trình kết xuất CesiumJS

Bạn có thể sử dụng trình kết xuất CesiumJS với Thẻ thông tin 3D của API Thẻ thông tin bản đồ chỉ bằng cách cung cấp URL ô gạch gốc.

Ví dụ đơn giản

Ví dụ sau đây khởi chạy trình kết xuất CesiumJS, rồi tải tập hợp thông tin gốc.

<!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>

Để biết thông tin về requestRenderMode, hãy xem bài viết Bật chế độ hiển thị yêu cầu.

Trang HTML hiển thị như minh hoạ ở đây.

Tích hợp Places API

Bạn có thể sử dụng CesiumJS với API Địa điểm để truy xuất thêm thông tin. Bạn có thể sử dụng tiện ích Tự động hoàn thành để di chuyển đến khung nhìn của Địa điểm. Ví dụ này sử dụng API Tự động hoàn thành địa điểm (được bật bằng cách làm theo các hướng dẫn này) và API JavaScript của Maps (được bật bằng cách làm theo các hướng dẫn này).

<!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>

Xoay chế độ xem máy bay không người lái

Bạn có thể điều khiển máy ảnh để tạo ảnh động thông qua tập hợp ô. Khi kết hợp với Places API và Height API (API độ cao), ảnh động này sẽ mô phỏng thao tác bay bằng máy bay không người lái tương tác đến bất kỳ địa điểm yêu thích nào.

Mã mẫu này sẽ đưa bạn đi quanh địa điểm mà bạn đã chọn trong tiện ích Tự động hoàn thành.

<!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>

Vẽ hình nhiều đường và nhãn

Mã mẫu này minh hoạ cách thêm hình nhiều đường và nhãn vào bản đồ. Bạn có thể thêm hình nhiều đường vào bản đồ để hiện thông tin chỉ đường lái xe và đi bộ, để hiển thị ranh giới của thuộc tính hoặc để tính thời gian lái xe và đi bộ. Bạn cũng có thể nhận các thuộc tính mà không cần thực sự kết xuất cảnh.

Bạn có thể đưa người dùng tham quan một khu vực lân cận có tuyển chọn hoặc hiển thị các tài sản lân cận đang được bán, sau đó bạn có thể thêm các đối tượng 3D như biển quảng cáo vào cảnh.

Bạn có thể tóm tắt chuyến đi, liệt kê các thuộc tính mà bạn đã xem, hiển thị những thông tin này trong các đối tượng ảo.

<!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>

Quỹ đạo của máy ảnh

Trong Cesium, bạn có thể xoay camera quanh một địa điểm yêu thích, tránh va chạm với các toà nhà. Ngoài ra, bạn có thể làm cho các toà nhà trong suốt khi máy ảnh di chuyển qua các toà nhà đó.

Trước tiên, hãy khoá máy ảnh vào một điểm, sau đó bạn có thể tạo quỹ đạo máy ảnh để thể hiện nội dung của mình. Bạn có thể thực hiện việc này bằng cách dùng hàm lookAtTransform của máy ảnh với trình nghe sự kiện, như minh hoạ trong mã mẫu dưới đây.

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

Để biết thêm thông tin về cách điều khiển máy ảnh, hãy xem phần Điều khiển máy ảnh

Làm việc với Cesium cho Unreal

Để sử dụng trình bổ trợ Cesium cho Unreal với 3D Tiles API, hãy làm theo các bước dưới đây.

  1. Cài đặt trình bổ trợ Cesium cho Unreal.

  2. Tạo một dự án Unreal mới.

  3. Kết nối với API Thẻ thông tin 3D Google Photos thực tế.

    1. Mở cửa sổ Cesium bằng cách chọn Cesium > Cesium trong trình đơn.

    2. Chọn Thẻ thông tin 3D trống.

    3. Trong World Outliner (Trình vẽ dàn ý thế giới), hãy mở bảng điều khiển Details (Thông tin chi tiết) bằng cách chọn mục Cesium3DTileset này.

    4. Thay đổi Nguồn từ Từ Cesium Ion thành Từ URL.

    5. Đặt URL thành URL Thẻ thông tin 3D của Google.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. Bật chế độ Hiện thông tin ghi công trên màn hình để hiển thị chính xác các thông tin ghi nhận tác giả.
  4. Điều này giúp tải cả thế giới. Để di chuyển đến vĩ độ/kinh độ/chiều cao bất kỳ, hãy chọn mục CesiumGeoreference trong bảng điều khiển Outliner, sau đó chỉnh sửa Vĩ độ/Kinh độ/chiều cao gốc trong bảng điều khiển Details (Thông tin chi tiết).

Làm việc với Cesium dành cho Unity

Để sử dụng thẻ thông tin ảnh thực tế với Cesium cho Unity, hãy làm theo các bước dưới đây.

  1. Tạo một dự án Unity mới.

  2. Thêm một Scoped Registry mới trong phần Package Manager (Trình quản lý gói) (thông qua Editor > Project Settings) (Trình chỉnh sửa > Cài đặt dự án).

    • Tên: Cesium

    • URL: https://unity.pkg.cesium.com

    • Phạm vi: com.cesium.unity

  3. Cài đặt gói Cesium dành cho Unity.

  4. Kết nối với API Thẻ thông tin 3D thực tế của Google Photo.

    1. Mở cửa sổ Cesium bằng cách chọn Cesium > Cesium trong trình đơn.

    2. Nhấp vào Thẻ thông tin 3D trống.

    3. Trên bảng điều khiển bên trái, trong tuỳ chọn Nguồn tập hợp thông tin trong phần Nguồn, hãy chọn Từ URL (thay vì Từ Cesium Ion).

    4. Đặt URL thành URL Thẻ thông tin 3D của Google.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. Bật chế độ Hiện thông tin ghi công trên màn hình để hiển thị chính xác các thông tin ghi nhận tác giả.
  5. Điều này giúp tải cả thế giới. Để di chuyển đến bất kỳ LatLng nào, hãy chọn mục CesiumGeoreference trong Cảnh phân cấp, sau đó chỉnh sửa Vĩ độ/Kinh độ/Chiều cao ban đầu trong Inspector (Trình kiểm tra).

Làm việc với Deck.gl

deck.gl, do WebGL hỗ trợ, là một khung JavaScript nguồn mở để trực quan hoá dữ liệu trên quy mô lớn, hiệu suất cao.

Phân bổ

Đảm bảo bạn hiển thị đúng các thuộc tính dữ liệu bằng cách trích xuất trường copyright từ thẻ thông tin gltf asset, sau đó hiển thị trường đó trên chế độ xem kết xuất. Để biết thêm thông tin, hãy xem bài viết Thuộc tính dữ liệu hiển thị.

ví dụ về trình kết xuất Deck.gl

Ví dụ đơn giản

Ví dụ sau đây khởi chạy trình kết xuất deck.gl, sau đó tải một địa điểm ở chế độ 3D. Trong mã của bạn, hãy nhớ thay thế YOUR_API_KEY bằng khoá API thực tế.

<!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>

Trực quan hóa các lớp 2D trên các Thẻ thông tin 3D thực tế của Google Photo

Ngoài ra, Deck.gl TerrainExtension (Tiện ích địa hình) sẽ kết xuất dữ liệu 2D lên một nền tảng 3D. Ví dụ: bạn có thể xếp đặt GeoJSON của một khu vực toà nhà lên trên Hình học Thẻ thông tin 3D ảnh thực.

Trong ví dụ sau, một lớp toà nhà sẽ được trực quan hoá bằng các đa giác phù hợp với bề mặt Thẻ thông tin 3D ảnh thực.

<!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>