SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

てづくりVAT

少し前にSPARKGEARのVertex Animation Texture機能のデバッグをしていたプログラマはいったい誰でしょう。私です!

こんにちはクライアントエンジニアの中島です。 そんなわけで唐突ですが Vertex Animation Texture (VAT) の話を少し書いてみたいと思います。

VATとは

そもそもVATとは何かといえば、いまでは使えないGPUのほうが珍しいVertex Texture Fetch (VTF)という、頂点シェーダーでテクスチャを使える機能を活用した面白テクニックの1つです。

複雑な物理シミュレーションの結果など、リアルタイムに処理するのは難しいものをあらかじめテクスチャに書き込んでおいて、それをポリゴンモデルに反映してレンダリングするものです。 アニメーションするポリゴンモデルを全フレーム分個別に用意するジオメトリキャッシュなどと呼ばれるものの一種で、大量のデータを扱うためにシェーダーで使える最も大きいメモリ領域といえるテクスチャを使うという、なかなかワイルドな代物です。

同種のテクニックには水面や雪原の凹凸を書き込んだテクスチャをリアルタイムに更新し続けて、頂点シェーダーで高さを変動させるようなものもあります。 しかしVATは動画のように同じ動きしか再生できません。 かわりにボーンやモーフではつらくなるような複雑な変形でも軽い負荷で再現できる、というのが良いところでしょうか。

VATを使う

VATを使うにはVAT用のデータを書き込んだテクスチャが必要になります。 UE4には3Ds MAX用のツールが用意されているようですが、いまではHoudiniで書き出すのがデファクトスタンダードなようです。

Houdini 18では『SideFX Labs』というシェルフに、『VAT』というそのままのノードが用意されています。

SideFX Labs www.sidefx.com

SideFX Labs インストール www.sidefx.com

SideFX Labs のリポジトリ github.com

これは各ゲームエンジン用の座標軸の設定などもできるかなり多機能なものです。 またGitHubリポジトリにはUE4とUnityのシェーダーのサンプルもあります。

このノードの使い方についてはチュートリアルがあります。

GAME TOOLS | 頂点アニメーションテクスチャ (VAT) www.sidefx.com

また各ゲームエンジン向けにも参考になりそうな動画もあります。

Unity Japan 「何でも出せる万能エクスポーター VAT で Houdini の可能性が100億倍広がる - Unity道場2020 2月」 www.youtube.com

インディゾーンHoudini情報日本語ブログ 「Labs Vertex Animation Textures ROP / 作成したVATファイルをUE4で読み込む方法」 houdinifx.jp

VATのデータ

SideFX LabsのVATノードからは4種類のVATのデータを書き出せます。

  • Soft Body
  • Rigid Body
  • Fluid
  • Sprite

それぞれについて少し説明を書きます。

共通事項

その前にすべてに共通していることをまとめます。

Positionマップに記録されている値は、取りうる座標値の最大、最小の成分の値を使って正規化されています。 画像として記録するには0から1の範囲に収めたほうが都合が良いためです。 VATを使うためのシェーダーには Bounding Max と Bounding Min という2つの数値のパラメーターがあり、それを使って元の値を復元します。 ここに入れるべき値はHoudiniから書き出すときにJSONの形でまとめて出てくるようになっています。

それから格納された要素はテクスチャ画像の横方向、xyでいえばx、uvでいえばuの方向に並んでいきます。 また書き出す画像の最大の幅の設定があり、1フレーム分の要素の量が最大幅より多くなった場合には次のy(またはv)の行に続きます。 例えば横幅8、要素数12のVATを書き出すと下のように並びます

 u00u01u02u03u04u05u06u07
frame 00P00P01P02P03P04P05P06P07
P08P09P10P11P12---
frame 01P00P01P02P03P04P05P06P07
P08P09P10P11P12---
frame 02P00P01P02P03P04P05P06P07
P08P09P10P11P12---

したがって出力画像のサイズは下のようになります。

出力画像の幅   = min(指定の最大幅, 要素の数)
出力画像の高さ = frame数 * ceil(要素数 / 出力画像の幅)

そして Geometryとして書き出されるモデルのuv2(Fluidではuv1)には最初のフレームの要素のuv座標が入っています。 この表でいう frame 00 の P00 などのテクセルの位置です。

共通点はこのくらいです。 それでは個別にみていきます。

Soft Body

布のような柔らかいもののシミュレーションの結果を書き出します。 もうすこし具体的に言うと、ジオメトリの構成が変化しないもの です。 ジオメトリの構成が変化しない、とはポリゴンの数や、面や辺の並び方が変わらないということです。

書き出されるデータで必須のものは2つです。

  • Geometry:原型のポリゴンモデル。uv2にVAT用のテクセル位置が記録されている。
  • Position:原型からの変化量が記録されたテクスチャ用の画像ファイル。

ジオメトリの構成が変化しない前提なので、Positionマップには変化量だけが記録されています。

Rigid Body

岩が割れたり建物が崩れたりするいわゆる剛体シミュレーション用です。 剛体も ジオメトリの構成が変化しないもの です。 複数の剛体のシミュレーション結果を1つの画像ファイルに書き込みます。 1つの剛体は重心の位置と回転量で姿勢をあらわせるので、剛体1つ分のデータが1ピクセルになります。

書き出されるデータで必須のものは3つです。

  • Geometry:初期状態のポリゴンモデル。uv2にVAT用のテクセル位置、頂点カラーに重心の初期位置が記録されている。
  • Position:シミュレーションの結果の剛体の重心位置が記録されたテクスチャ用の画像ファイル。
  • Rotation:シミュレーションの結果の剛体の回転量が記録されたテクスチャ用の画像ファイル。

Rotationマップは四元数で記録されています。 また、Geometryの頂点カラーの重心座標は専用の正規化係数を持つようになっています。 以前はPositionマップと共通だったようですが精度の改善のために分離したようです。 最終的な頂点の座標はこのGeometryの頂点カラーの重心の初期位置と、Positionマップの各フレームの重心位置を使って求めます。

Fluid

流体シミュレーションに代表される、 ジオメトリの構成が変化するもの を記録するための形式です。

書き出されるデータで必須のものは2つです。

  • Geometry:最大限必要なポリゴン数のポリゴンモデル。uv1にVAT用のテクセル位置が記録されている。
  • Position:各フレームの座標が記録されたテクスチャ用の画像ファイル。

Geometryはただの素材なので、最大限必要なポリゴンの数の三角形が適当に入っているだけのモデルです。 FluidだけVAT用のテクセル位置が uv1 に入っているのでご注意ください。 またPositionマップには各フレームでの頂点の位置がそのまま入っています。

なおこの形式では三角形が各フレームでどの位置に使われるか不定なため、他の形式では可能なフレーム間の補間ができません。 そのため再生するフレームレートに合わせて適当なフレーム数を書き出す必要があります。 とはいえSideFX Labsにあるサンプルのシェーダーでは他の形式でも補間はしていないようですが…

Sprite

パーティクルのシミュレーション結果をビルボードのスプライトとして表示するための形式です。

書き出されるデータで必須のものは2つです。

  • Geometry:最大限必要なポリゴン数のポリゴンモデル。uv2にVAT用のテクセル位置が記録されている。
  • Position:各フレームのパーティクルの座標が記録されたテクスチャ用の画像ファイル。

このGeometryは1パーティクルあたり三角形2個分のポリゴンが適当に入っているモデルです。 Positionマップには各フレームでのパーティクルの位置がそのまま入っています。 またPositionマップのアルファにはパーティクルのスケールを格納できるようですが、SideFX Labsのサンプルのシェーダーでは対応していないように見えます。

ライセンスはお持ちですか

さて、HoudiniでVATを作る方法を見てきましたが、データを書き出すにはIndie以上のライセンスが必要です。 一応Apperenticeでも書き出せるようですが、ウォーターマークが入ってしまうため動作がおかしくなるそうです。

というよりもプログラマがテスト用のデータとして欲しいのは、むしろとてもシンプルなものだったりします。

しかしここまで仕様がわかっていれば作ることはそれほど難しくありません。 まずは一番簡単なSpriteのVATのデータを作ってみましょう。

てづくりVAT

まず1つのパーティクルが(-1, 0, 0)から(1, 0, 0)まで動くものをつくります。

Position

Positionマップは前述の通り正規化したものになります。

移動する範囲が(-1, 0, 0)から(1, 0, 0)なので、取りうる値の最小値は-1、最大値は1です。 これらの値を使って(-1, 0, 0)と(1, 0, 0)を正規化すると、(0, 0.5, 0.5)と(1, 0.5, 0.5)になります。 したがって画像はRが0.0から1.0へ変化し、GとBは0.5の固定値にすれば良いということになります。

フレーム数は適当に決めればよいので9にしました。 なぜ9かといえば値の切りがよくなるからです。

ペイントソフトでポチポチと色を置いてできた画像は幅1pixel、高さ9pixelです。

f:id:spark-nakajima-satoru:20210326114855p:plain

小さくてわかりにくいので表で再現するとこのような感じです。

#007f7f
#1f7f7f
#3f7f7f
#5f7f7f
#7f7f7f
#9f7f7f
#bf7f7f
#df7f7f
#ff7f7f

HoudiniはOpenEXRで出力してくれるのですが、この程度のデータでは意味が無いのでPNGで保存しました。

Geometry

GeometryのためのモデルはBlenderやMayaといったモデリングツールで作るのが一番簡単です。 とはいえSpriteでは三角形の数がスプライトの数と合っていれば良いだけなので、もう少し手軽に作りたいところです。 しかしuv2にVATのuv座標を入れなければいけないため、複数のuvを格納できるファイル形式でなければいけません。 そうなるとFBXかColladaくらいしか選択肢がないのですが、FBXは手で書くのは難しいためColladaにしました。 Unityでしか読めませんが仕方がありません。 本当はUE4とUnityに対応したかったところですが。

というわけで四角形1つのColladaです。

<?xml version="1.0" encoding="utf-8"?>
<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <asset>
    <unit name="meter" meter="1"/>
    <up_axis>Y_UP</up_axis>
  </asset>
  
  <library_geometries>
    <geometry id="vat-geometry" name="vatgeom">
      <mesh>

        <!-- 頂点データ
        スプライトは4つの頂点をもつ四角形
        最適化で融合されてしまったりしないよう重ならないように適当に配置
         -->
        <source id="vat-positions">
          <float_array id="vat-positions-array" count="12">
            -1 -1 0
             1 -1 0
             1  1 0
            -1  1 0
          </float_array>
          <technique_common>
            <accessor source="#vat-positions-array" count="4" stride="3">
              <param name="X" type="float"/>
              <param name="Y" type="float"/>
              <param name="Z" type="float"/>
            </accessor>
          </technique_common>
        </source>
        
        <!-- UV1:スプライトごとのUV
        スプライトの各頂点に割り当てたUV
         -->
        <source id="vat-uvs1">
          <float_array id="vat-uvs1-array" count="8">
            0 0
            1 0
            1 1
            0 1
          </float_array>
          <technique_common>
            <accessor source="#vat-uvs1-array" count="4" stride="2">
              <param name="S" type="float"/>
              <param name="T" type="float"/>
            </accessor>
          </technique_common>
        </source>
        
        <!-- UV2:スプライトごとのVATテクスチャの最初のフレームのテクセルの位置
        基本的な算出法
        スプライトに0から順に割り当てた番号をIDとするとN個のスプライトのIDは0からN-1になる
        ("スプライトのID"であって、"頂点のID"ではないので注意)
        またVATテクスチャのサイズが幅W、高さHとすると
        u = ((ID % W) + 0.5) / W
        v = 1.0 - (floor(ID / W) + 0.5) / H
        になる
         -->
        <source id="vat-uvs2">
          <float_array id="vat-uvs2-array" count="8">
            0.5 0.944444
            0.5 0.944444
            0.5 0.944444
            0.5 0.944444
          </float_array>
          <technique_common>
            <accessor source="#vat-uvs2-array" count="4" stride="2">
              <param name="S" type="float"/>
              <param name="T" type="float"/>
            </accessor>
          </technique_common>
        </source>

        <!-- 頂点データの指定 -->
        <vertices id="vat-vertices">
          <input semantic="POSITION" source="#vat-positions"/>
        </vertices>

        <!-- 三角形のリスト
        スプライトは四角形なので1つあたり三角形2つ
         -->
        <triangles count="2">
          <input semantic="VERTEX" source="#vat-vertices" offset="0"/>
          <input semantic="TEXCOORD" source="#vat-uvs1" offset="0" set="0"/>
          <input semantic="TEXCOORD" source="#vat-uvs2" offset="0" set="1"/>
          <p>
            0 1 2
            2 3 0
          </p>
        </triangles>
      </mesh>
    </geometry>
  </library_geometries>

  <library_visual_scenes>
    <visual_scene id="vat-scene" name="vatscene">
      <node id="vat-mesh" name="vatmesh" type="NODE">
        <matrix sid="transform">
          1 0 0 0
          0 1 0 0
          0 0 1 0
          0 0 0 1
        </matrix>
        <instance_geometry url="#vat-geometry" name="vatmeshgeom"/>
      </node>
    </visual_scene>
  </library_visual_scenes>

  <scene>
    <instance_visual_scene url="#vat-scene"/>
  </scene>
</COLLADA>

Unityでセットアップ

できたデータをUnityでセットアップします。 テクスチャのインポート設定は少し注意が必要です。

  1. まずsRGBのチェックを外します。カラー以外のパラメーターをテクスチャで使う場合のお約束です。
  2. 次に2のN乗にリサイズされないように設定します。AdvancedにあるNon-Power of 2をNoneにしましょう。同時にMax Sizeも十分に大きなものにしましょう。VATのテクスチャはサイズも重要な要素なので勝手にリサイズされては困ります。
  3. それからVATのテクスチャでは近傍を補間した値には意味が無いので、補間されないような設定にしておきます。まずはミップマップは無駄なのでAdvancedにあるGenerate Mipmapsをオフにします。さらに下のほうにあるFilter ModeをPointにして最も近いテクセル値だけが取得されるように設定しましょう。
  4. 最後に画像の圧縮を無効にしておきます。必須でもないのですが、非可逆な圧縮がかかると動きが壊れる可能性があります。

f:id:spark-nakajima-satoru:20210325175241p:plain

それからSideFX LabsのサンプルのシェーダーをUnityで読み込むとコンパイルエラーが出ます。 とはいえ行末に『;』が無いというちょっとしたタイプミスなので簡単に直せます。 とはいうもののそもそもコンパイルエラーになる箇所の texturePos.xyz = pow(texturePos.xyz, 2.2);PNGで読み込んでいるためか不要なようなので、コメントアウトしてしまっても良いように思えます。

あとはGeometryのメッシュのマテリアルのシェーダーを sidefx/vertex_sprite_shader にしてPositionマップに先ほどの画像を設定します。 そして Bounding Max1.0 に、Bounding Min-1.0 に設定すると、1つのビルボードが(-1,0,0)から(1,0,0)の範囲を動きはじめます。

f:id:spark-nakajima-satoru:20210324204559g:plain

できました!

ちなみにMaxを5、Minを-3などとすればその範囲で動きます。 yとzの位置もずれてしまいますが。

そしてここまでできれば3つのパーティクルがx、y、z軸に沿って動くものなども同様につくれます。 スプライトの色もColorマップを用意すれば簡単につけられます。

f:id:spark-nakajima-satoru:20210324204635g:plain

おまけ

さて、このあたりでそういえば自分はプログラマだったと思い出しました。

そうなるとちょっとした計算値を入れて出力するスクリプトなどを作ってみたくなってきます。 そこでプレビューもなしにJavascriptをevalするだけのスパルタンなものをご用意しました。

Sprite VAT Generator

ちなみに特に何も出ずに処理が終わるまでブロックされるので、フレーム数やスプライト数を大きくしてSaveボタンを押したときは気長にお待ちください。

とはいえお約束な感じでローレンツアトラクターを入れてみるとそれっぽく動くものが出てきました。

f:id:spark-nakajima-satoru:20210326103814g:plain

下のようなスクリプトでスプライトは10000個、フレーム数は300の設定です。

// position generator of lorenz atractor
VAT.positionGenerator.init = function() {
    let tmpp = new Array(VAT.numSprites);
    for(let i = 0; i < VAT.numSprites; i++) {
        let p = new VAT.Point();
        p.x = (Math.random() * 2.0 - 1.0) * 0.04;
        p.y = (Math.random() * 2.0 - 1.0) * 0.04;
        p.z = (Math.random() * 2.0 - 1.0) * 0.04;
        tmpp[i] = p;
    }
    VAT.positionGenerator.temporal = { tempp: tmpp };
}
VAT.positionGenerator.compute = function(spriteID, frame) {
    let tmpp = VAT.positionGenerator.temporal.tempp[spriteID];
    const p = new VAT.Point(tmpp.x, tmpp.y, tmpp.z);
    const dt = 1.0 / 1000.0;
    for(let i = 0; i < 60; i++) {
        const x = tmpp.x;
        const y = tmpp.y;
        const z = tmpp.z;
        tmpp.x = x + (10.0 * (-x + y)) * dt;
        tmpp.y = y + (-x * z + 28.0 * x - y) * dt;
        tmpp.z = z + (x * y - 8.0 / 3.0 * z) * dt;
    }
    return p;
};
// color generator example
VAT.colorGenerator.compute = function(spriteID, frame) {
    let c = new VAT.Color(1.0, 1.0, 1.0, 1.0);
    const t = frame / (VAT.numFrames - 1);
    const s = spriteID / (VAT.numSprites - 1);
    const tr = t * (Math.PI * 2.0 * (1.0 + s * 3.8));
    const tg = t * (Math.PI * 3.0 * (1.0 + s * 5.7));
    const tb = t * (Math.PI * 5.0 * (1.0 + s * 7.6));
    c.r = Math.cos(tr) * 0.3 + 0.7;
    c.g = Math.cos(tg) * 0.3 + 0.7;
    c.b = Math.cos(tb) * 0.3 + 0.7;
    c.a = 1.0;
    return c;
};

ちょっと面白くなってきましたが今回はこのあたりで。

ついでなので上のジェネレーターのGitHubにこの記事で使ったUnityプロジェクトなども上げておきました。

それではまた次回お会いしましょう。