シン・three.jsでPlateauモデルを表示する方法

国交省による都市モデル、Plateauの3DモデルをThree.jsで表示する方法が実質ネットになかったのでまとめました

TL;DR(最初にまとめると)

  • Plateauの3Dモデルは座標系が通常の3Dモデルとは異なっており、そのままだと表示されません
  • Lod1と呼ばれる低解像度のモデルではテクスチャが設定されていないので、設定する必要があります
  • モデル自体にも法線ベクトルが設定されておらず、そのままだと黒い塊になるので設定する必要があります

サンプルコードはどこ?

ここにあります

github.com

どうやるの?

レンダラーの初期化

このあたりは他の場合と変わりません。ここまでに限れば公式のサンプルなど参考になるリソースが大量にあるので、適当に持ってくればOKです

github.com

Plateauの座標系をなんとかする

Plateauのモデルにおいて、各頂点のz軸は通常の高さですが、x軸とy軸は平面直角座標系に基づいています。平面直角座標系は測量用の座標系であり、日本国内に複数ある基準点のいずれか一つからの距離を表しています(詳しくはこちら)。逆に言うとこのおかげでモデル上の位置を通常の緯度経度に変換するなんてこともできるのですが、一方でたいていのモデルが中心から数キロ程度離れた場所に配置されることになり、通常のモデル表示用のコードをそのまま持ってきただけでは表示されません。

そこで中心位置を何とかする必要があります。方法としてはいくつかあるのですが、threejsで表示することを考えると一番楽なのはメッシュに変換した後でそれぞれ最大値と最小値を取得中心を計算してその分を動かすという方法になります。ただしモデルは実際には建物単位であろうと思われるサブモデルの集合体なのですが、fbx形式では親子関係がループしている場所がありその対策も必要です。

テクスチャのないモデル対策

Lod1のようにテクスチャが設定されていないモデルの場合、代替モデルを設定する必要があります。three.jsでは3Dモデルは頂点を表すgeometryとマテリアルを表すmaterialをメンバーとして有しており、テクスチャの無い場合はmaterial.mapが空になるのでこれを利用して判定します。

法線ベクトルを計算する

Plateauのモデルは3DCGやゲームでいうところのアセットよりもCAD等の設計データに近いため、光の反射等を計算するのに必要な法線ベクトルが設定されていません。先述の通りthreejsでは頂点に関わるデータはgeometryに収納されており、computeVertexNormalsメソッドにより設定できます

最終的なコード

            const loader = new FBXLoader();
            const lod1Example = 'models/53392546_bldg_6677.fbx';
            const lod2Example = 'models/53392633_bldg_6677.fbx';
            const alterMaterialColor = 0x90D7EC;
            
            loader.load(lod2Example, function (obj) {
                let maxX = -Infinity;
                let minX = Infinity;
                let maxY = -Infinity;
                let minY = Infinity;
              
                let checked = {};
                
                function traverser(child) {
                    if (checked[child.id] === true) {
                        return;

                    }
                    child.castShadow = true;
                    child.receiveShadow = true;
                    checked[child.id] = true;
                    if (child.isMesh !== true) {
                        return child.traverse(traverser)

                    }

                    child.castShadow = true;
                    child.receiveShadow = true;
                    child.geometry.computeVertexNormals() 
                    if (!child.material.map) {
                        child.material = new THREE.MeshToonMaterial({ color:alterMaterialColor })
                    }



                    
                    const position = child.geometry.attributes.position
                    for (let index = 0; index < position.count; index++) {
                        const x = position.getX(index)
                        if (x > maxX) {
                            maxX = x;
                        }
                        else if (x < minX) {
                            minX = x
                        }
                        const y = position.getY(index)
                        if (y > maxY) {
                            maxY = y;
                        }
                        else if (y < minY) {
                            minY = y
                        }

                       

                    }
                };
                obj.traverse(traverser);
                const centerX = (maxX + minX) / -2
                const centerY = (maxY + minY) / -2;
                
                obj.position.set(centerX, centerY, 0);
                


            
                scene.add(obj);

            });