[C# WPF] 3D Model Explorer #01 — VRM/PMXサムネイル一覧管理ツールを作成
作成動機
VRMやPMXファイルが増えてくると、ファイル名だけでは何のモデルかわからない。
Windowsエクスプローラーにはサムネイル表示がなく、単体ファイルのビューアーはあるが、フォルダ内のモデルをサムネイル一覧で管理するツールが存在しない。
「VRM閲覧ソフト」「3D Model Explorer」で検索しても、同様のツールは見つからなかった。
ないなら作る。
技術スタック
- 言語: C# / WPF (.NET 8.0)
- 外部ライブラリ: なし(標準ライブラリのみ)
- 総コード量: 約800行(MainWindow.xaml + MainWindow.xaml.cs)
- 配布: self-contained SingleFile(.NETランタイム不要、ZIP配布で約64MB)
NuGetパッケージを一切使わず、System.Text.Json、System.IO.Compression、BinaryReader だけで全機能を実装している。
アーキテクチャ
画面構成
WPFの Grid による3ペインレイアウト + ツールバー + ステータスバー。
┌─ ツールバー(フォルダパス + 検索)──────────────────┐
├──────┬──────────────────┬──────────┤
│ 左ペイン │ 中央(サムネグリッド) │ 右ペイン │
│ お気に入り │ WrapPanel in ScrollViewer │ ファイル情報 │
│ 登録フォルダ│ │ メタデータ │
│ タグフィルタ│ │ タグ │
├──────┴──────────────────┴──────────┤
│ ステータスバー │
└────────────────────────────────────┘
ペイン間には GridSplitter を配置して、ドラッグで幅を調整可能にしている。
データ永続化
設定データは %APPDATA%/3DModelExplorer/settings.json にJSON形式で保存。
{
"Folders": ["D:\\path\\to\\vrm_folder", "D:\\path\\to\\pmx_folder"],
"Favorites": ["D:\\path\\to\\model.vrm"],
"Tags": {
"D:\\path\\to\\model.vrm": ["anime", "VRChat用"]
}
}
System.Text.Json の JsonSerializer でシリアライズ/デシリアライズ。アプリ起動時に読み込み、変更のたびに書き出し。
実装の詳細
VRMサムネイル抽出
VRMファイルの実体はGLB(glTF Binary)。バイナリ構造は以下の通り。
[GLBヘッダー 12bytes]
magic: 0x46546C67 ("glTF")
version: uint32
length: uint32
[JSONチャンク]
chunkLength: int32
chunkType: uint32 (JSON)
data: byte[] → UTF-8 JSON文字列
[BINチャンク]
chunkLength: int32
chunkType: uint32 (BIN)
data: byte[] → バイナリデータ(テクスチャ、メッシュ等)
サムネイルの取得手順は以下。
1. GLBヘッダーの検証
BinaryReader でマジックナンバー 0x46546C67 を確認。VRMでなければ即 return null。
2. JSONチャンクのパース
JSONチャンクを System.Text.Json.JsonDocument でパース。VRM 0.xと1.0でメタデータの構造が異なるため、分岐処理が必要。
VRM 0.x: extensions.VRM.meta.texture → textures[n].source → images[n]
VRM 1.0: extensions.VRMC_vrm.meta.thumbnailImage → images[n]
VRM 0.xは texture インデックス → textures 配列 → source で images インデックスを取得する二段階の参照。VRM 1.0は thumbnailImage が直接 images インデックスを持つ。
3. BINチャンクからの画像抽出
images[n].bufferView から bufferViews の byteOffset と byteLength を取得し、BINチャンク内の該当バイト列を BitmapImage に変換。
fs.Seek(binDataOffset + byteOffset, SeekOrigin.Begin);
var imageData = reader.ReadBytes(byteLength);
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.StreamSource = new MemoryStream(imageData);
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze(); // UIスレッド外で使えるようにFreeze
Freeze() は重要で、これを呼ばないとWPFのスレッドアフィニティでクロススレッドアクセス時に例外が発生する。
PMX/FBXサムネイル抽出(ZIP対応)
MMDモデル(PMX)やFBXモデルはZIPで配布されることが多い。ZIP内の thumbnail.png、thumbnail.jpg、thumbnail.webp を探索して抽出する。
using var archive = ZipFile.OpenRead(zipPath);
var entry = archive.Entries.FirstOrDefault(e =>
thumbnailNames.Contains(e.Name.ToLower()));
System.IO.Compression.ZipFile で開いて、エントリ名でマッチング。ストリームから BitmapImage に変換する流れはVRMと同じ。
ZIP内のモデル形式判定
ZIPのラベル表示は、内部のファイル拡張子で判定する。
private string DetectZipModelType(string zipPath)
{
using var archive = ZipFile.OpenRead(zipPath);
foreach (var entry in archive.Entries)
{
var ext = Path.GetExtension(entry.Name).ToLower();
if (ext == ".pmx") return "PMX";
if (ext == ".fbx") return "FBX";
if (ext == ".glb" || ext == ".gltf") return "GLB";
}
return "ZIP";
}
これにより、同じ .zip でも中身がPMXなら「PMX」、FBXなら「FBX」とラベル表示される。将来的にGLBやその他の形式にも拡張可能。
VRMメタデータ抽出
GLBのJSONチャンクから、VRMの extensions 内のメタデータを取得する。
VRM 0.xと1.0で構造が異なる。
VRM 0.x:
extensions.VRM.meta.title → string
extensions.VRM.meta.author → string
extensions.VRM.meta.version → string
extensions.VRM.meta.licenseName → string
VRM 1.0:
extensions.VRMC_vrm.meta.name → string
extensions.VRMC_vrm.meta.authors → string[]
extensions.VRMC_vrm.meta.version → string
extensions.VRMC_vrm.meta.licenseUrl → string
VRM 1.0では authors が配列になっている点に注意。EnumerateArray() で展開して string.Join で結合。
PMXメタデータ抽出
PMXのバイナリヘッダーからモデル名とコメントを読み取る。
PMXヘッダー構造:
[4bytes] シグネチャ "PMX " (0x504D5820)
[4bytes] バージョン (float: 2.0 or 2.1)
[1byte] グローバル情報バイト数
[n bytes] グローバル情報
globals[0]: エンコーディング (0=UTF-16LE, 1=UTF-8)
[4bytes] モデル名(日本語)長さ
[n bytes] モデル名(日本語)
[4bytes] モデル名(英語)長さ
[n bytes] モデル名(英語)
[4bytes] コメント(日本語)長さ
[n bytes] コメント(日本語)
エンコーディングフラグで Encoding.Unicode(UTF-16LE)か Encoding.UTF8 を切り替える。ZIPの場合は ZipArchiveEntry.Open() でストリームを取得してから同じ ReadPmxHeader に渡す。
お気に入り機能
サムネカードに ☆/★ トグルボタンを配置。クリックで settings.json の Favorites 配列に追加/削除。
お気に入り一覧表示時には、ファイルの存在確認を行い、削除済みファイルを自動除外する。
var validFavorites = _settings.Favorites.Where(f => File.Exists(f)).ToList();
VRM/PMX/FBX混在のリストでも、CreateThumbnailCard を共通で使うため、カードの見た目は統一される。
タグ機能
モデルごとにタグを付与し、フィルタリングに利用する。
データ構造は Dictionary<string, List<string>>(ファイルパス → タグリスト)。右ペインのタグ入力UIは、TextBoxでEnterキー押下またはボタンクリックでタグ追加。# プレフィックスは自動付与/除去。
タグ削除は各タグの「✕」ボタンで個別に削除可能。左ペインのプルダウンで登録済みタグの一覧からフィルタリングできる。
検索機能
ファイル名の部分一致検索。「現フォルダ」と「全登録フォルダ」のスコープ切り替え対応。
matchedFiles.AddRange(GetModelFiles(folderPath)
.Where(f => Path.GetFileName(f).ToLower().Contains(query)));
モデルファイルの列挙は GetModelFiles メソッドに共通化し、LoadFolder と ExecuteSearch の両方で利用。
なぜ誰も作らなかったのか
3Dモデルのサムネイル一覧ツールは需要があるにもかかわらず、同等のツールは見当たらない。
理由は、必要な知識の組み合わせが希少だから。
- VRM(GLB)のバイナリ構造を理解している
- VRM 0.xと1.0のメタデータ仕様の違いを把握している
- PMXのバイナリヘッダーも読める
- MMD配布ファイル(ZIP)の慣習を知っている
- Windowsデスクトップアプリ(WPF)も書ける
技術的には難しくない。個々の処理はバイナリの読み取りとJSONパースだけ。知っているかどうかの問題。
自分の場合、1年間のWeb開発(VRM Thumbnail Editor、VRM Validator、PMX Checker等)でGLB/PMXのバイナリ知識を蓄積しており、それをそのままC#に移植できた。
開発環境と開発時間
- IDE: Visual Studio 2022 + VS Code(ターミナル操作用)
- ソース管理: Git + GitHub(Private)
- 開発時間: 約5時間(朝4時〜9時)
MainWindow.xaml.csの1ファイルに全ロジックを集約。v1.0.0時点で約800行。
ビルドと配布
self-contained SingleFileビルドで、.NETランタイムなしで動作する単一EXEを生成。
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
ZIP圧縮で約64MBの配布ファイルになる。
今後の予定
- UI改善(ダークテーマ、カード角丸、ホバーエフェクト)
- 3Dプレビューポップアップ(OpenTK or HelixToolkit)
- GLBサムネイル対応(独自拡張でthumbnail埋め込み)
- サブフォルダ再帰スキャン
- 配布(GitHub Releases or Microsoft Store)
リポジトリ
GitHub: fixtan/3D_Model_Explorer(現在Private)