PLATEAUデータをUnityで扱う時用のメモ

# 初めに
PLATEAUを扱う際にデータの扱いってどんなんだっけな。。。
実寸大はスケールが違うから設定しないといけないんだったよな。。。
確かそのまま扱う際には重かったよな。。。

と毎回しらべて、いるのでまとめてみました。


追記　2022/06/13
フォルダ選択して、全コンバートするとUnityが何度か落ちてしまう事があるので、
コンバートしてあるものは、読み飛ばすようにしました。
unityが何度か落ちても、何度でも挑戦すれば全コンバートも夢じゃない！

追記　2022/06/16
Unityが落ちる原因は
AssetDatabase.ImportAssetでfbxを読み込んでいる最中にCrashしてしまうみたいです。
※下記はEditor.logを見ると下記のようなログ残してました。
```
Start importing Assets/hoge.fbx using Guid(xxxxx) Importer(-1,00000000000000000000000000000000) Crash!!!
```
ImportAssetをStartAssetEditingしてからすればいいのかっておもったんだけど。。
インポートしたfbxをLoadMainAssetAtPathでロードするんだけど、読み込むタイミングでは
ImportAssetが終わっていないのでnullが返ってくるどうやって扱えばいいのかな。。
！！！なにかいい方法あれば教えてください。！！！


## PLATEAUデータどこだっけ？

https://www.geospatial.jp/ckan/dataset/plateau

私の住んでいる２３区のデータはfbxで公開されているので、
unityはfbx対応しているのでラクチンですね。

### fbxデータが公開されていない地域

「３D都市モデルのデータ変換マニュアル」

https://www.mlit.go.jp/plateau/libraries/

マニュアルみると。。なんか大変そう。。

やり方は、下記に紹介されています。

http://tatsuya1970.com/?p=15666

もっと簡単な方法が無いかと探してみるとobj形式ですが変換ツールを作ってる方が

@ksasao 氏の CityGML形式 (.gml) を Wavefront OBJ (.obj) 形式に変換するツール

https://github.com/ksasao/PlateauCityGmlSharp

objはUnityで対応しているのでfbxが対応していない地域は上記の変換ツール使うといいです。



#### スケール問題

fbxデータを読み込んでHierarchyにモデルデータを置くとなんか小さい
それは、PLATEAUのデータとUnityでのスケールの扱いが違うためです。

下記でも紹介されていますが、
実際のデータにしたい場合は
fbx の Inspector から Scale Factor を 100 にして Apply します。

https://qiita.com/matchyy/items/abed4930d88bf324606a

ここで私がちょっとお馬鹿さんなので、わからないのですが
一個のデータは１キロのデータでunityにもっていくときに
データが1/1000になってしまうのでスケールを1000倍しないといけないのでないのか？？
なのに、実寸サイズにするさいにスケールを100倍にするのはどうしてなんだろう？？
どなたか教えてください。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37068/19b6036b-2c0a-702b-1563-6f242d5c1ba9.png)

追記
@matchyy さんのコメントでモデルの縮尺は1/100で作られているって事でした。


##### データをそのまま使うと重い問題
PLATEAUのデータはオブジェクトが全部バラバラで存在しているので
処理にとても時間がかかります。そこで下記のを参考にオブジェクトを
結合して軽量化します。

https://zenn.dev/fumo/articles/7cad6c25dd99b7

上記で紹介されているものは、fbxデータを一個ずつ選択して
軽量化してますが、せっかくデータが沢山あるし、全部コンバートして
使いたいと欲求が。。
という事でファイルを選ぶ形からフォルダ選択をしてそのフォルダ内の
データを全部一気にコンバートする形にしちゃいました。

```c#
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;

public sealed class PlateauLod2FbxImporterWindowFolder : EditorWindow
{
    [MenuItem("Window/PLATEAU LOD2 FBX Importer Folder")]
    public static void ShowWindow()
    {
        var window = GetWindow<PlateauLod2FbxImporterWindowFolder>();
        window.titleContent = new GUIContent("PLATEAU LOD2 FBX Importer");
        window.position = new Rect(10, 10, 300, 300);
    }

    public void OnGUI()
    {
        GUILayout.Label("LOD2のFBXを指定してください。");

        if (GUILayout.Button("FBX読み込み", GUILayout.Height(40)))
        {
            var path = EditorUtility.OpenFolderPanel("Load Scripts", Application.dataPath, string.Empty);
            //var path = EditorUtility.OpenFilePanel("Import FBX", "", "FBX");
            if (string.IsNullOrEmpty(path)) return;


            foreach (var file in Directory.GetFiles(path))
            {
                if (file.EndsWith(".fbx"))
                {
                    Debug.Log(file);
                    fbxConvert(file);
                }
            }
            return;

        }
    }
    private bool CheckExistence(string path)
    {
        var ret = false;
        // FBMディレクトリのパス
        var fbmDir = path.Replace(".fbx", "");

        if (Directory.Exists(fbmDir))
        {
            ret = true;
        }

        return ret;
    }
    private void fbxConvert(string path)
    {
        // FBXファイル名
        var fileName = Path.GetFileName(path);

        // コピー先の絶対パス
        var newPath = Path.Combine(Application.dataPath, fileName);

        // アセットインポート用相対パス
        var importPath = Path.Combine("Assets", fileName);

        // FBMディレクトリのパス
        var fbmDir = path.Replace(".fbx", ".fbm");

        // コピー先のFBMディレクトリ名
        var newFbmDirName = "";

        if (CheckExistence(newPath))
            return;

        var t0 = Time.realtimeSinceStartup;
        try
        {
            // FBMディレクトリをコピー
            if (Directory.Exists(fbmDir))
            {
                newFbmDirName = fileName.Replace(".fbx", ".fbm");
                var newFbmDirPath = Path.Combine(Application.dataPath, newFbmDirName);
                Directory.CreateDirectory(newFbmDirPath);

                CopyFiles(fbmDir, newFbmDirName);
            }

            // FBXファイルをコピー
            File.Copy(path, newPath);

            // FBXファイルをインポート
            AssetDatabase.ImportAsset(importPath);

            AssetDatabase.StartAssetEditing();

            var fbxObject = AssetDatabase.LoadMainAssetAtPath(importPath) as GameObject;

            // 1Prefab辺りのGameObject数
            var objectPerPrefab = 20;
            // 作成するPrefab数
            var prefabCount = fbxObject.transform.childCount / objectPerPrefab;
            if (fbxObject.transform.childCount % objectPerPrefab != 0)
                prefabCount++;

            // PrefabのRootを作成
            var parents = new GameObject[prefabCount];
            for (var i = 0; i < parents.Length; i++)
            {
                parents[i] = new GameObject($"{fbxObject.name}_{i:0000}");
            }

            // メッシュを読み込んで結合する
            for (var i = 0; i < fbxObject.transform.childCount; i++)
            {
                var t = fbxObject.transform.GetChild(i);
                var target = parents[i / objectPerPrefab];
                MergeMesh(t, target.transform, fbxObject.name);
            }

            // Prefab保存
            foreach (var parent in parents)
            {
                PrefabUtility.SaveAsPrefabAsset(parent, $"Assets/{fbxObject.name}/{parent.name}.prefab");
                DestroyImmediate(parent);
            }

            // FBMをリネーム
            if (!string.IsNullOrEmpty(newFbmDirName))
            {
                AssetDatabase.MoveAsset($"Assets/{newFbmDirName}", $"Assets/{fbxObject.name}/texture");
            }
        }
        finally
        {
            // 元のFBXファイルを削除
            AssetDatabase.DeleteAsset(importPath);

            AssetDatabase.StopAssetEditing();
            var time = Time.realtimeSinceStartup - t0;
            Debug.Log($"Done: {time}sec");
        }
    }
    private void CopyFiles(string sourceDir, string destDirName)
    {
        var textureFiles = Directory.GetFiles(sourceDir);
        foreach (var path in textureFiles)
        {
            var fileName = Path.GetFileName(path);
            var assetPath = Path.Combine("Assets", destDirName, fileName);
            File.Copy(path, assetPath);
            AssetDatabase.ImportAsset(assetPath);
        }
    }

    private void MergeMesh(Transform t, Transform parent, string assetDir)
    {
        var newMesh = new Mesh();
        var vertices = new List<Vector3>();
        var indexes = new List<int>();
        var uvs = new List<Vector2>();
        var index = 0;

        foreach (Transform child in t)
        {
            var meshFilter = child.GetComponent<MeshFilter>();
            var mesh = meshFilter.sharedMesh;

            vertices.AddRange(mesh.vertices);

            if (mesh.uv.Length == 0)
            {
                // UVが無いモデルは0で埋める。
                for (var i = 0; i < mesh.vertices.Length; i++)
                    uvs.Add(Vector2.zero);
            }
            else
            {
                uvs.AddRange(mesh.uv);
            }

            var tris = mesh.triangles;
            foreach (var i in tris) indexes.Add(i + index);

            index += mesh.vertices.Length;
        }
        var material = new Material(Shader.Find("Standard"));

        if (t.childCount - 1 > 0)
        {
            // FIXME 最後のマテリアルをセットするが、意図したマテリアルになる保証はない。
            var meshRenderer = t.GetChild(t.childCount - 1).GetComponent<MeshRenderer>();
            material = new Material(meshRenderer.sharedMaterial);
        }
        // FIXME 最後のマテリアルをセットするが、意図したマテリアルになる保証はない。
        //var meshRenderer = t.GetChild(t.childCount-1).GetComponent<MeshRenderer>();
        //var material = new Material(meshRenderer.sharedMaterial);

        newMesh.vertices = vertices.ToArray();
        newMesh.triangles = indexes.ToArray();
        newMesh.uv = uvs.ToArray();
        newMesh.RecalculateNormals();
        CreateObject(newMesh, material, t.name, assetDir, parent);

    }

    // GameObject生成
    private void CreateObject(Mesh mesh, Material material, string objName, string assetDir, Transform parent)
    {
        CreateAsset(mesh, objName, $"{assetDir}/mesh");
        CreateAsset(material, objName, $"{assetDir}/material");

        var go = new GameObject(objName);
        go.transform.localRotation = Quaternion.Euler(-90, 0, 0);
        go.transform.SetParent(parent);
        var mf = go.AddComponent<MeshFilter>();
        mf.sharedMesh = mesh;
        var mr = go.AddComponent<MeshRenderer>();
        mr.sharedMaterial = material;
    }

    // アセット生成
    private void CreateAsset(Object obj, string assetName, string directory)
    {
        if (AssetDatabase.Contains(obj)) return;

        var dir = Path.Combine(Application.dataPath, directory);
        if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);

        var fileName = assetName + ".asset";

        var path = Path.Combine(Application.dataPath, directory, fileName);

        var cnt = 1;
        while (File.Exists(path))
        {
            fileName = $"{assetName} {cnt}.asset";
            path = Path.Combine(Application.dataPath, directory, fileName);
            cnt++;
        }

        AssetDatabase.CreateAsset(obj, Path.Combine("Assets", directory, fileName));
    }
}
```

基本はサイトのプログラムまんまです。
選択する所のみを良い感じ改良しました。

フォルダ選択

https://docs.unity3d.com/ja/current/ScriptReference/EditorUtility.OpenFolderPanel.html

##### 最後に
PLATEAUデータを扱う時に、毎回どんな風に扱っていたっけな～って
思いながらgoogle先生に聞いてました。（今回もそうでした。）
なので、自分用に色々まとめたのといつも疑問に思ってい所を書きました。
スケールの問題を私にやさしく教えてくれる方いましたら教えてください。