Writing Blender Plugin in C++

ここしばらく DCC ツール上のモデルをリアルタイムに Unity に同期する、MeshSync というツールを開発しています。その Blender 対応にとても苦労したので情報を残しておこうと思います。
なお、Blender の世界ではプラグインは "add-on" と呼ぶことが多いようですが、本記事では "プラグイン" で統一します。

Blender は Maya や 3ds Max のような、総合型の 3DCG の DCC ツールです。競合するツールはいくつかありますが、主要な DCC ツールの中で Blender は唯一オープンソースフリーソフトウェアなのが最もユニークな点だと思います。
Blender の大部分は C で書かれています。C++ ではなく、C です!Blender はコア部分以外は基本 python で機能を拡張していく構造になっています。UI や各種編集機能、fbx in/exporter など根幹をなす機能も python で実装されているものが多く、ゆえに python バインディングに絡む部分はよくメンテされています。一方で BlenderC/C++ 用のプラグイン API を用意していません。手の混んだ拡張をしたいなら fork して独自ビルドを作れ、ということなのでしょう。よって、Blender 用に拡張機能を作るときは普通は python で書くことになります。

MeshSync も最初は python で実装していたのですが、同期はできたものの絶望的に遅く、到底実用には耐えられないものでした。pythonスクリプト言語の中でも遅い部類でであり、頂点一つ一つに対して加工を施す、というようなローレベルな処理には向きません。頂点一つにアクセスするたびに python オブジェクトの生成/破棄が行われるため、C/C++ でアクセスするのと比べて数百倍とかのオーダーで遅くなります。しかもマルチスレッド処理もできません。
これをどうにかするには、C/C++ で直接 Blender のデータを扱うしかありません。加えて、既存のメジャーリリースの Blender で動かないと意味がないので fork & 独自ビルドの選択肢は取れず、Windows, Mac, Linux で動作させたい&長期的にサポートしたいのであまり Hacky な手も使いたくありません。…というのが今回のチャレンジでした。

いつも通り成果物は github で公開されているので、詳しく知りたい場合は同時にソースコードも参照ください。正直この記事だけでは理解が困難な部分もあるかと思われます。
https://github.com/unity3d-jp/MeshSync/tree/20181130/Plugin/MeshSyncClientBlender

基本戦略&事前準備

Blender の pyhton オブジェクトは内部的に C 側のデータへのポインタを保持しています。そのポインタから、データだけではなく python バインディングの型情報へもアクセスできるようになっており、python から呼べる関数やプロパティは C++ からも呼べます。これを使ってどうにかするのが基本戦略になります。

事前準備として、Blender をソースからビルドしてデバッグ実行できるようにしておきます。これは必須です。何かわからないことがあったらとりあえずデバッグ実行、関係ありそうな部分で break して動作を調べる必要があるためです。加えて、Blender はビルドの過程で自動生成されるソースが大量にあり (主に python バインディング)、その部分を調べるためにも一度はビルドしなければなりません。
幸いビルドは簡単です。wiki の Building_Blender の手順通りにやればすんなり通ります。
https://wiki.blender.org/wiki/Building_Blender/Windows
svn でライブラリのバイナリを取得、git でソースを取得、cmake でプロジェクトを生成して VisualStudio でビルド。でかい OSS は大抵手順通りやってもどっかで躓くものですが、Blender は何の問題もなくビルドできました。偉い。

一点、BlenderソースコードGPL であることは留意しておきましょう。Blender のデータ構造にアクセスするには Blenderソースコードの .h ファイルをコピペして使うなどが必要になりますが、そうするとプラグインGPL に感染します。Blender に依存する部分が小さいのであれば、同じデータ構造の struct を自力で定義することで対応できるかもしれません。しかし Blender はバージョンアップのたびにデータ構造も細かく変わっていて、追従するのがとても大変になります。MeshSync では素直に GPL を受け入れることにしましたが、DCC ツール依存部分とそれ以外を別ライブラリに分離して感染を最小限に抑えています。

C++ から Blender に触れる

前述の通り、python オブジェクトから Blender のデータポインタを得てどうにかしていくのが基本方針となります。(Blender のデータポインタについては、@vipper36 さんが解説を書いてくださいました。感謝。:https://qiita.com/vipper36/items/3e6012c3c770ade0d412 )
ほとんどの処理を C++ 側で行うため、最終的に MeshSync は python 側はほぼ UI 処理とコールバック登録だけになりました。コールバックは 2.79 では bpy.app.handlers.scene_update_post を使っています。これはシーンになにか変化があったときに呼ばれるコールバックで、live link が目的な今回はおあつらえ向きです。2.80 ではタイマーを使っています (後述)。 pythonC++ の橋渡しには pybind11 を使いました。

pythonBlender のオブジェクトを扱うときは大抵 bpy.context, bpy.data, bpy.scene の 3 つが起点になります。まずは C++ でこれらを扱えるようにします。
bpy.context は C 側では bContext に相当します。bContext は常にグローバルに一つ存在するオブジェクトです。bpy.data や bpy.scene なども bContext から取得でき、プラグイン側は初期化時に bContext を得ておけばあとは大体やりたいことはできます。

python バインディングの型情報は StructRNA というオブジェクトに収められています。bpy.context もこれを持っているので、bContext を得るときについでに StructRNA も得ておきましょう。StructRNA はリンクリストになっており、一つ StructRNA を得られればそこから全ての型を巡回できます。

// python から bpy.context を引数に呼ばれる想定 
void setup(pybind11::object bpy_context)
{
    auto rna = (BPy_StructRNA*)bpy_context.ptr();
    if (strcmp(rna->ob_base.ob_type->tp_name, "Context") != 0)
        return;

    auto first_type = (StructRNA*)&rna->ptr.type->cont;
    while (first_type->cont.prev)
        first_type = (StructRNA*)first_type->cont.prev; // 最初の StructRNA までリンクをたどる
    
    // ...
}

ちなみに StructRNA に限らず、Blender の主要なデータは大体リンクリストで繋がっています。実に C 的です。
StructRNA はメンバ関数 (StructRNA::functions, FunctionRNA) やプロパティ (StructRNA::cont.properties, PropertyRNA) の情報も持っています。これらもリンクリストになっています。

python 用の関数 (FunctionRNA) を呼ぶには、引数と戻り値用のスペースをまとめたメモリブロックを用意し、それと bContext と self となるポインタを引数として FunctionRNA::call を呼びます。このメモリブロックは 1 byte align でなければなりません (VisualStudio なら #pragma pack(push, 1)、gcc や clang なら __attribute__( ( packed ) ) を指定)。例えば、float と int を受け取って float を返す Object のメンバ関数を呼びたい場合以下のようになります。

static bContext *g_context;

float test_call(Object *self, FunctionRNA *f, float a1, int a2)
{
    PointerRNA ptr;
    ptr.data = self;

#pragma pack(push, 1) // VisualStudio
    struct {
        float a1;
        int a2;
        float ret;
    }
#pragma pack(pop)
    params = {a1, a2};
    ParameterList param_list;
    param_list.data = &params;

    f->call(g_context, nullptr, &ptr, &param_list);
    return params.ret;
}

template である程度一般化ができます。

static bContext *g_context;

template<class R> struct ret_holder
{
    using ret_t = R&;
    R r;
    R& get() { return r; }
};
template<> struct ret_holder<void>
{
    using ret_t = void;
    void get() {}
};

#pragma pack(push, 1)
template<typename R, typename A1, typename A2>
struct param_holder2
{
    A1 a1; A2 a2;
    ret_holder<R> ret;
    typename ret_holder<R>::ret_t get() { return ret.get(); }
};
#pragma pack(pop)

template<typename T, typename R, typename A1, typename A2>
R call(T *self, FunctionRNA *f, const A1& a1, const A2& a2)
{
    PointerRNA ptr;
    ptr.data = self;

    param_holder2<R, A1, A2> params = { a1, a2 };
    ParameterList param_list;
    param_list.data = &params;

    f->call(g_context, nullptr, &ptr, &param_list);
    return params.get();
}

…この書き方だと引数の数分 param_holder と call のバリーエーションを作る必要があり、もっとスマートに書けそうな気もしますが、今回はこれで妥協しています。
プロパティは引数が self だけの関数なので単純です。スカラ用の getter と配列用の getter が別に用意されています。

template<typename Self>
static inline int get_int(Self *self, PropertyRNA *prop)
{
    PointerRNA ptr;
    ptr.id.data = ptr.data = self;

    return ((IntPropertyRNA*)prop)->get(&ptr);
}

template<typename Self>
static inline float get_float(Self *self, PropertyRNA *prop)
{
    PointerRNA ptr;
    ptr.id.data = ptr.data = self;

    return ((FloatPropertyRNA*)prop)->get(&ptr);
}

template<typename Self>
static inline void get_float_array(Self *self, float *dst, PropertyRNA *prop)
{
    PointerRNA ptr;
    ptr.id.data = ptr.data = self;

    ((FloatPropertyRNA*)prop)->getarray(&ptr, dst);
}

これで関数呼び出しとプロパティの取得はできるようになりました。あとは必要な型とその関数/プロパティを文字列ベースで探して呼ぶだけです。

話を bpy.context, bpy.data, bpy.scene に戻しましょう。bpy.data は C 側では Main (注:型名です) に相当し、bpy.context.blend_data で取得できます。つまり名前が "Context" の StructRNA を探し、その中から名前が "blend_data" のプロパティを探し、最初に取得した bContext を self にしてプロパティを取得すればいいわけです。同様に bpy.scene も bpy.context.scene で取得できます。これで必要な処理を実装する準備が整いました。

一点、python バインディングが用意されてない関数を呼びたいケースがたまにあります。Blender のソースは多数のモジュールに分離されているため、必要なモジュールだけプラグインにリンクすればいいんじゃないかと考えたんですが、これは断念しました。多くのモジュールが他のモジュールへ依存関係を持っており、単純な関数一つ呼びたいだけでも大量のモジュールをリンクする必要が出てきて収集がつかなくなります。結局、単純な関数に限りそのソースをコピペして持ってくる、というのが一番現実的だという結論に至りました。

Blender の Outliner に表示されるオブジェクトは、大体 C 側では Object 型のインスタンスです。Object はトランスフォームを持ち、Object::data に Camera や Mesh などタイプ別データへのポインタが入っています。data のタイプは Object::type で判別でき、OB_MESH などの enum が定義されています。Unity で例えると Object が GameObject 相当品、Object::data が Component 相当品、というようなイメージです。
以降はこのタイプ別データを取得する手順になります。

Transform & Animation

トランスフォーム (TRS) を取得しようと思ったとき、DCC ツールは大抵個別の要素 (位置, 回転, スケール) を取得する API とグローバル行列を取得する API を用意しています。Blender もこの両方があります。(Object.matrix_world, Object.location, 等など) 取得したい情報はオブジェクトのローカルな位置や回転なので、個別要素の方を取得したくなります、が、これは泥沼へ続く道です。

Blender には delta と呼ばれる概念があります。delta は仮想的な親オブジェクトとして機能し、Global TRS = Local TRS * Delta TRS * Parent TRS となっています。個別要素を取得したい場合でもこの delta を考慮しなければなりません。加えて回転は色んなモードが用意されており (Eular XYZ, XZY ..., Axis Angle, Quaternion)、モードに応じた取得処理を書く必要があります。
話が Blender から逸れますが、Maya の場合、delta はありませんが代わりに offset, pivot, joint orientation といったまた独自の要素が絡んできます。単純に位置を取りたいだけでもこれら全てを考慮しなければ正しい結果は得られません。要するに、個別要素の取得は DCC ツール固有のトランスフォーム処理を正確に理解しておく必要があり、見た目よりもずっと難度が高いです。

一方、グローバル行列から取得する場合は話は単純です。グローバル行列は delta や constraints などが全て適用された結果であるため、DCC ツール固有処理について気にする必要はほぼありません (座標系や単位の違いくらい)。Blender でも Maya でも 3ds Max でもほぼ同じ処理で TRS を取得することができます。MeshSync で採用したのもこちらです。
今回欲しいのはオブジェクトのローカル TRS であるため、親を持つ場合はその逆行列を掛けてローカル化しています。行列からの TRS の取得の仕方は、ぐぐれば簡単に見つけられますが、MeshSync ではこんな処理になっています。

template<class T> inline tvec3<T> extract_position(const tmat4x4<T>& m)
{
    return (const tvec3<T>&)m[3];
}

template<class TMat>
inline tquat<typename TMat::scalar_t> extract_rotation_impl(const TMat& m_)
{
    using T = typename TMat::scalar_t;
    tmat3x3<T> m{
        normalize((tvec3<T>&)m_[0]),
        normalize((tvec3<T>&)m_[1]),
        normalize((tvec3<T>&)m_[2])
    };
    {
        auto s = extract_scale_sign(m_);
        m[0] *= s;
        m[1] *= s;
        m[2] *= s;
    }

    tquat<T> q;
    T tr, s;

    tr = T(0.25) * (T(1) + m[0][0] + m[1][1] + m[2][2]);

    if (tr > T(1e-4f)) {
        s = sqrt(tr);
        q[3] = (float)s;
        s = T(1) / (T(4) * s);
        q[0] = (m[1][2] - m[2][1]) * s;
        q[1] = (m[2][0] - m[0][2]) * s;
        q[2] = (m[0][1] - m[1][0]) * s;
    }
    else {
        if (m[0][0] > m[1][1] && m[0][0] > m[2][2]) {
            s = T(2) * sqrt(T(1) + m[0][0] - m[1][1] - m[2][2]);
            q[0] = T(0.25) * s;

            s = T(1) / s;
            q[3] = (m[1][2] - m[2][1]) * s;
            q[1] = (m[1][0] + m[0][1]) * s;
            q[2] = (m[2][0] + m[0][2]) * s;
        }
        else if (m[1][1] > m[2][2]) {
            s = T(2) * sqrt(T(1) + m[1][1] - m[0][0] - m[2][2]);
            q[1] = T(0.25) * s;

            s = T(1) / s;
            q[3] = (m[2][0] - m[0][2]) * s;
            q[0] = (m[1][0] + m[0][1]) * s;
            q[2] = (m[2][1] + m[1][2]) * s;
        }
        else {
            s = T(2) * sqrt(T(1) + m[2][2] - m[0][0] - m[1][1]);
            q[2] = T(0.25) * s;

            s = T(1) / s;
            q[3] = (m[0][1] - m[1][0]) * s;
            q[0] = (m[2][0] + m[0][2]) * s;
            q[1] = (m[2][1] + m[1][2]) * s;
        }
    }
    return normalize(q);
}

template<class TMat>
inline typename TMat::scalar_t extract_scale_sign(const TMat& m)
{
    using T = typename TMat::scalar_t;
    auto ax = (const tvec3<T>&)m[0];
    auto ay = (const tvec3<T>&)m[1];
    auto az = (const tvec3<T>&)m[2];
    return sign(dot(cross(ax, ay), az));
}
template<class TMat>
inline tvec3<typename TMat::scalar_t> extract_scale_signed_impl(const TMat& m)
{
    using T = typename TMat::scalar_t;
    auto s = extract_scale_sign(m);
    return tvec3<T>{ length(m[0]) * s, length(m[1]) * s, length(m[2]) * s };
}

ただ、グローバル行列を使う場合にも問題があって、スケールの符号が一部失われてしまいます。 XYZ 全て正、もしくは全て負の場合は正しく判別できるのですが、X だけ負のような場合にこれを復元する方法がなく、全て負と誤認されてしまいます。これは対応を諦めてそういう仕様としました。

アニメーションにも同じことが当てはまります。アニメーションを取得する場合、F-Curve を巡回して位置や回転を算出するのが正道に思えますが、これだと delta や constraints を自力でハンドリングしないといけないため、非常に難しいです。
F-Curve 巡回よりも、一定間隔で時間を進めつつ、各オブジェクトのグローバル行列から TRS を取得していく、いわゆるベイクの方が簡単かつ確実と言えます。MeshSync が採用したのもベイク方式です。

Camera & Light

これらは特に難しいことはありません。強いて注意点を挙げると、Camera の vertical fov は angle_y プロパティであること、Unity 互換にするには 90 度 X 軸回転を入れる必要があることくらいでしょうか。

Mesh

今回の主目的。python 側でも C 側でも型名は Mesh ですが、以下基本的に C 側の Mesh の話になります。
インデックスは Mesh::mloop (要素数は totloop) から取得できます。Blender では per-index のデータは "Loop" と呼ばれているようです。ポリゴン情報は Mesh::mpoly (要素数は totpoly) から取得できます。アサインされているマテリアルもここから取得できます。基本的な頂点情報は Mesh::mvert (要素数は totvert) から取得できます。

Mesh::mvert には法線も入っていますが、これは per-vertex の法線です。欲しいのは普通 per-index の法線のはずです。per-index の法線は Mesh::ldata に含まれます。ldata はレイヤーと呼ばれる追加の頂点データ郡を格納する場所になっており、UV や頂点カラーもここに格納されています。汎用的な分取得の手順も複雑で、レイヤーの種類と数からインデックスを得てデータポインタを得るという流れになります。(
BMesh::normals(), CustomData_get())
per-index の法線は未構築であったり現在の Mesh の状態と同期していないことがあります。python に公開されている Mesh.calc_normals_split() を呼ぶことによりこれを最新の状態に更新できます。この関数は既に最新の状態であれば何もしないので、毎回呼んでもパフォーマンス的な問題はないはずです。
頂点データは単なる生の配列なので並列処理が可能ですが、calc_normals_split() はマルチスレッド非対応のようです。

UV は python の UVLoopLayers という型 (StructRNA) の active プロパティから取得できます。UVLoopLayers は実体を持たないインターフェースのような型で、self として指定するのは Mesh 自身になります。UV は per-index です。頂点カラーは python の LoopColors という型の active プロパティから取得できます。処理内容は UV と同じです。頂点カラーも per-index です。(この二つは法線同様 Mesh::ldata に格納されているデータであり、ldata 経由でもアクセスできます)

Blendshape は Mesh::key->block に格納されています。最初の KeyBlock は基準 (basis) となる頂点で、以降は basis との差分と weight 値から移動量を求めます。

Skinning

Blender のスキニング処理は特殊で、知っていないととても苦労すると思います。
重要なポイントは、Bone は特殊な座標系で扱われているということです。以後この特殊な座標系を "Armature 座標系" と呼びます。
Blender の座標系は Z-up ですが、Armature 座標系は Y-up かつ Z が反転しています。つまり Armature 座標系と World 座標系を変換するには以下の行列を掛ける必要があります。

Armature to World

  1, 0, 0, 0,
  0, 0,-1, 0,
  0, 1, 0, 0,
  0, 0, 0, 1

World to Armature

  1, 0, 0, 0,
  0, 0, 1, 0,
  0,-1, 0, 0,
  0, 0, 0, 1

Blender で Outliner に表示されるオブジェクトのほとんどは Object のインスタンスですが、Bone は例外的に Object ではありません。Armature の中のみに存在できる特殊な型となっています。にもかかわらず、Object は Bone を親に持てます。つまり親が Object の時と Bone の時で別処理になります。このことは TRS 取得処理にも関わってきます。(ちなみに、Object は Bone のみならず特定の頂点を親にすることもできます。今回の私のケースではさすがに対応を諦めました…)

Object には bDeformGroup のリストである Object::defbase というデータがあり、bDeformGroup::name が影響を受ける Bone を示しています。頂点の Bone との関連付けは Mesh::dvert からアクセスできる MDeformVert から取得できます。

EditMesh

「頂点一つ動かすたびにそれをリアルタイムで同期する」が MeshSync の基本理念なので、編集中の Mesh も対応する必要がありました。
編集中の Mesh とは Blender 上で Edit モードの時の Mesh で、通常の Mesh とは全く違う扱いになっています。この編集中の Mesh、python からアクセスする手段がたぶん存在しません。つまりこの辺の処理はプラグインからのアクセスを考えていない無法地帯となっています。

Blender は、Mesh を編集する際、BMEditMesh という編集用のデータを用意します。例えば Cube を作成して選択し、Edit モードに入ったという状況を考えてみましょう。この場合、Edit モードに入った瞬間に元 Mesh から編集用の BMEditMesh オブジェクトが作成され、編集はそちらに適用されていきます。ビューポートに描画されるのも編集中は BMEditMesh です。Edit モードを抜けると BMEditMesh の内容が Mesh に適用され、BMEditMesh は破棄されます。つまり、Mesh の内容は編集中もずっと編集開始前のままです。編集中のモデルデータを取得したい場合、BMEditMesh の方のデータを見なければなりません。

BMEditMesh は Mesh::edit_btmesh で取得できます。編集中か否かは単純に Mesh::edit_btmesh が null かどうかで判別できます。Mesh::edit_btmesh->bm->vtable から頂点、Mesh::edit_btmesh->looptris からインデックスを取得できます。BMEditMesh は全てのポリゴンが三角形になっています。
法線は都度計算する必要があります。(BM_loop_calc_face_normal) UV は複雑で、Mesh::edit_btmesh->bm->ldata.layers[CD_MLOOPUV].offset で UV データへのオフセットが得られ、BMLoop::head.data + offset が UV データの位置になります。(BMLoop は Mesh::edit_btmesh->looptris の型です)

注意すべき点として、インデックスは未構築状態なことがあり、このときエクスポートしようとするとクラッシュを招きます。未構築のときは Mesh::edit_btmesh->bm->elem_table_dirty が真になっているので、一旦諦めて次の update を待ちましょう。再構築はビューポートに BMEditMesh を描画する処理の中で行われるため、有効なデータはすぐに来ます。

モディファイアの Bake

python メソッドの Object.to_mesh() を呼ぶと、Mesh を新規に作成してモディファイアを bake した結果を格納して返します。また、NURBS や 3D Font などの Mesh に変換可能なオブジェクトを Mesh 化することもできます。
to_mesh() で返ってきたオブジェクトは BlendDataMeshes.remove() を呼ばないと開放されない点に注意が必要です。怠るとすごい勢いでメモリを食いつぶすでしょう。

Blender 2.80

現在 beta の 2.80 シリーズですが、ソースコードレベルでも大きな変更が加わっています。が、幸い上記の Transform/Camera/Light/Mesh/BMesh 取得はほぼ変更なしでそのまま動きました。
変更が激しいのは私が把握している範囲で、SceneGraph 関連 (Scene::master_collection という Collection でオブジェクトが管理されるようになった)、Material (もはや原型を留めていない)、UI 関連などです。2.80 は現在進行系であちこち変更されているので、今詳しく触れるのは避けます。

一点、プラグイン開発者には暴挙と言うべき重大な変更があって、scene_update コールバック、および Object の is_updated が削除されています。
https://developer.blender.org/T47811
しょうがないので MeshSync は 2.80 ではタイマーを用いて定期的に全オブジェクトを巡回して 1 つ前のデータと比較、自力でデータ更新チェックを行っています。つまり、2.79 版とは比べ物にならないくらい遅いです。beta が取れるまでに scene_update が復活することを願います。

Conclusion

解説しきれていないことも多数あるのですが、力尽きた&重要な部分は大体書いたと思うので今回はここで切り上げます。
見ての通り C/C++Blenderプラグインを書くのは茨の道です。エンジニアの視点ですが、Blender が Maya 等と比べて致命的に弱い点がここだと感じます。しかも 2.80 ではここを改悪する方向に変更が加えられつつあります。とはいえ、今回の live link であったり、複雑な constraint や simulation を実装するような場合、労力を割いてでも C/C++ で書く価値はあるのではないかと思います。
近頃 Blender を採用するスタジオが増えてきているように見受けられますが、アーティスト以上に TA/エンジニアの方々が苦労されることになるのではないかと予想されます。関係者の方々の健闘を祈ります。

余談

MeshSync は現在 Blender の他に Maya, Maya LT, 3ds Max, Motion Builder, Metasequoia などもサポートしています。これらのツールから頂点等を抽出する処理を書きたい場合、MeshSync のソースコードが参考になるかもしれません。気が向いたら他のツールの解説も今後書こうと思います。
github.com

publications (2016 - 2017)

最近の活動記録とか。

CEDEC 2017 で、TokyoDemoFest チームとして講演しました。intro でよく用いられる Distance Function & Raymarching (Sphere Tracing とも呼ばれる) をがんばってゲーム制作でも使えるようにしてみようという内容です。 Distance Function の話はもはや珍しいものではなくなっていますが、ゲームに使うことを真面目に検証した話は珍しいのではないかと思います。

CEDEC 2017 「デモシーンへようこそ - 4KBで映像作品を作る技術、およびゲーム開発への応用」
docs.google.com


CEDEC 2016 でも TokyoDemoFest として講演を持ちました。こちらはデモシーンを知らない人向けに紹介するのを目的としたもので、デモシーンの歴史などの解説と、4k intro (容量 4KB の exe による映像作品) の作り方の大雑把な解説などを行っています。

CEDEC 2016 「デモシーンへようこそ - 4KBで映像作品をつくる技術」
https://onedrive.live.com/view.aspx?resid=61562C8AE88D716B!4355


あとは 2015-2016 年に The Gift という短編映像作品に関わっていて、それに関する講演を MARZA の方々と行いました。私は Alembic の再生と、最後の方のボールの海のレンダリングを主に担当しました。
www.youtube.com

CEDEC 2016 「MAKING OF "THE GIFT" ~ Unityを用いた高品質映像制作」
cedil.cesa.or.jp


The Gift の件以降、仕事は映像関係がメインになっていて、USD や FBX のインポート&エクスポート、各種 DCC ツールとの連携 (プラグイン制作)、Unity 上でのデータの編集機能、などの実装を日々行っています。これらは大体 github でソースごと公開しているので、ソースが何らかの参考になることもあるかもしれません。
https://github.com/unity3d-jp/MeshSync
https://github.com/unity3d-jp/NormalPainter
https://github.com/unity3d-jp/FbxExporter
https://github.com/unity3d-jp/USDForUnity

alembic for realtime rendering


ここ数ヶ月仕事で Alembic を扱ってきたので、Alembic に関してここまで得られた知見を書き残しておこうと思います。
想定しているシチュエーションは主に、Alembic のライブラリを用いて alembic ファイル (.abc) を読み込み、そのデータを D3D11 などのグラフィックス API に流し込んで描画する、というようなものです。
Alembic はゲームに使うのは難しいと思われますが、例えば VR コンテンツには使い出がありますし、近年のゲームエンジンを使った映像制作でも重要な役割を担う可能性があります。また、Alembic は書き込みも簡単にできるため、プレイ中のゲームのシーンを Alembic で 3D 録画し、それを映像制作などに利用、といった応用も考えられます。

Alembic 概要

http://www.alembic.io/

過去にも軽く触れましたが、Alembic とは主に映像業界で使われているデータフォーマットです。ポリゴンメッシュなどを全フレームベイクして格納するのに用いられます。映像業界ではスキニングや物理シムは全てベイクして Alembic に格納し、レンダラやコンポジットソフトに渡す、といった使い方がなされます。
全フレームベイクして格納するわけですから、当然ながらゲーム屋からするとデータ量は桁外れにでかいものとなります。

Alembic はいろんなデータを時系列に沿って格納できるようになっています。データの各要素は property と呼ばれ、格納されているデータ群は sample と呼ばれます。ポリゴンモデルなどの具体的なオブジェクトは schema と呼ばれるオブジェクトに格納されています。この schema はいろんな property の集合体になっています。
例えばポリゴンモデルは PolyMesh という schema に格納されており、この PolyMesh は position、uv、index などの各種 property で構成されています。そして、時間 (またはインデックス) を指定すると、Alembic のライブラリはその時間/インデックスに対応する sample (PolyMesh の場合モデルデータ) を返す、といった具合に機能します。ちなみに Alembic は補間は行いません。

Alembic 導入

いきなりですが、自分的にこの導入が Alembic を使うにあたっての最大の難関だと思っています。
Alembic はバイナリの配布がされておらず、使うには自力でビルドする必要があります (関連する HDF や ILMBase も含めて)。
ビルドシステムが CMake なので、CMake で VisualStudio のプロジェクトを生成してビルドするだけ…、といきたいところですが、残念ながらそう簡単にはいきません。映像業界では Linux がデファクトスタンダードだそうで、映像関係のオープンソースソフトウェアは Windows はまともにサポートされてないことが多いです。Alembic も素直にはビルドできず、CMakeList とソース両方にいくつか手を加えてビルドを通るようにする必要がありました。
その詳細は面倒なので省略しますが、ここらへんにヘッダファイルと VisualStudio2015 用のビルド済みバイナリを置いています (容量削減のため .lib ファイル郡は .7z に圧縮済み)。とりあえず手を付けてみたいという方はこれの使用をおすすめします。

入門用の簡単なサンプルプログラムが欲しいのであれば、Alembic のソースに含まれるテストコードが参考になるでしょう。(これ とか同ディレクトリにある他のテストプログラム) 意外と簡単そうだというのが見て取れると思います。
より実践的な例が欲しいのであれば、拙作 AlembicImporter のソースが参考になるかもしれません。下の Schema の項目で触れる問題点に一通り対処してあります。Unity のプラグインとして使う前提で作ってはいますが、このプラグイン自身は Unity には非依存です。

Alembic の archive には HDF と Ogawa の 2 種類がありますが、HDF はレガシーなフォーマットです。自分で .abc を生成する場合は Ogawa を選んだほうがいいでしょう。こちらの方が速くて小さいことが多く、最近の DCC ツールでも大体 Ogawa がデフォルトになっています。
また、Alembic はエラーは例外を投げて通知してくることが多いです。ゲーム畑の人は面食らうかもしれません。私は面食らいました。

Schema

先に触れたように、ポリゴンモデルなどの具体的なデータは schema というオブジェクトが保持します。ここではいくつかの schema について使用の際の注意点などを挙げます。Xform, Camera, Points, PolyMesh の 4 種です。
schema は他にも何種類かありますが、Light はそれ自身にはライトに関連するパラメータは入っていないし、Material は抽象的すぎて対応が困難な上 DCC ツール側もろくに対応していない有様で、それ以外は Subdivision や NURBS といったもので今回は保留です。Material に関してはインポートの後独自対応が必要になりますが、現状映像業界でもそんな感じなようです。

  • Xform

Xform とは位置、回転、スケール などを表すもので、ゲームでは同等品は Transform と呼ばれることが多いと思います。Alembic では 1 つのオブジェクトが複数の schema を持つことはできないため、ほとんどのケースでは Xform の子として他の schema がぶら下がる形になっています。
例:

root - Xform - Camera
             |- Xform - PolyMesh

この Xform、Transform 相当品と書くと何ら難しいことはなさそうに見えますが、内部処理を理解していないとハマる可能性があります。
Xform は座標やスケール値などを直接保持するのではなく、各オペレーションおよびその順番を保持しています。オペレーションは 移動、任意軸回転、X/Y/Z 軸回転、スケールなどの種類があり、これらを任意の数、任意の順番で保持できます。そして、XformSample::getMatrix() を呼ぶとこのオペレーションを全て実行して結果を行列で返します。いろんな DCC ツールに対応するための苦慮が伺える実装です…。

問題は回転を取得したい場合です。XformSample には getAxis(), getAngle() というのが用意されているのでそれで簡単に取れるように見えますが、罠があります。getAxis() getAngle() は、getMatrix() で全オペレーションを実行して行列を算出した後、その行列からクオータニオンを抽出し、axis/angle に変換して返します。これは遅いだけでなく、回転とスケールが同時にかかっている場合に元と違う結果になってしまいます。正確な回転を取得したい場合、自力で全オペレーションを巡回して回転オペレーションだけを見て算出しなければなりません。
XformSample::getScale() も全く同じ問題を含んでおり、これも getMatrix() の結果からスケール値を抽出しようとするので、正確な値を取得したければ自力で全オペレーションを巡回して算出する必要があります。

  • Camera (ICamera, OCamera)

カメラの情報が入った schema です。含まれる情報は実にプリレンダ向けといった趣きで、焦点距離、撮影距離、口径、シャッター開始時間/終了時間 などなど、そういう感じです。
幸い、CameraSample には getNearClippingPlane(), getFarClippingPlane(), getFieldOfView() といったリアルタイムでも馴染み深いのもちゃんとあります。ただし、getFieldOfView() は横方向の FoV を返してきます。リアルタイムの場合必要なのは縦方向のそれです。残念ながらこれは自力で算出する必要があります。算出の処理はこうなります。

    verticalFoV: 2.0 * RadiansToDegrees( atan( verticalAperture * 10.0 / (2.0 * focalLength)));

ちなみにこれは getFieldOfView() の実装をコピペ改変しただけです。CameraSample は FoV そのものは保持しておらず、aperture と focal length から算出するようになっています。このため、逆に Alembic へカメラ情報をエクスポートしたい場合、FoV から aperture か focal length を算出する必要があります。AlembicImporter では aperture を固定して focal length を算出するようにしています。focal length 算出は以下のような処理になります。

    focalLength:  (aperture * 10.0) / tan(DegreesToRadians(fieldOfView / 2.0)) / 2.0;

本格的なレンズエフェクトを使う場合、その他の詳細なカメラの情報も必要になると思われますが、その際は単位に注意が必要です。focus distance や aperture は cm、focal length は mm といった具合に、メートルじゃない上に統一されていません。

  • PolyMesh (IPolyMesh, OPolyMesh)

ポリゴンメッシュの schema です。多くの場合これをレンダリングするのが Alembic をインポートする主目的になるでしょう。そして、やはりこやつもプリレンダ向けなデータ構造をしており、リアルタイム用途では扱いづらいものとなっています。障害になるのは主に次の 2 点だと思われます。

任意の n 角形を保持できる。混在もできる
3 角形だったり 4 角形だったり、100 角形である可能性すらなきにしもあらずで、それらが 1 つのモデルの中に混在しています。
何角形かという情報は IPolyMeshSchema::Sample::getFaceCounts() で取れるコンテナに保持されており、これが例えば [4,3,5] だった場合、index[0-3] が最初の 4 角系、index[4-6] が次の 3 角系、index[7-11] が最後の 5 角系、といった具合です。レンダリングの際は 3 角形化などの中間処理を入れることになるでしょう。

位置、法線、UV、それぞれ個別にインデックスを持てる
位置、法線、UV の要素数がそれぞれ異なる可能性があり、それぞれが個別にインデックスを持っている可能性があります。(インデックスの要素数は全て一致)
モデリングツールにおけるポリゴンの扱いを考えると、なるほど、という感じですが、レンダリングする側としては厄介です。インデックスが独立している場合、インデックスを展開するか、D3D11 世代以降の機能を利用して独自に頂点&インデックスバッファを用意して複数インデックスに対応させる必要があります。(インデックス展開 = index の要素数分の各要素の配列を用意して、for(int i=0; i<num_indices; ++i) { new_pos[i]=pos[index[i]]; } みたいに展開する)
前者はメモリ使用量が膨れ上がる上にたぶん速度も後者より劣りますが、既存のシェーダをそのまま使えます。一方後者は実行効率はいいものの頂点シェーダで特殊なことをやる必要があり、専用のシェーダを整備する必要が生じます。AlembicImporter では Unity 側で一般的なシェーダでレンダリングできるようにする必要があったため、インデックスを展開する方法を採っています。

他には、トポロジが変化する可能性があることもリアルタイム用途としてはやや特殊です。
IPolyMeshSchema::getTopologyVariance() でトポロジがどう変化するかという情報が取れます。これが kConstantTopology であれば頂点の位置もインデックスも一切変化しない static な mesh であり、kHomogenousTopology であれば頂点の位置は変化するもののインデックスは変化しない mesh であり、kHeterogenousTopology であれば頂点の位置もインデックスも変化する mesh です。
kHeterogenousTopology が一番取り扱いが面倒です。インデックスの内容だけでなく、インデックスや頂点の要素数も変化する可能性があります。事前に全サンプルを巡回して最大数を算出し、その分のバッファを割り当てておく、などの対応が必要になるかもしれません。

  • Points (IPoints, OPoints)

パーティクルなどを格納する schema です。sample が持つデータは位置、速度、ID、バウンディングボリュームだけで、リアルタイム用途でもほぼ中間処理なしで使えるでしょう。
ID はパーティクルの数が増減するときの識別用に使います。例えば位置や色をランダムにずらしたいような場合、パーティクルのインデックス (0-n) を乱数のシードにすると、パーティクルの増減に伴ってランダム値も変わってしまいます。こういう場合 ID を乱数のシードにすることで、パーティクルが増減してもランダム値に影響は出ないように改善できます。

Export

インポートができたらならエクスポートも簡単です。schema オブジェクトを作り、sample を内容を埋めて set するだけです。ただ、いくつか注意が必要な点もあります。

読み込み用のオブジェクト群と書き込み用のオブジェクト群ではなぜか class の関係が若干違っています。
読み込みの場合、IObject と schema class 群 (IXform など) は継承関係がなく、IObject が shcema を保持する形になっています。一方、OObject と schema class 群 (OXform など) は継承関係になっています。なので、読み込み用 class 群だけを見た場合 1 つのオブジェクトが複数の schema を持てるように見えますが、実際にはそういうケースはないと見ていいようです。

Alembic はデフォルトでは 1 sample / seconds (= 1 FPS) で記録するようになっています。これを変えるには TimeSampling を作り、各 schema にその TimeSampling のインデックスを与える必要があります。
TimeSampling とは、サンプルがどういう周期で記録されているかという情報です。これには 3 種類のモードがあります。

Uniform: 時間間隔が均一なモードです。開始時間とインターバルだけが記録され、n 番目のサンプルの時間は 開始時間 + インターバル * n になります。
Acyclic: 時間間隔が不均一なモードです。この場合全てのサンプルの時間がそのまま配列として記録されます。サンプル間には delta time が負になってはいけない以外に規則性はありません。
Cyclic: 時間間隔が不均一ながら周期的なモードです。インターバルとサンプル (配列) が記録されます。 例えばインターバルが 1.0、サンプルが [0.0, 0.1, 0.3] であれば、0.0, 0.1, 0.3, 1.0, 1.1, 1.3, ... という時間分布になります。

多くの場合は Uniform で事足りると思われますが、例えばゲームを 3D 録画したい場合は Acyclic にするとゲームへの影響を最小限にできるでしょう。
Cyclic は主にプリレンダにおけるモーションブラーのためのモードだそうで、シャッターオープン開始、シャッターフルオープン、シャッタークローズ開始、シャッターフルクローズ、のサイクルを繰り返し記録したいような時使うそうです。リアルタイムからは最も縁遠いモードと言えそうです。
TimeSampling は混在もでき、schema 単位はもちろん、property 単位で個別に設定できます。

object space raymarching

ProceduralModeling

夏コミ版 exception reboot で用いた、オブジェクトスペースでレイマーチする手法について解説してみます。(ここで言うレイマーチは厳密には sphere tracing のことですが、面倒なのでレイマーチで統一します)
アイデア自体は特に新しくも難しくもなく、シーン内に単純なポリゴンモデルを配置し、そのモデルの中でレイマーチを行うというものです。レイマーチにより G-Buffer を生成し、あとは通常通りライティングを行います。

レイマーチについては過去にこの blog でも簡単な入門を書きました。最近では日本語でも結構情報が出てくるくらい知名度が上がってきているように見受けられます。
raymarching for games - primitive: blog
レイマーチで G-Buffer を生成する手法もしばらく前にこの blog で紹介しました。
rendering fractals in Unity5 - primitive: blog

実装の詳細の大部分は既にこれらの記事中に書かれており、本記事ではそれらとかぶってる部分は省略します。また、成果物はこちらになります。上の画像のシーンがそのまま含まれています。(ProceduralModeling.unity)
https://github.com/i-saint/Unity5Effects
ちなみに作例は Unity で作られていますが、レンダリング方式が deferred shading かつ自作シェーダを動かせる環境であればどこでもこの手法は使えるはずです。

レイマーチで distance function を描く場合、fullscreen quad で画面全体を描くことが多いですが、それを小分けにしてオブジェクトスペースでやろうというのが今回の主題です。これにより、レイマーチの欠点である柔軟性のなさと計算量の多さの緩和を狙います。
レイマーチでは、シェーダに式を書くことで図形を表現するため、図形の変更はつまりシェーダソースの変更になります。外部パラメータ化によりある程度変更可能にはできますが、例えば特定の位置に図形を追加、みたいな変更に耐える柔軟性を持たせるのは結構大変です。今回のオブジェクトスペースのやり方であれば、オブジェクトの配置や位置の変更はゲームエンジンのエディタ上でオブジェクトを追加したり TRS をいじることで簡単に実現できます。
画面全体をレイマーチする場合、レイの開始点は常にカメラ位置 (もしくは view plane) であり、図形に到達するまでに相応のステップ数が必要になります。一方オブジェクトスペースの場合、レイの開始点がそれなりに目的の図形の近くであることが期待でき、ステップ数削減が見込めます。

以下実装の手順。
まず、描画に使うポリゴンモデルは立方体か球だと都合がいいです。これらの場合ピクセルシェーダ内の単純な計算でレイがオブジェクトの内側にいるかが判定できます。もっと複雑なモデルでも適用は可能ですが、その場合内外判定に裏面の depth の情報が必要になります (最大マーチ距離などで適当に近似するのもアリではありますが) 。内外判定を怠ると オブジェクトの裏面=無限遠 と扱われることになり、シーンによっては顕著にヘンな結果になります。

シェーダ内の処理。頂点シェーダの出力に、頂点のワールド座標と法線を追加します。このワールド座標 (=ポリゴン表面位置) をレイマーチの開始位置とするわけです。
ピクセルシェーダでレイマーチを行いますが、この中でレイをオブジェクトのローカル座標系に変換する処理を挟みます。具体的には、レイをオブジェクトの TRS の逆行列で変換し、scale だけ掛けなおします (TR の逆行列でもいいと思われますが。Unity の場合 TRS の逆行列はエンジン側が提供しているのでこうしました)。レイの方向は normalize(頂点のワールド座標-カメラ位置) で算出できます。
注意すべき点として、レイの開始点が既に図形の内側だった場合の対処が必要です。これを怠ると法線が反転した面が出てきてライティングがおかしくなります。対処法としては、レイマーチの結果得られた距離が負 (=レイは図形の内側) である場合、元ポリゴンモデルの法線を出力する、というものになります。

あとは通常通り distance function を捏ねていい感じに見える形状を構築します。distance function 以外は定型的な処理なので、distance function だけ書いてパラメータを設定すればいい感じにレンダリングできるようなフレームワークを作るといいでしょう。私の場合この 2 つがフレームワークになっています:ProceduralModeling.cginc Framework.cginc

レイがオブジェクトの外に行ってしまったら discard、とすることで、元ポリゴンモデルと輪郭が異なる形状を表現できます。(前述の 立方体か球だと都合がいい のはこの内外判定が容易なため)
SV_Depth を用いてレイの到達点の depth を出力すれば、影や立体交差も正しく表現できるようになり、SSAO などもいい感じに載るようになります。ただし、early Z culling が効かなくなるため顕著に重くなります。また、元ポリゴンモデルとの形状がかけ離れるほど、正しく形状を表現するのに多くのステップ数が必要になり、重くなります。ここらへんはバランスを考える必要がありそうです。
https://raw.githubusercontent.com/i-saint/Blog/master/ObjectSpaceRaymarching/depth.jpg
例:元ポリゴンモデルとの形状の差が激しい状況。左は元ポリゴンモデルの depth をそのまま出力したもの。右は SV_Depth でレイの到達位置を出力したもの。見ての通り左は交差部分がおかしいとか色々エラーがあります。が、右よりだいぶん速いです。

作例:
https://raw.githubusercontent.com/i-saint/Blog/master/ObjectSpaceRaymarching/cube.jpg
レイマーチを試したことがある方なら一目で式が分かると思います。使い古された形状ですが、画面密度を増やすには割と効果的なんじゃないかと。経過時間で縦方向の座標を動かせばそれっぽくアニメーションになります。

https://raw.githubusercontent.com/i-saint/Blog/master/ObjectSpaceRaymarching/hex.gif
みんな大好き六角形。歯抜けにしたりランダム高低差をつけたりすると見た目の華やかさが上がります。六角形の中心が元図形の範囲外だったら描画しない、とすることで、元ポリゴンモデルをスケールするだけで六角模様が追随するようになります (=元モデルの境界で六角形がぶった切られない)。

https://raw.githubusercontent.com/i-saint/Unity5Effects/master/doc/Metaball.gif
メタボール。これはレイマーチだとすっごく単純な式で実現できます (実装のコア部分)。ただし、これだとボールの数に比例してすごい勢いで重くなっていきます。数百や数千を想定する場合もっと高次のアルゴリズムを考える必要がありそうです。


冒頭の画像のシーンは、これらにポストエフェクトやパーティクルを盛りまくることでできあがりました。

Unity ちゃん以外の背景オブジェクトは全て立方体をピクセルシェーダで加工したものです。
下図はシェーダによる差を示したもので、両方とも同じシーンで左はシェーダを通常の Standard Shader に差し替えたものになります。
https://raw.githubusercontent.com/i-saint/Blog/master/ObjectSpaceRaymarching/before.pnghttps://raw.githubusercontent.com/i-saint/Blog/master/ObjectSpaceRaymarching/after.png

ここまでやってみた実感として、レンダリング結果や編集のしやすさは良好なんですが、この方法でも図形が複雑になると重くて、もう一工夫入れないとちょっと実用に耐えられない印象です。上記画像のシーンは六角模様の床が明らかに危険域の重さになっています…。
いくつか最適化のネタはあるものの、まだ検証中の段階でそちらの詳細はまた別の機会に書く予定です。まあ adaptive sub-sampling、temporal、screen space normals、early-Z culling モドキ など、ネタとしてはごくありふれたものになりそうです。
(ちなみに夏コミ版 exception reboot には時間がなくて一つも入れられませんでした…。つまり最終的に今よりは描画負荷は軽くなる見込みです)


今後ハードウェアの進化に伴ってこういうポリゴン以外の 3D 形状の表現がだんだん市民権を得ていくんじゃないかと思います。
ただ、今回みたいなプロシージャル的なネタはなかなか一般化が難しく、タイトル固有の実装にならざるを得ないところが多いです。逆に言うと、ゲームエンジンが当たり前に使える時代にプログラマーが最も輝けるのがこういう領域なのかもしれません。

before C88

C88 で頒布する exception reboot 開発途中版の PV です。R23b でお待ちしております。
今回も体験版です…。前回同様 100 円でソースコード (Unity プロジェクト一式) 同梱です。カワノさんによる新曲も追加されています。
残念ながらゲーム内容は冬コミ版とほとんど変わっていません。しかしグラフィックは大きく変わりました。見ての通りだいぶん派手になっています。相応に重くもなっていますが、もう低スペックは切り捨てて全力で自分の目指す絵を追求する方向に舵を切りました。ただ、冬コミ版は非 D3D11 環境では色がおかしかったりパーティクルが出なかったりしたのが、今回は一応一通り機能するようになっています。


Unity 5 移行に伴い、レンダリングに絡む処理は全て書き直しています。このせいでえらい手間取ってしまいましたが、標準レンダリングパイプラインで描画するように変更したことで、他のプロジェクトへ簡単にアセットを流用できるようになりました。労力に見合う成果は出せたんじゃないかと思います。(冬コミ版の時はレンダリングパイプラインを全て独自に実装してオレオレ deferred shading で描いていたので、他プロジェクトへの流用は困難でした)
レンダリング関連のソースはほとんどこちらで公開しているので、ソースが目的であればこちらで事足りると思います。
https://github.com/i-saint/Unity5Effects

今回の絵作りの肝となっているのが、オブジェクトスペースでレイマーチするという小技。要するに立方体をピクセルシェーダで加工して複雑な図形に見せかけるというものですが、思ってた以上にいい結果が得られたので、後日詳細を書いてみようと思います。
exception reboot

playing with Unity 5's deferred shading pipeline

WaterSurface2
Unity4 の時に書いた自作 deferred shading 用の機能を Unity5 用に再実装しています。その経過の記録です。成果物はこちら https://github.com/i-saint/Unity5Effects

Screen Space Boolean

https://raw.githubusercontent.com/i-saint/Unity5Effects/master/doc/Boolean.gifhttps://raw.githubusercontent.com/i-saint/Unity5Effects/master/doc/BooleanDesc.gif
ステンシルや ZTest Greater の組み合わせでスクリーンスペースでブーリアン演算をやるというトリック。以前解説したものですが、Unity5 でこれをやるのは苦労しました。
実装の方針として、ブーリアン演算の段階では depth だけを出力し、実際に G-Buffer を書き出す段階では ZTest Equal を使用するように変更を加えた Standard Shader を用います。これは後述のステンシルの制限の回避や、必要なシェーダのバリエーションを最小限にするためです。代償として drawcall の数が増えています。

まずステンシル。deferred の G-Buffer 生成からライティングの段階では、カメラのデフォルトのレンダーターゲットに対するステンシルの書き込み指定は無視されるようです。(おそらく Unity が内部的にライトとオブジェクトの組み合わせをステンシルで実現しているため) これをなんとかするため、レンダーターゲットを別に用意してそこでブーリアン演算を行い、depth を元のレンダーターゲットにマージしています。

ZTest Equal を使う場合注意が必要で、Unity で SV_Depth で depth を出力する場合、デフォルトの depth と出力を一致させることはたぶんできないようです。そんなはずはないと思いたいのですが、方法を見つけられませんでした。(D3D11 に限れば SV_POSITION の z を使えば一致しましたが、他プラットフォームでは使えません) このせいでずいぶん悩むことになりました。
ブーリアンの減算は、貫通していたら depth を初期化する処理が必要になります。これをストレートに実装すると

if(減算する側のモデルの depth > 減算される側のモデルの裏面の depth) {
    output_depth = 1.0; // 貫通しているので depth 初期化
}
else {
    output_depth = 減算する側のモデルの depth;
}

こんな感じになります。しかし、depth を自力で算出して出力すると精度がズレるようで、その後 ZTest Equal で同モデルを書くと細かい穴があいてしまいました。しょうがないので貫通処理は 2 パスに分けることで対処しました。最初のパスではごく普通に depth を出力。2 パス目では

if(減算する側のモデルの depth > 減算される側のモデルの裏面の depth) {
    output_depth = 1.0;
}
else {
    discard;
}

とすることで、貫通箇所以外は SV_Depth を経由しないようにします。

あとは影。これは対処不能という結論に至りました。
Unity の影は描画順がランダムのようで、こういう 不透明だけど描画順に依存する 代物はまともな方法では対処できないようです。CommandBuffer にも影バッファ生成の前後に差し込めるイベントはなく、たぶんライティング処理を全部自力で書いて影バッファ生成をフルコントロールしないと実現不可能だと思われます。

Screen Space Shadow

https://raw.githubusercontent.com/i-saint/Unity5Effects/master/doc/ScreenSpaceShadows.gif
スクリーンスペース全方位影。これを実現するにはライティング処理を自力で書く必要がありますが、幸いそこはいい公式サンプルがあるのでごっそりコピペしました。あとはごく普通にライトの中心からピクセル位置までレイマーチして途中で遮られたら光量を減らすだけです。

一つ問題になったのが、カメラが HDR ではないときへの対処。これは上記公式サンプルでも未対処でした。
HDR が有効なときとそうでないときでは内部処理が変わります。HDR が有効な場合、G-Buffer の emission buffer にはカメラのデフォルトのターゲットが使われており、HDR 無効の場合専用のバッファが用意されます。よって、レンダーターゲットを自力で G-Buffer に設定したいような場合、以下のような場合分けが必要になります。

Camera cam = GetComponent<Camera>();
CommandBuffer cb = new CommandBuffer();
cb.SetRenderTarget(new RenderTargetIdentifier[] {
    BuiltinRenderTextureType.GBuffer0,
    BuiltinRenderTextureType.GBuffer1,
    BuiltinRenderTextureType.GBuffer2,
    cam.hdr ? BuiltinRenderTextureType.CameraTarget : BuiltinRenderTextureType.GBuffer3
}, BuiltinRenderTextureType.CameraTarget);

また、HDR が無効な場合、emission は logarithmic encoding という方法でエンコードされた状態で保持されます。これはより精度を保つためのエンコード方法だそうで、色を exp2(-color) で加工した状態で保持し、後で -log2() で復元する、というものです。(詳細)
この影響で、HDR の有無でブレンド方法とピクセルシェーダの出力を変える必要があります。HDR 無効な場合のブレンド方法は Blend DstColor Zero、有効な場合 Blen One One です。Properties で外部から変えられるようにした方がいいでしょう。ピクセルシェーダには以下のような処理を加えます。

half4 frag(v2f I]N) : SV_Target
{
    half4 color;
    // ...
#ifndef UNITY_HDR_ON
    color = exp2(-color);
#endif
    return color;
}
#pragma multi_compile ___ UNITY_HDR_ON

まあしかし、古いモバイルデバイスを視野に入れない限りは "強制的に HDR をオンにする" が一番簡単な対処法であると思われます。

あと、今回はやっていませんが、こういう放射状のレイマーチには epipolar sampling という強力な最適化方法があるのを最近知りました。
アルゴリズムの詳細はこの記事の 3. Epipolar sampling 以降で詳しく解説されています。こちらの Unity 用 Light shaft はこのテクニックを用いてるそうです。いずれ試してみたいところです。

Screen Space Reflections

https://raw.githubusercontent.com/i-saint/Unity5Effects/master/doc/ScreenSpaceReflections.jpg
以前実装の詳細を書いたやつです。幸いほぼそのまま移植できました。
障害になったのが、depth からピクセルの位置を算出する方法。ライティングの処理の中に同等処理があるのですが、なんだかよくわからない難解な処理になってる上、ポストエフェクトの場合これは機能しないように見えます。
結局ストレートに view projection の逆行列をシェーダに渡して対処しました。 projection 行列は Unity が内部的に加工を施すので、逆行列を求める際にも同じことをやる必要があります。

// C# 側処理
var cam = GetComponent<Camera>();
Matrix4x4 view = cam.worldToCameraMatrix;
Matrix4x4 proj = cam.projectionMatrix;
// Unity が内部でやってる projection 行列の加工
proj[2, 0] = proj[2, 0] * 0.5f + proj[3, 0] * 0.5f;
proj[2, 1] = proj[2, 1] * 0.5f + proj[3, 1] * 0.5f;
proj[2, 2] = proj[2, 2] * 0.5f + proj[3, 2] * 0.5f;
proj[2, 3] = proj[2, 3] * 0.5f + proj[3, 3] * 0.5f;
Matrix4x4 inv_view_proj = (proj * view).inverse;
Shader.SetGlobalMatrix("_InvViewProj", inv_view_proj);
// shader 側処理
sampler2D_float _CameraDepthTexture;
float4x4 _InvViewProj;

float4 GetPosition(float2 uv)
{
    float2 screen_position = uv * 2.0 - 1.0;
    float depth = tex2D(_CameraDepthTexture, uv).x;
    float4 pos4 = mul(_InvViewProj, float4(screen_position, depth, 1.0));
    return pos4 / pos4.w;
}

また、D3D9 ではなぜか _CameraDepthTexture はバイリニアフィルタがかかっているようで、そのままだとなんかヘンな結果になります。ポイントフィルタに切り替えたいところですが、まともな方法ではできそうにないので、ピクセルの中心をサンプリングすることで対処しました。(_ScreenParams.zw-1.0)*0.5 がピクセルサイズの半分になるのでこれを利用しています。

ちなみに Screen Space Reflection は近い先に標準搭載される予定です。この分野でたぶん世界一詳しい人が実装を担当しているのでそれを待っていたのですが、当初の予定から伸びに伸びている上、近々に必要になったので結局自作のを移植しました。
Roadmap によると 5.2 (2015/09/08 リリース) にリストされていますが、delayed になっています…。

Rim Light

https://raw.githubusercontent.com/i-saint/Unity5Effects/master/doc/RimLight.jpg
法線と カメラ -> ピクセル位置 の角度が浅いところを明るくするアレです。全く根拠レスな処理なのになんかカッコよく見えるようになります。
前述のピクセル位置の復元さえできれば特に難しいところはないですが、今更ながらフレネル反射の式 (正確にはそれの簡易版) を今回導入してみました。

// ...
float3 N = tex2D(_CameraGBufferTexture2, uv) * 2.0 - 1.0;
float3 I = normalize(pixel_pos.xyz - _WorldSpaceCameraPos.xyz);
float fresnel = saturate(_FresnelBias + pow(dot(I, N) + 1.0, _FresnelPow) * _FresnelScale);

いい感じになった気がしますが、最大のメリットはパラメータの調整がアーティストフレンドリーになることでしょうか。

Water Surface & Caustics Field

https://raw.githubusercontent.com/i-saint/Unity5Effects/master/doc/WaterSurface.gif
Tokyo Demo Fest 2015 用に作った demo の水面のリファイン版。3D ノイズとレイマーチでいい感じに見せかけるストレートな実装です。
こちらもピクセル位置の復元以外 Unity5 固有の難しい問題はありませんでした。フラクタル図形や Rim Light と組み合わせると実にいい感じになります。


そんなわけで、予想より大分苦労しましたが、主要な機能は大体移植できました。既存プロジェクトへの組み込みが簡単になったのが大きな成果です。

あと、夏コミ参加します。場所は 日曜日 R 23b です。
今回も exception reboot の製作途中版でソース同梱の予定です。Unity 5 移行に伴い、レンダリング部分を全面的に書きなおしています。結果としてゲーム内容は前回からあまり変わらないものになりそうです…。

recent works

最近作ったもの紹介。全て Unity から使う前提で作ってはいますが、BatchRenderer 以外は Unity 依存度は低く、一応他のプログラムから使うこともできるはずです。また、全て MIT ライセンスか CC BY ライセンスなので、ソースレベルで切り貼りして使うのも問題ないようになっています。

PatchLibrary

https://github.com/i-saint/PatchLibrary
実行中のプログラムにロードされている dll を強引に更新するツールです。プラグイン開発などの際、VisualStudio ビルド後イベントでこれを呼び、実行中のホストプログラムを再起動することなく更新する、というような使い方を想定しています。
以前作った DynamicPatcher の簡易版的なもので、dllexport されている関数のテーブルを書き換えて新しい dll の関数にリダイレクトさせることで更新を実現しています。このため、dll で作成したコンテキストを保持しているような場合に途中でこれで更新すると死ぬなどの問題がありますが、その点を理解して使えばそれなりに有用です。以降のツール群はこれ使いながら開発しています。

FrameCapturer

https://github.com/unity3d-jp/FrameCapturer
https://raw.githubusercontent.com/unity3d-jp/FrameCapturer/master/Screenshots/gif_capturer.gif

ゲーム画面をキャプチャして gif アニメや exr などに出力する代物です。後述の TweetMedia を同梱しており、画面のキャプチャから Twitter への投稿までを全てゲーム内からできるようになっています。
開発ブランチでは mp4 キャプチャも実装中で、まだ若干動作が怪しいものの、キャプチャから Twitter への投稿まで機能するようになっています。(Twitter は web からは動画は投稿できないものの、API レベルでは mp4 を受け付けるようになっています) こちらも近い先ちゃんと公開する予定です。

TweetMedia

https://github.com/unity3d-jp/TweetMedia
前述の通り、Twitter にメッセージと画像/mp4 動画を投稿できるようにする代物です。
既にいい実装があればそれを使いたかったんですが、画像/動画まで対応したのは見つけられなかったので仕方なく自作しました。(と言っても大部分は twitcurl を流用していますが)


BatchRenderer

https://github.com/i-saint/BatchRenderer
https://raw.githubusercontent.com/i-saint/BatchRenderer/master/Screenshots/procedural_gbuffer.gif
こちらは単なる機能追加ですが。Unity 5.1 で CommandBuffer.DrawProcedural() が加わり、やっと Unity の描画パイプラインからインスタンシング描画をできるようになりました。これを使って DrawProcedural() で G-Buffer を生成する機能を追加した、というものです。
本物のインスタンシング描画なので当然ながら何十万インスタンスであろうと 1 drawcall で描けはするのですが、残念ながら速度面では擬似インスタンシングとあまり変わらないようです。DrawProcedural() はその不思議な仕様上、頂点データへのアクセスに間接参照が必要になるため、理想的なケースに比べると一歩パフォーマンスが落ちる感触です。それでも実用に足る速さは出てるとは思うんですが、surface shader が使えなくなるデメリットを覆せるほどのメリットが擬似インスタンシングに対してあるかと問われると、現状返答に詰まる感じです…。
スクリーンショットは SD Unity ちゃんを 2000 体 (約 16,000,000 triangles) 出してみたもので、このくらいがうちの GeForce GTX 970 で 60 FPS 出せるギリギリのラインになっています。

AlembicImporter

https://github.com/unity3d-jp/AlembicImporter
https://raw.githubusercontent.com/unity3d-jp/AlembicImporter/master/Screenshots/alembic_example.gif

Unity 上で Alembic ファイルを再生する代物です。
Alembic とは映像業界でよく使われているファイル、およびそのライブラリで、シーン上のオブジェクトをフレーム毎に全て頂点キャッシュへ bake して格納するのに使われます。映像業界ではこれを用いてシミュレーション結果を bake してレンダラなどに渡すような使い方をするそうです。



当分仕事が映像関係になる予定なので、今後しばらくこの blog の内容もそれっぽいものになるかもしれません。AlembicImporter はまさに映像関係者向けですし、FrameCapturer も G-Buffer を exr でエクスポートしたいという要望を受けたのが始まりでした。映像方面はゲーム業界とは近いようで全く違う文化圏を持っていて、おもしろい発見が多いです。