20
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C#でBlenderのファイルからパーティクルの位置を取り出してWebGLデモを作ってみた

Last updated at Posted at 2014-12-11

【この記事はWebGL Advent Calendar 2014 11日目です】

はじめまして。ワロタロです。
個人的にWebGLのゲームを作っていて、3DのエディタにはBlenderを使っています。

BlenderでWebGLというと、普通は何らかのエクスポータを使ってファイルを出力するのではないかと思います。しかし、筆者は何の因果かC#でBlenderのファイルを解析してjson形式のテキストファイルを出力させています。

なにか深い戦略があったわけではないので後付けですが理由を挙げてみました。

筆者がC#でBlenderファイルを扱う理由

  • 標準のエクスポータではだいたいデータが足りない、または加工が必要
  • ゲームを作り始めた当時はBlenderの2.5系の出始めでエクスポータを書くには情報が少なかった
  • それ以前にPythonが書けなかった
  • C#はVisual Studioのコーディング支援とデバッグが強力、Linqもある
  • とにかくC#で書いておけば損はしないと先輩が言ってた(言語の将来性)

こういうのは大抵真ん中あたりのが本当の理由です(ぉぃ

それと、できるだけ自分で作りたいという思いがあります。WebGLに限らず3DコンテンツのDIYはそれなりに大変なのですが、うまくいけば目的のコンテンツに最適なシステムやデータを作ることができます。

今回はそんなDIYな筆者の、C#とBlenderによるオレオレWebGL作成過程をご紹介します。
なお、WebGL成分は少なめです。ごめんなさい。

#やること・やったこと
クリスマスツリーのライブデモを作成しました。

  1. Blenderでシーンを作成
  2. .blendファイルについて知る
  3. C#で.blendファイルからパーティクルの位置情報を読み込む
  4. C#でjson(TypeScript)を出力
  5. WebGLで表示

webgl_rendered2.jpg

スペルミスはご愛嬌という事で(自分で言う)。

ライブデモ
ソースコードzip (C#、TypeScript)

Githubとかを持っていないので直リンクです。

#1.Blenderでシーンを作る
Blenderはいわずと知れたオープンソースの3DCGツールです。バージョン2.5以降から色々と近代的な雰囲気になり、レンダリングエンジンも増えるなど発展めざましいです。筆者はゲームを作りはじめた当時主流だった2.49をいまだに使っていたりしますが…。

ともかく、まずはBlenderでシーンを作成します。シーンはBlender界では皆知っているAndrew Price氏のチュートリアル 『Create a Christmas Wallpaper in 10 Minutes 』 をベースにさせていただきました。

次の画像は筆者がチュートリアルをもとにレンダリングしたものです。下手ですね。Andrew Price氏の偉大さがわかる良い例ですね。

rendered.jpg

Blenderのバージョンは2.60です。バージョンによってレンダリング結果がかなり違うため注意が必要です。

particles_in_blender.jpg

Blenderではパーティクルはメッシュの形状に合わせて配置され、パーティクルの一つ一つの位置情報が.blendファイルに保存されます。ワールド座標で位置が保存されているようなので簡単に扱えるのでは?と思ったのが、今回パーティクルを題材にした理由です。

ただ、C#での読み込でみるとHairではパーティクルのワールド座標がそのまま保存されていなかったため、Emitterで発生時刻を1~1にして代用しました。

#2..blendファイルについて知る

.blendはBlenderのファイル形式の拡張子です。.blendファイルには次のような特徴があります。

  • バイナリデータ
  • ファイルヘッダと複数のデータブロックで構成
  • データブロックの中身はブロックヘッダ(BHead)とc言語の構造体の配列
  • ファイル内の全構造体の仕様を記述したDNA構造体が同梱されている

blendfile.jpg

.blendファイルは、いわゆるTLV(Type Length Value)形式のデータブロックを集めたバイナリデータです。データブロックを切り出すだけであれば、c言語(Blenderはc言語で作られています)以外の多くの言語でも比較的簡単に読み込むことができます。

特徴の3つ目に挙げたDNA構造体ですが、ここにファイル内の全ての構造体の仕様が記述されています。つまりファイル内にどのような構造体があり、その構造体にはどのようなフィールドがどの位置にあるか、という情報が記述されています。また、この情報を使った仕組みをDNAと呼ぶようです。こちらのサイト『The mystery of the blend』がとても参考になります。

BlenderはDNAを使うことで高い後方互換性を実現している…らしいです。Blenderはバージョンが進むテンポが速いので、それを見越して考えられた仕組みかもしれません。そのあたりの詳しいことは筆者はわからないのですが、DNAのおかげでBlenderのバージョンを気にすることなくC#読み込み用のクラスを作ることができたのは確かです。

今回必要なパーティクルの情報を取得するには

 Object構造体 → ParticleSystemのリスト → ParticleSystem構造体 → ParticleData構造体 → ParticleKey構造体

とデータを辿っていく必要がありますが、こういった構造を知るにはBlenderのソースコードを見るか、DNA構造体をダンプして確認することになります。実は読み込み処理自体よりもBlenderを理解することの方が敷居が高かったりします。

ちなみに2.4系であれば前述のサイトに全ての構造体のダンプのページがあります。

#3.C#による読み込み

実はBlenderはc言語の構造体をメモリ上の配置のままファイルに書き出しています。読み込みもそのまま構造体に読み込みます。そのためファイルの読み書きが非常に高速で嬉しいのですが、C#では同じことができません。メモリ上の指定した位置に構造体を当てはめて使うというようなことがしづらいのです。すごく頑張ればできるらしいですが。

そこでDNAを利用することである程度簡単に構造体を扱う仕組みを作りました。
とりあえずBlenderFileReader.csという名前にしました。今回のソースコードのzipファイルに含まれています。
開発環境はVisual Studio Express 2013 for Windows Desktopです。

以下は実際の使用例です。

C#
    var inputFilePath = @"..\..\tree.blend";
    var outputFilePath = @"..\..\kurisumasuturee.ts";

    // ファイル読み込み
    var all_DataSetList = DNADataSet.ReadFromFile(inputFilePath);

    // ParticleSystemを持つObjectのみ抽出
    var object_DataSetList = all_DataSetList
        .Where(ds => ds.StructureName == "Object")
        .Where(ds => ds["particlesystem"]["first"].GetAddress() != 0)
        .ToList();

    var blenderObjectList = new List<BlenderObject>();
    foreach (var object_DataSet in object_DataSetList)
    {
        blenderObjectList.Add(new BlenderObject()
        {
            Name = object_DataSet["id"]["name"].GetString(),
            ParticleSystemAddressFirst = object_DataSet["particlesystem"]["first"].GetAddress(),
            ParticleSystemAddressLast = object_DataSet["particlesystem"]["last"].GetAddress(),
        });
    }

    // ObjectのParticleSystemを取得
    foreach (var blenderObject in blenderObjectList)
    {
        var particleSystem_Address = blenderObject.ParticleSystemAddressFirst;

        // ObjectのParticleSystemは連結リストであるため、
        // 連結リストの最後まで抽出する
        while (true)
        {
            var particleSystem_BHead = all_DataSetList
                .First(bh => bh.OldMemoryAddress == particleSystem_Address);

            var blenderParticleSystem = new BlenderParticleSystem()
            {
                NextAddress = particleSystem_BHead["next"].GetAddress(),
                ParticleDataAddress = particleSystem_BHead["particles"].GetAddress(),
            };

            blenderObject.ParticleSystemList.Add(blenderParticleSystem);

            if (particleSystem_Address == blenderObject.ParticleSystemAddressLast)
            {
                break;
            }

            particleSystem_Address = blenderParticleSystem.NextAddress;
        }
    }

    // ParticleSystemのParticleData、ParticleKeyを取得
    foreach (var blenderObject in blenderObjectList)
    {
        foreach (var particleSystem in blenderObject.ParticleSystemList)
        {
            var particleData_DataSet = all_DataSetList
                .First(bh => bh.OldMemoryAddress == particleSystem.ParticleDataAddress);

            // ParticleDataのブロックは複数個のデータを持つため、
            // 個数分のParticleDataを抽出する
            for (int i = 0; i < particleData_DataSet.ContentCount; i++)
            {
                // ParticleKeyを取得
                var particleKey = particleData_DataSet[i]["state"];

                particleSystem.BlenderParticleDataList.Add(new BlenderParticleData()
                {
                    ParticleKey = new BlenderParticleKey()
                    {
                        Location = particleKey["co"].GetFloat32Array(),
                        Velocity = particleKey["vel"].GetFloat32Array(),
                        Rotation = particleKey["rot"].GetFloat32Array(),
                        Average = particleKey["ave"].GetFloat32Array(),
                        Time = particleKey["time"].GetFloat32(),
                    }
                });
            }
        }
    }

DNADataSet.ReadFromFileの中でDNA構造体を解析したりDNAをもとにディクショナリ風にアクセスするためのクラスを生成したりしています。なお、Blender~で始まるクラスは結果データを保存するためのクラスで、今回作った仕組みとは関係ありません。

#4.C#による出力

以下のような形式のjsonと思いきやTypeScriptを出力します。
出力用の特別な仕組みは無く、ひたすら文字列を作って出力しています。

TypeScript
var Particles = {
 "FarLights": [{"Loc": [2.3297, 5.3748, 0.4265], "Rot": [0.0, 0.0, 1.4995]}, {"Loc": [-3.687 ...
 , "NearLights": [{"Loc": [1.0023, -11.9331, -0.0686], "Rot": [0.0, 0.0, 2.4905]}, {"Loc": [ ...
 , "Tree": [{"Loc": [2.4578, -3.0388, 0.3601], "Rot": [0.0, 0.0, 0.2415]}, {"Loc": [3.2480,  ...
 , "TreeTop": [{"Loc": [2.5865, -1.6480, 2.6617], "Rot": [0.0, 0.0, 3.3748]}, {"Loc": [2.573 ...
};

#5.WebGLで表示

シーンと同じ方法でBlenderで作成したテクスチャ

particle.jpg

をパーティクルの位置に描画します。今回は行列演算ライブラリ以外はTypeScriptで全て書いてみました。
開発環境はVisual Studio Express 2013 for Webです。

webgl_rendered3.jpg

特筆すべき点は特に無いのですが、Visual Studioのデバッグ機能とIEでテストをして、それがChromeやFire Foxでそのまま動いたのは少し嬉しかったです。普段はChromeで開発していますので…。

#まとめ
.blendファイルをC#で読み込み、WebGL用にパーティクルの位置を出力し、クリスマスツリーを表示しました。
Blenderを活用する手段も増えたかもしれません。

明日はjThree開発者の松田光秀さんです!

20
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?