フリーダムの日記

GIS(地理情報システム)を中心に技術的なことを書いています。

3D ライブラリ(Cesium)を使用して、3D都市モデル(Project PLATEAU)を表示してみた。

はじめに

本エントリーは FOSS4G Advent Calendar 2022 11日目の記事です。

qiita.com

CesiumJS を使用して OGC Indexed 3D Scene Layers(I3S)で公開されている 東京都23区・八王子市南大沢 3D都市モデル(Project PLATEAU)を表示してみましたので、紹介してきたいと思います。

I3S, CesiumJS

CesiumJS ですが、バージョン 1.99 から I3S に対応しており、Cesium のリリースカレンダーにも紹介されています。

An example of I3S 3D Object Scene Layer loaded in Cesium Sandcastle application. I3S layer compatibility is achieved through a new scene primitive, I3SDataProvider in CesiumJS. I3SDataProvider enables transcoding of I3S geometry payload, materials and texture to glTF (glb). An example of I3S 3D Object Scene Layer loaded in Cesium Sandcastle application. I3S layer compatibility is achieved through a new scene primitive, I3SDataProvider in CesiumJS. I3SDataProvider enables transcoding of I3S geometry payload, materials and texture to glTF (glb).

CesiumJS 1.99 is now available. Highlights of the release include:

  • Added support for I3S 3D Object and IntegratedMesh Layers.
  • Fixed a bug where the scale of a Model was being incorrectly applied to its bounding sphere.
  • Fixed a bug where rendering a Model with image-based lighting while specular environment maps were unsupported caused a crash.
  • Fixed a bug where request render mode was broken when a ground primitive is added.

cesium.com

I3S としては 3D ObjectIntegrated Mesh に対応し、Point Scene Layer や Point cloud、Building scene layer については今後に期待でしょうか。

I3S の詳細な仕様については以下が参考になるかと思います。 github.com

データは、Living Atlas に公開されている「Project PLATEAU」の東京都23区・八王子市南大沢 3D都市モデル(Project PLATEAU)を使用しました。このデータは無料で利用することができます。ただし、ご利用にあたっては、国土交通省ホームページ PLATEAU Policy に定められた利用条件を必ず遵守する必要があります。

livingatlas.arcgis.com

作成したサンプルアプリは、GitHub でも公開していますので、興味のある方は触ってみてください。

CesiumJS で実装

CesiumJS をこれから触ってみたい方は、Ceisum のチュートリアルのクイックスタートや Sandcastle での CesiumJS のサンプルコードが豊富にありますので、参考になるかと思います。

cesium.com

cesium.com

具体的な I3S を使用したサンプルコードとして、I3S 3D Object LayerI3S IntegratedMesh Layer などが参考になります。

サンプルコードを参考にして実装した部分が以下のコードになります。

// More datasets to tour can be added here...
// The url passed to I3SDataProvider supports loading a single Indexed 3D Scene (I3S) layer (.<host>/SceneServer/layers/<id>) or a collection of scene layers (.<host>/SceneServer) from a SceneServer.
//東京都23区・八王子市南大沢 3D都市モデル(Project PLATEAU)
// https://www.arcgis.com/home/item.html?id=ca7baa183c6e4c998a668a6fadc5fc49
const tours = {
    "東京都23区・八王子市南大沢 3D都市モデル": "https://tiles.arcgis.com/tiles/wlVTGRSYTzAbjjiC/arcgis/rest/services/13100_13201_Tokyo-23ku_Minamiosawa_Building/SceneServer/layers/0",
};

// Initialize a terrain provider which provides geoid conversion between gravity related (typically I3S datasets) and ellipsoidal based
// height systems (Cesium World Terrain).
// If this is not specified, or the URL is invalid no geoid conversion will be applied.
// The source data used in this transcoding service was compiled from https://earth-info.nga.mil/#tab_wgs84-data and is based on EGM2008 Gravity Model
const geoidService = new Cesium.ArcGISTiledElevationTerrainProvider({
    url: "https://tiles.arcgis.com/tiles/z2tnIkrLQ2BRzr6P/arcgis/rest/services/EGM2008/ImageServer",
});

// Create i3s and Cesium3DTileset options to pass optional parameters useful for debugging and visualizing
const cesium3dTilesetOptions = {
    skipLevelOfDetail: false,
    debugShowBoundingVolume: false,
};

const i3sOptions = {
    url: tours["東京都23区・八王子市南大沢 3D都市モデル"],
    traceFetches: false, // for tracing I3S fetches
    geoidTiledTerrainProvider: geoidService, // pass the geoid service
    cesium3dTilesetOptions: cesium3dTilesetOptions, // options for internal Cesium3dTileset
};

// Create I3S data provider
const i3sProvider = new Cesium.I3SDataProvider(i3sOptions); 

// Add the i3s layer provider as a primitive data type
viewer.scene.primitives.add(i3sProvider);

// Center camera on I3S once it's loaded
i3sProvider.readyPromise.then(function () {
    const center = Cesium.Rectangle.center(i3sProvider.extent);
    center.height = 10000.0;
    viewer.camera.setView({
        destination: Cesium.Ellipsoid.WGS84.cartographicToCartesian(center),
    });
});

// An entity object which will hold info about the currently selected feature for infobox display
const selectedEntity = new Cesium.Entity();
// Show metadata in the InfoBox.
viewer.screenSpaceEventHandler.setInputAction(function onLeftClick(
    movement
) {
    // Pick a new feature
    const pickedFeature = viewer.scene.pick(movement.position);
    if (!Cesium.defined(pickedFeature)) {
        return;
    }

    const pickedPosition = viewer.scene.pickPosition(movement.position);

    if (
        Cesium.defined(pickedFeature.content) &&
        Cesium.defined(pickedFeature.content.tile.i3sNode)
    ) {
        const i3sNode = pickedFeature.content.tile.i3sNode;
        if (pickedPosition) {
            i3sNode.loadFields().then(function () {
                let description = "No attributes";
                let name;
                console.log(
                    `pickedPosition(x,y,z) : ${pickedPosition.x}, ${pickedPosition.y}, ${pickedPosition.z}`
                );

                const fields = i3sNode.getFieldsForPickedPosition(
                    pickedPosition
                );
                if (Object.keys(fields).length > 0) {
                    description =
                        '<table class="cesium-infoBox-defaultTable"><tbody>';
                    for (const fieldName in fields) {
                        if (i3sNode.fields.hasOwnProperty(fieldName)) {
                            description += `<tr><th>${fieldName}</th><td>`;
                            description += `${fields[fieldName]}</td></tr>`;
                            console.log(`${fieldName}: ${fields[fieldName]}`);
                            if (!Cesium.defined(name) && isNameProperty(fieldName)) {
                                name = fields[fieldName];
                            }
                        }
                    }
                    description += `</tbody></table>`;
                }
                if (!Cesium.defined(name)) {
                    name = "unknown";
                }
                selectedEntity.name = name;
                selectedEntity.description = description;
                viewer.selectedEntity = selectedEntity;
            });
        }
    }
}, 
Cesium.ScreenSpaceEventType.LEFT_CLICK);

function isNameProperty(propertyName) {
    const name = propertyName.toLowerCase();
    if (
        name.localeCompare("name") === 0 ||
        name.localeCompare("objname") === 0
    ) {
        return true;
    }
    return false;
}

コードは、サンプルコードをそのまま使用して、I3S の参照先を変更しました。I3S は REST の仕様に基づいていますので、東京都23区・八王子市南大沢 3D都市モデルのサービス URL 「https://tiles.arcgis.com/tiles/wlVTGRSYTzAbjjiC/arcgis/rest/services/13100_13201_Tokyo-23ku_Minamiosawa_Building/SceneServer」を指定します。

I3S をサポートするクラスとして I3SDataProviderAPI に追加されています。

  • An I3SDataProvider is the main public class for I3S support. The url option should return a scene object. Currently supported I3S versions are 1.6 and 1.7/1.8 (OGC I3S 1.2). I3SFeature and I3SNode classes implement the Object Model for I3S entities, with public interfaces.

I3SDataProvider に渡される url は、SceneServer から単一の Indexed 3D Scene (I3S) レイヤー (./SceneServer/layers/) またはシーンレイヤーのコレクション (./SceneServer) をロードすることをサポートしています。

また、I3SFeatureI3SNode クラスは、I3S エンティティのオブジェクトモデルを実装しており、パブリックなインターフェイスを備えています。I3SNode を使用してフィールド名とその値を格納したオブジェクトにアクセスすることができます。実際にサンプルの中でもフィールド名とその値にアクセスして表示しています。
ただし、1 点残念なことに、おそらくマルチバイトだと思うのですが、日本語だと文字化けした状態になっており、文字化けして表示されてしまいます。こちらは、Ceisum の GitHub の issue にでも報告してみたいと思います。

今回作成したサンプルコードは以下になります。 github.com

最後に

Cesium が OGC の 3DTiles に加えて、I3S にも対応したことで、3D のデータを扱える幅が広がりました。I3S はCesium 以外にもLoaders.gl と Deck.gl にも対応していて、3D Object, IntegratedMesh, Building Scene Layers (OGC I3S Version 1.2 & OGC I3S Version 1.3 (candidate)) の読み込みと可視化が可能です。

このように I3S は FOSS(Free and Open Source Community)での I3S 採用が進んでいます。 I3S3D Tiles 間の双方間のバッチ変換を可能にするオープンソースTile Converter と組み合わせることで、地理空間のコミュニティ全般における相互運用性の目標をさらに前進することを今後に期待したいです。

関連リンク