教育現場で使える！無料の日本の街3DデータをUnityに表示するまで

UnityやBlenderで、日本のリアルな街並みを手軽に再現してみたい──そう考える学生や教育関係者の方は多いのではないでしょうか。

でも、「すぐに使える、良い感じの街の3Dデータって、どこにあるの…？」と迷うこと、ありませんか？ 実はこれ、多くの人が直面する共通の悩みです。

たとえば有名なのが、国土交通省が整備する「PLATEAU（プラトー）」。最近ではUnity向けにPrefab形式で提供されているので、「とにかく表示させるだけ」なら非常に簡単。Unityにインポートするだけで、日本の街並みが高精細に再現されます。

ただ、カスタマイズや教育目的で中身を詳しくいじろうとすると、独自のマテリアル構造（例：PlateauTriplanarシェーダー）やAPIが壁になることも。「どこでテクスチャ貼ってるの？」「バラバラの建物をどう統合するの？」と戸惑う学生さんの声も少なくありません。

一方で、産総研が提供する「[3DDB Viewer](https://www.digiarc.aist.go.jp/team/gsvrt/information/digiarch-3ddb-viewer.html/)」は、OBJやFBXといった標準フォーマットでデータが提供されており、UnityやBlenderでの“自前構築”を学ぶにはうってつけです。

例：
![スクリーンショット 2025-06-05 8.56.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/a2c9153d-0e76-4dc2-9b7c-388d3951cc4d.png)


ただし、こちらはそのまま読み込むとテクスチャずれや座標系の違いといった細かいトラブルが起こるため、ちょっとした調整が必要になります。

本記事では、その「調整」の手間を自動で解消するBlender用の変換スクリプトをご紹介します！

# これが実際にUnityに組み込んだお台場の3Dモデルです

![スクリーンショット 2025-06-05 8.43.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/adfc2203-01c3-4161-a424-5c9a12d018f4.png)

試行錯誤しながら、BlenderとPythonスクリプトで自動化して、Unityに組み込みできました！（感動）

自動ツール化した際にハマったポイントはこちらです。

- PNGファイルのテクスチャはblenderで取り込んでくれない（たぶん）
- 空白入りのテクスチャファイルがあって、取り込めない。（つらかった）
- デフォルトのカメラなどを削除したかった
- 座標系が違うのでUnityに変換しないと、Unityでの手作業の手間が残る


# 必要なもの

- Blender 3.6 
  - **(補足)** 筆者の環境ではBlender 4.X系でOBJインポート時に一部アドオンとの相性問題（またはBlender自体の変更）からか、うまく動作しないケースがありました。そのため、比較的安定している長期サポート版（LTS）でもあるバージョン3.6を推奨します。他のバージョンでも動作する可能性はありますが、もしうまくいかない場合は3.6でお試しください。
- **Pythonライブラリ「Pillow」**:
  - スクリプト内でPNG画像をJPGに変換する処理などで使用します。通常、BlenderにバンドルされているPython環境にはPillowが含まれていないため、別途インストールが必要です。
  - **インストール方法例 (Windowsの場合):**
    1. Blenderのインストールフォルダ内 (例: `C:\Program Files\Blender Foundation\Blender 3.6\3.6\python\bin`) にある `python.exe` のパスを確認します。
    2. コマンドプロンプトやPowerShellを開き、そのパスを使ってpipでインストールします。
       ```bash
       "C:\Program Files\Blender Foundation\Blender 3.6\3.6\python\bin\python.exe" -m pip install Pillow
       ```
    3. (上記パスはご自身の環境に合わせて適宜変更してください)
- Unity Hub/Unity
- ブラウザ（今回はChromeを使いました）

# 3Dモデルのダウンロード

まずは、ブラウザでこちらにアクセスしましょう！

https://gsrt.digiarc.aist.go.jp/3ddb_demo/tdv/index.html

ジャーン、地球が見えます！

![スクリーンショット 2025-06-05 8.45.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/c77d275a-7959-4b22-ba8d-38091c297730.png)

左上のGUI上で、場所を設定して、サーチをかけることができます。今回は、Titleに「お台場」と入力し、Model Typeに Surfaceと入力しました。あとは、所望のものをダウロードするだけ！

![スクリーンショット 2025-06-05 8.45.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/3925aca7-b592-4e82-a888-77dafe1157c9.png)

TIPS：
- Titleは、おそらく、3Dモデルに与えられているキーワードです
- Model Typeについては、点群データとか、構造データなど、色々あるのですが、Unityに組み込む場合は Surfaceだけで良いので、フィルタしました

# OBJファイルをマージしてFBXに変換
ダウンロードしたものを展開すると、以下のようにフォルダが分かれています。部品ごとにフォルダ化されています。

![スクリーンショット 2025-06-05 8.46.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/e962e9d3-4b44-4004-b363-3bdb06e630de.png)

これらを１個ずつUnityに組み込むこともできますが、フォルダ数が１００個とかあると泣けてきます。なので、これらを一括してマージして、FBXファイルに変換するツールを作ったのです。（エライ）

このPythonスクリプトでは、主に以下のような処理を自動化しています。

* **面倒な前処理を自動化:**
    * 「ハマったポイント」でも触れた、ファイル名に含まれる**空白をアンダースコアに置換**して、Blenderでのエラーを防ぎます。
    * Blenderが直接扱いにくい**PNGテクスチャをJPG形式に自動変換**します。
    * 各部品（OBJファイル）に付属する**MTLファイル内のテクスチャパスを修正**し、テクスチャが正しく読み込まれるようにします。
* **Blenderでの作業を自動化:**
    * 指定したフォルダ内にある大量の**OBJファイルを順番にBlenderにインポート**します。
    * インポート時に自動で作成されることがある**不要なカメラやライト、初期キューブを削除**して、シーンをスッキリさせます。
    * インポートされた全ての部品（メッシュ）を**一つに統合（マージ）**します。
    * Unityで扱いやすいように、**オブジェクトの座標系を調整（X軸で-90度回転）**します。
* **Unity向けの出力:**
    * 最終的に**FBX形式でエクスポート**します。
    * 使用された**テクスチャファイルも出力フォルダにまとめてコピー**するので、Unityへの持ち込みが楽になります。

では、実際のスクリプトです！

```python
import bpy
import os
import addon_utils
import shutil
import sys
from PIL import Image

# Blenderの特殊なコマンドライン構造に対応
if "--" not in sys.argv:
    print("Usage: blender --background --python import_all_obj.py -- <top-level OBJ directory>")
    sys.exit(1)

args = sys.argv[sys.argv.index("--") + 1:]
if len(args) < 1:
    print("Usage: blender --background --python import_all_obj.py -- <top-level OBJ directory>")
    sys.exit(1)

root_dir = args[0]
addon_utils.enable("io_scene_obj")
output_dir = "./output_unity"
os.makedirs(output_dir, exist_ok=True)
used_textures = set()

# 空白が含まれるファイル名を正規化（スペース→アンダースコア）
def normalize_filenames(root):
    for dirpath, dirnames, filenames in os.walk(root):
        for name in filenames:
            if " " in name:
                old_path = os.path.join(dirpath, name)
                new_name = name.replace(" ", "_")
                new_path = os.path.join(dirpath, new_name)
                os.rename(old_path, new_path)
                print(f"Renamed: {old_path} → {new_path}")

# PNG→JPG変換
def convert_png_to_jpg(png_path):
    jpg_path = os.path.splitext(png_path)[0] + ".jpg"
    if os.path.exists(jpg_path):
        return jpg_path
    try:
        print(f"Converting PNG to JPG: {png_path} → {jpg_path}")
        with Image.open(png_path) as im:
            rgb_im = im.convert("RGB")
            rgb_im.save(jpg_path, "JPEG")
        return jpg_path
    except Exception as e:
        print(f"Failed to convert {png_path} to JPG: {e}")
        return None

# MTLファイルの処理
def fix_mtl_paths(mtl_path, base_dir):
    print(f"Processing MTL file: {mtl_path}")
    with open(mtl_path, "r", encoding="utf-8") as f:
        lines = f.readlines()

    modified = False
    new_lines = []

    for line in lines:
        if line.strip().startswith("map_Kd"):
            parts = line.strip().split(maxsplit=1)
            if len(parts) == 2:
                tex_file = parts[1].strip()
                tex_file = tex_file.replace(" ", "_")  # 一応ここでも置換

                tex_path = None

                # 解決候補パス
                candidates = [
                    tex_file,
                    os.path.abspath(tex_file),
                    os.path.join(root_dir, tex_file),
                    os.path.join(base_dir, tex_file),
                ]

                for candidate in candidates:
                    candidate = os.path.normpath(candidate)
                    if os.path.exists(candidate):
                        tex_path = candidate
                        break

                # PNGならJPGに変換
                if tex_path and tex_path.lower().endswith(".png"):
                    converted = convert_png_to_jpg(tex_path)
                    if converted:
                        tex_path = converted

                # テクスチャ使用記録＋書き換え
                if tex_path and os.path.exists(tex_path):
                    used_textures.add(tex_path)
                    tex_path_fixed = tex_path.replace("\\", "/")
                    if tex_file != tex_path_fixed:
                        print(f"Fixing texture path: {tex_path_fixed}")
                        line = f"map_Kd {tex_path_fixed}\n"
                        modified = True
                else:
                    print(f"Texture file not found: {tex_file}")

        new_lines.append(line)

    if modified:
        with open(mtl_path, "w", encoding="utf-8") as f:
            f.writelines(new_lines)

# 空白ファイル名の事前処理
normalize_filenames(root_dir)

# OBJ 読み込みループ
for dirpath, dirnames, filenames in os.walk(root_dir):
    for file in filenames:
        if file.lower().endswith(".obj"):
            obj_path = os.path.normpath(os.path.join(dirpath, file))
            mtl_path = obj_path.replace(".obj", ".mtl")

            if os.path.exists(mtl_path):
                fix_mtl_paths(mtl_path, dirpath)

            print(f"Importing: {obj_path}")
            bpy.ops.import_scene.obj(filepath=obj_path)

# カメラとライトを削除
for obj in bpy.data.objects:
    if obj.type in ['CAMERA', 'LIGHT' ]:
        bpy.data.objects.remove(obj, do_unlink=True)

for obj in bpy.data.objects:
    if obj.name.lower().startswith("cube"):
        bpy.data.objects.remove(obj, do_unlink=True)

# 統合と回転
print("Joining all imported objects...")
bpy.ops.object.select_all(action='SELECT')
objs = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']
if objs:
    bpy.context.view_layer.objects.active = objs[0]  # アクティブに設定
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.join()

    # -90度回転（X軸）
    obj = bpy.context.active_object
    obj.rotation_euler[0] -= 1.5708
else:
    print("No mesh objects to join or rotate.")


# FBX出力
print("Exporting to FBX...")
fbx_output = os.path.join(output_dir, "output.fbx")
bpy.ops.export_scene.fbx(
    filepath=fbx_output,
    use_selection=False,
    apply_unit_scale=True,
    bake_space_transform=True,
    axis_forward='-Z',
    axis_up='Y'
)

# テクスチャコピー
for tex_path in used_textures:
    if os.path.exists(tex_path):
        try:
            shutil.copy(tex_path, output_dir)
            print(f"Copied texture: {tex_path}")
        except Exception as e:
            print(f"Failed to copy {tex_path}: {e}")

```

(冗長なところあると思いますが、ご容赦ください）

# 使い方

blender をバックグラウンドモードで起動して、引数にこのスクリプト本体と、対象のルートディレクトリを指定します。

例：temp直下に先の解凍されたディレクトリ群がある場合

```bash
 blender --background --python import_all_obj.py -- ./temp
```

成功すると、output_unity というディレクトリが作成され、その中に、FBXファイルおよびテクスチャファイル一式が配置されています。

あとは、このディレクトリを、Unityのプロジェクトの任意の場所にドラッグ＆ドロップするだけです。


例：
![スクリーンショット 2025-06-05 8.48.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/658d45a6-4dad-4362-ab40-6fed339cf4bb.png)

output_unityの中を見ると、fbxファイル(output.fbx)がありますから、それをUnityシーンにドラッグ＆ドロップすれば完了です！

![スクリーンショット 2025-06-05 8.49.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244147/d1ce09fb-fbdd-4b83-ac64-ee9ba0c313dd.png)

# おわりに

今回は、産総研の3DDB Viewerからダウンロードした都市モデルを、BlenderのPythonスクリプトを使ってUnityで手軽に扱えるようにする方法をご紹介しました。

PLATEAUのような高機能なデータも素晴らしいですが、学習コストや取り回しの手軽さを考えると、本記事で紹介したような「ひと手間を自動化する」アプローチも、特に学生さんや教育現場では有効なのではないかと思います。

このスクリプトが、皆さんの3D都市モデル活用のハードルを少しでも下げ、新しい作品制作や研究の一助となれば幸いです。
ぜひ、お使いのPCで試してみてください！そして、もし「こんな風に改善できたよ！」「ここで詰まったけどこう解決した！」といった情報があれば、コメントなどで共有いただけると、さらに多くの方の助けになるかと思います。