RustでVRMファイルをパースしてみた

本記事では、Rustという安全性と高速性を兼ね備えた言語を使って、VRMファイルを読み込み、その内部構造を解析してみた手順を紹介します

## 環境準備

今回使用する環境は以下の通りです：

- Rust 1.70以上
- 必要なクレート：
  - `gltf` - glTF/GLB形式のパーサ（VRMはglTFの拡張形式）
  - `serde_json` - JSON操作用

まずは新しいRustプロジェクトを作成し、必要なクレート（ライブラリ）を追加します：

```bash
# 新しいプロジェクトの作成
cargo new vrm_parser
cd vrm_parser

# 必要なクレートの追加
cargo add gltf
cargo add serde_json
```

## VRMファイルとは

VRM（Virtual Reality Model）は、humanoid型の3Dアバターデータを扱うためのファイルフォーマットです。基本的には、glTF 2.0という3Dモデル用の標準フォーマットを拡張したもので、アバターの表情や物理演算、利用許諾情報などが含まれています。

VRMは内部的にglTF（あるいはバイナリ形式のGLB）として構成されており、拡張情報が含まれています。そのため、まずはglTFのパーサを使ってVRMファイルを読み込む必要があります。

## コードの全体像

以下が今回実装するVRMファイル解析プログラムの全体像です：

```rust
mod vrm_loader;
use std::{fs, path::Path};
use gltf::Gltf;
use serde_json::{Map, Value};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // VRMファイルのパスを指定
    let vrm_file_path = Path::new("models/sample_avatar.vrm");
    
    // ファイルをバイナリとして読み込み
    let vrm_data = fs::read(vrm_file_path)?;
    
    // gltfクレートを使ってVRMファイルを解析
    let gltf_content = Gltf::from_slice(&vrm_data)?;
    let document = gltf_content.document;
    
    // JSON形式に変換して中身を調べる
    let gltf_json = &document.clone().into_json();
    println!("{:?}", &gltf_json);
    
    // 基本情報の表示
    println!("VRM file loaded successfully!");
    println!("Assets: {:?}", &gltf_json.asset);
    println!("Scenes: {:?}", &gltf_json.scenes);
    println!("Node Count: {:?}", &gltf_json.nodes.len());
    println!("Mesh Count: {:?}", &gltf_json.meshes.len());
    println!("Material Count: {:?}", &gltf_json.materials.len());
    println!("Texture Count: {:?}", &gltf_json.textures.len());
    println!("Image Count: {:?}", &gltf_json.images.len());
    println!("Animation Count: {:?}", &gltf_json.animations.len());
    println!("Skin Count: {:?}", &gltf_json.skins.len());
    println!("Camera Count: {:?}", &gltf_json.cameras.len());
    
    // 拡張情報の取得と表示
    let extension = document.extensions() as Option<&Map<String, Value>>;
    for (key, _) in extension.unwrap_or(&Map::new()) {
        println!("Extension Key: {:?}", key);
    }
    
    // VRM拡張情報の詳細取得
    if let Some(vrm_extension) = document.extension_value("VRM") as Option<&Value> {
        if let Some(meta) = vrm_extension.get("meta") {
            if let Some(title) = meta.get("title") {
                println!("VRM Title: {:?}", title);
            }
            if let Some(version) = meta.get("version") {
                println!("VRM Version: {:?}", version);
            }
            if let Some(author) = meta.get("author") {
                println!("VRM Author: {:?}", author);
            }
            if let Some(spec) = meta.get("specVersion") {
                println!("VRM Spec Version: {:?}", spec);
            }
        } else {
            println!("No VRM meta information found.");
        }
    } else {
        println!("No VRM extension found.");
    }
    
    Ok(())
}
```

ここでは、VRMファイルを読み込み、その内部構造を解析して、基本情報やVRM特有の拡張情報を表示しています。それでは、このコードを段階的に解説していきましょう。

## 各部分の詳細解説

### 1. ファイルの読み込み

```rust
let vrm_file_path = Path::new("models/ichigofrog_0314.vrm");
let vrm_data = fs::read(vrm_file_path)?;
```

この部分では、Rustの標準ライブラリである`std::fs`を使ってVRMファイルをバイナリデータとして読み込んでいます。`Path::new()`で指定したパスにあるファイルを、`fs::read()`関数で読み込み、結果はバイト配列(`Vec<u8>`)として`vrm_data`に格納されます。

`?`演算子は、エラーが発生した場合にそのエラーを呼び出し元に伝播させるRustの機能です。例えばファイルが存在しない場合などにエラーが発生します。

### 2. gltfクレートを使った解析

```rust
let gltf_content = Gltf::from_slice(&vrm_data)?;
let document = gltf_content.document;
```

読み込んだバイナリデータを`gltf`クレートの`Gltf::from_slice()`関数に渡して解析します。VRMはglTFのサブセットなので、glTFのパーサで解析できます。`gltf_content`からは`document`プロパティを取得して、ドキュメントのメインコンテンツにアクセスします。

### 3. 基本情報の表示

```rust
let gltf_json = &document.clone().into_json();
println!("{:?}", &gltf_json);

println!("VRM file loaded successfully!");
println!("Assets: {:?}", &gltf_json.asset);
println!("Scenes: {:?}", &gltf_json.scenes);
// 他の情報表示...
```

ここでは、`document`をJSON形式に変換して、様々な基本情報を表示しています。glTF形式には以下のような要素が含まれています：

- **asset**: バージョンなどのglTFアセット情報
- **scene**: シーンの構造
- **nodes**: 3Dオブジェクトのノード（階層構造）
- **meshes**: 3Dメッシュデータ
- **materials**: マテリアル（テクスチャの割り当て方）
- **textures**: テクスチャ情報
- **images**: 画像データ
- **animations**: アニメーション情報
- **skins**: スキニング情報（ボーンによる変形）
- **cameras**: カメラ情報

これらの要素を表示することで、モデルの基本的な構成を把握できます。

### 4. 拡張情報の探索

```rust
let extension = document.extensions() as Option<&Map<String, Value>>;
for (key, _) in extension.unwrap_or(&Map::new()) {
    println!("Extension Key: {:?}", key);
}
```

glTFフォーマットは拡張可能な設計になっており、VRMはその拡張の一つです。ここではどのような拡張が含まれているかをチェックしています。VRMファイルの場合、`"VRM"`という拡張キーが存在するはずです。

`unwrap_or()`メソッドは、拡張情報が存在しない場合に空のマップを返す安全な方法です。

### 5. VRM特有の情報取得

```rust
if let Some(vrm_extension) = document.extension_value("VRM") as Option<&Value> {
    if let Some(meta) = vrm_extension.get("meta") {
        // タイトル、バージョン、作者などの取得
        // ...
    } else {
        println!("No VRM meta information found.");
    }
} else {
    println!("No VRM extension found.");
}
```

ここでは、VRM拡張情報から「メタデータ」を取得し、モデルのタイトル、バージョン、作者などの情報を表示しています。

Rustの`if let`構文は、パターンマッチングの一種で、「値がある場合のみ処理を行う」という場合に便利です。これにより、拡張情報がない場合や、必要なメタデータがない場合にも適切に対応できます。

## 実行結果の例

以下は、VRMファイルを解析した場合の出力例です：

```
VRM file loaded successfully!
Assets: Asset { version: "2.0", generator: Some("VRM Exporter"), min_version: None, copyright: None, extensions: None, extras: None }
Scenes: [Scene { name: Some("Scene"), nodes: [0, 1], extensions: None, extras: None }]
Node Count: 35
Mesh Count: 1
Material Count: 3
Texture Count: 6
Image Count: 6
Animation Count: 0
Skin Count: 1
Camera Count: 0
Extension Key: "VRM"
VRM Title: "サンプルアバター"
VRM Version: "1.0"
VRM Author: "サンプル作者"
VRM Spec Version: "0.0"
```

このような情報を取得できれば、VRMモデルの基本的な情報を把握できたことになります。

## 発展的な活用方法

今回はVRMファイルの基本情報を取得するだけでしたが、このコードを拡張することで以下のようなことにも挑戦してみたいです：

1. **モデルの構造解析**: ボーン（スケルトン）の階層構造を解析して表示
2. **メッシュデータの取得**: 頂点や面のデータを取得して3D描画や物理演算に利用
3. **表情（BlendShape）の抽出**: VRMの特徴である表情データを取得
4. **VRMのバージョン変換**: VRM 0.xからVRM 1.0への変換ツール