Работа с визуализатором 3D-плиток

Фотореалистичные 3D-плитки находятся в стандартном формате OGC glTF , что означает, что вы можете использовать любой рендерер, поддерживающий спецификацию OGC 3D Tiles, для создания ваших 3D-визуализаций. Например, Cesium — это основополагающая библиотека с открытым исходным кодом для рендеринга 3D-визуализаций.

Работа с CesiumJS

CesiumJS — это библиотека Javascript с открытым исходным кодом для 3D-визуализации в Интернете. Для получения дополнительной информации об использовании 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 . Для получения дополнительной информации см. Политики .

Метрики рендеринга

Чтобы узнать частоту кадров, посмотрите, сколько раз в секунду вызывается метод requestAnimationFrame .

Чтобы увидеть, как рассчитывается задержка кадра, взгляните на класс PerformanceDisplay .

Примеры рендеринга CesiumJS

Вы можете использовать рендерер CesiumJS с 3D-плитками API Map Tiles, просто указав URL-адрес корневого набора плиток.

Простой пример

В следующем примере инициализируется рендерер CesiumJS, а затем загружается корневой набор плиток.

<!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-страница отображается так, как показано здесь.

Интеграция API мест

Вы можете использовать CesiumJS с API Places для получения дополнительной информации. Вы можете использовать виджет Autocomplete для перехода к окну просмотра Places. В этом примере используется API автозаполнения Places, который включается с помощью следующих инструкций , и API JavaScript Maps, который включается с помощью следующих инструкций .

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

Вращающийся вид с дрона

Вы можете управлять камерой для анимации через набор плиток. В сочетании с API Places и API Elevation эта анимация имитирует интерактивный пролет дрона над любой точкой интереса.

Этот пример кода перенесет вас в место, выбранное вами в виджете автозаполнения.

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

Рисовать полилинии и метки

В этом примере кода показано, как добавлять полилинии и метки на карту. Вы можете добавлять полилинии на карту, чтобы показывать направления движения и ходьбы, или показывать границы собственности, или рассчитывать длительность движения и ходьбы. Вы также можете получать атрибуты без фактического рендеринга сцены.

Вы можете провести для пользователей организованную экскурсию по району или показать соседние объекты недвижимости, которые в настоящее время выставлены на продажу, а затем добавить в сцену 3D-объекты, например, рекламные щиты.

Вы можете подвести итоги поездки, перечислив просмотренные вами объекты недвижимости, отобразив эти данные в виртуальных объектах.

<!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 с прослушивателем событий, как показано в этом примере кода.

// 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 для Unreal

Чтобы использовать плагин Cesium для Unreal с API 3D Tiles, выполните следующие действия.

  1. Установите плагин Cesium для Unreal.

  2. Создайте новый проект Unreal.

  3. Подключитесь к API Google Photorealistic 3D Tiles.

    1. Откройте окно «Цезий», выбрав в меню Цезий > Цезий .

    2. Выберите набор плиток «Пустая 3D-плитка» .

    3. В World Outliner откройте панель Details , выбрав этот Cesium3DTileset .

    4. Измените Источник с Из цезиевого иона на Из URL .

    5. Установите URL-адрес как URL-адрес Google 3D Tiles.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. Включите функцию «Показывать титры на экране» для корректного отображения сведений об авторах.
  4. Это загрузит мир. Чтобы перейти к любой системе координат LatLng, выберите элемент CesiumGeoreference на панели Outliner , а затем отредактируйте Origin Latitude/Longitude/Height на панели Details .

Работа с цезием для Unity

Чтобы использовать фотореалистичные плитки с Cesium для Unity, выполните следующие действия.

  1. Создайте новый проект Unity.

  2. Добавьте новый реестр Scoped в раздел «Диспетчер пакетов» (через «Редактор» > «Настройки проекта» ).

    • Имя: Цезий

    • URL-адрес: https://unity.pkg.cesium.com

    • Область(ы): com.cesium.unity

  3. Установите пакет Cesium для Unity.

  4. Подключитесь к API Google Photorealistic 3D Tiles.

    1. Откройте окно «Цезий», выбрав в меню Цезий > Цезий .

    2. Нажмите Пустой набор плиток 3D .

    3. На левой панели в пункте «Источник набора плиток» в разделе «Источник» выберите «Из URL» (вместо «Из ионов цезия»).

    4. Установите URL-адрес на URL-адрес Google 3D Tiles.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. Включите функцию «Показывать титры на экране» для корректного отображения сведений об авторах.
  5. Это загрузит мир. Чтобы перейти к любой LatLng, выберите элемент CesiumGeoreference в иерархии сцены , а затем отредактируйте широту/долготу/высоту начала координат в инспекторе .

Работа с deck.gl

deck.gl , работающий на основе WebGL, представляет собой JavaScript-фреймворк с открытым исходным кодом для высокопроизводительной крупномасштабной визуализации данных.

Атрибуция

Убедитесь, что вы правильно отображаете атрибуты данных, извлекая поле copyright из плиток gltf asset , а затем отображая его в визуализированном представлении. Для получения дополнительной информации см. Отображение атрибутов данных .

примеры рендеринга deck.gl

Простой пример

Следующий пример инициализирует рендерер deck.gl, а затем загружает место в 3D. В вашем коде обязательно замените 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>

Визуализируйте 2D-слои поверх Google Photorealistic 3D Tiles

deck.gl TerrainExtension визуализирует 2D-данные на 3D-поверхности. Например, вы можете наложить GeoJSON контура здания поверх геометрии фотореалистичных 3D-плиток.

В следующем примере слой зданий визуализируется с помощью полигонов, адаптированных к поверхности фотореалистичных 3D-плиток.

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

Фотореалистичные 3D-плитки находятся в стандартном формате OGC glTF , что означает, что вы можете использовать любой рендерер, поддерживающий спецификацию OGC 3D Tiles, для создания ваших 3D-визуализаций. Например, Cesium — это основополагающая библиотека с открытым исходным кодом для рендеринга 3D-визуализаций.

Работа с CesiumJS

CesiumJS — это библиотека Javascript с открытым исходным кодом для 3D-визуализации в Интернете. Для получения дополнительной информации об использовании 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 . Для получения дополнительной информации см. Политики .

Метрики рендеринга

Чтобы узнать частоту кадров, посмотрите, сколько раз в секунду вызывается метод requestAnimationFrame .

Чтобы увидеть, как рассчитывается задержка кадра, взгляните на класс PerformanceDisplay .

Примеры рендеринга CesiumJS

Вы можете использовать рендерер CesiumJS с 3D-плитками API Map Tiles, просто указав URL-адрес корневого набора плиток.

Простой пример

В следующем примере инициализируется рендерер CesiumJS, а затем загружается корневой набор плиток.

<!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-страница отображается так, как показано здесь.

Интеграция API мест

Вы можете использовать CesiumJS с API Places для получения дополнительной информации. Вы можете использовать виджет Autocomplete для перехода к окну просмотра Places. В этом примере используется API автозаполнения Places, который включается с помощью следующих инструкций , и API JavaScript Maps, который включается с помощью следующих инструкций .

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

Вращающийся вид с дрона

Вы можете управлять камерой для анимации через набор плиток. В сочетании с API Places и API Elevation эта анимация имитирует интерактивный пролет дрона над любой точкой интереса.

Этот пример кода перенесет вас в место, выбранное вами в виджете автозаполнения.

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

Рисовать полилинии и метки

В этом примере кода показано, как добавлять полилинии и метки на карту. Вы можете добавлять полилинии на карту, чтобы показывать направления движения и ходьбы, или показывать границы собственности, или рассчитывать длительность движения и ходьбы. Вы также можете получать атрибуты без фактического рендеринга сцены.

Вы можете провести для пользователей организованную экскурсию по району или показать соседние объекты недвижимости, которые в настоящее время выставлены на продажу, а затем добавить в сцену 3D-объекты, например, рекламные щиты.

Вы можете подвести итоги поездки, перечислив просмотренные вами объекты недвижимости, отобразив эти данные в виртуальных объектах.

<!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 с прослушивателем событий, как показано в этом примере кода.

// 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 для Unreal

Чтобы использовать плагин Cesium для Unreal с API 3D Tiles, выполните следующие действия.

  1. Установите плагин Cesium для Unreal.

  2. Создайте новый проект Unreal.

  3. Подключитесь к API Google Photorealistic 3D Tiles.

    1. Откройте окно «Цезий», выбрав в меню Цезий > Цезий .

    2. Выберите набор плиток «Пустая 3D-плитка» .

    3. В World Outliner откройте панель Details , выбрав этот Cesium3DTileset .

    4. Измените Источник с Из цезиевого иона на Из URL .

    5. Установите URL-адрес как URL-адрес Google 3D Tiles.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. Включите функцию «Показывать титры на экране» для корректного отображения сведений об авторах.
  4. Это загрузит мир. Чтобы перейти к любой системе координат LatLng, выберите элемент CesiumGeoreference на панели Outliner , а затем отредактируйте Origin Latitude/Longitude/Height на панели Details .

Работа с цезием для Unity

Чтобы использовать фотореалистичные плитки с Cesium для Unity, выполните следующие действия.

  1. Создайте новый проект Unity.

  2. Добавьте новый реестр Scoped в раздел «Диспетчер пакетов» (через «Редактор» > «Настройки проекта» ).

    • Имя: Цезий

    • URL-адрес: https://unity.pkg.cesium.com

    • Область(ы): com.cesium.unity

  3. Установите пакет Cesium для Unity.

  4. Подключитесь к API Google Photorealistic 3D Tiles.

    1. Откройте окно «Цезий», выбрав в меню Цезий > Цезий .

    2. Нажмите Пустой набор плиток 3D .

    3. На левой панели в пункте «Источник набора плиток» в разделе «Источник» выберите «Из URL» (вместо «Из ионов цезия»).

    4. Установите URL-адрес на URL-адрес Google 3D Tiles.

    https://tile.googleapis.com/v1/3dtiles/root.json?key=YOUR_API_KEY
    
    1. Включите функцию «Показывать титры на экране» для корректного отображения сведений об авторах.
  5. Это загрузит мир. Чтобы перейти к любой LatLng, выберите элемент CesiumGeoreference в иерархии сцены , а затем отредактируйте широту/долготу/высоту начала координат в инспекторе .

Работа с deck.gl

deck.gl , работающий на основе WebGL, представляет собой JavaScript-фреймворк с открытым исходным кодом для высокопроизводительной крупномасштабной визуализации данных.

Атрибуция

Убедитесь, что вы правильно отображаете атрибуты данных, извлекая поле copyright из плиток gltf asset , а затем отображая его в визуализированном представлении. Для получения дополнительной информации см. Отображение атрибутов данных .

примеры рендеринга deck.gl

Простой пример

Следующий пример инициализирует рендерер deck.gl, а затем загружает место в 3D. В вашем коде обязательно замените 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>

Визуализируйте 2D-слои поверх Google Photorealistic 3D Tiles

deck.gl TerrainExtension визуализирует 2D-данные на 3D-поверхности. Например, вы можете наложить GeoJSON контура здания поверх геометрии фотореалистичных 3D-плиток.

В следующем примере слой зданий визуализируется с помощью полигонов, адаптированных к поверхности фотореалистичных 3D-плиток.

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