e.blog

主にUnity/UE周りのことについてまとめていきます

ジオメトリシェーダ入門

概要

今回はジオメトリシェーダ入門という形で記事を書いていきます。
頂点シェーダ、フラグメントシェーダに比べるとあまり書く機会の少ないシェーダ。
その2シェーダに比べて違いが結構あるのでそのあたりについてのメモを書いていこうと思います。

実際に使った例はこんな感じ↓
f:id:edo_m18:20180711140346p:plain

ジオメトリシェーダとは

Wikipediaによると以下のように説明されています。

ジオメトリシェーダー
ジオメトリシェーダー(英: Geometry Shader, GS)はピクセルシェーダーに渡されるオブジェクト内の頂点の集合を加工するために使用される。ジオメトリシェーダーにより、実行時に頂点数を増減させたり、プリミティブの種類を変更したりすることが可能となる。OpenGLではプリミティブシェーダーとも呼ばれる。

ジオメトリシェーダーはポイント、ライン、トライアングルといった既存のプリミティブから新しいプリミティブを生成できる。

ジオメトリシェーダーは頂点シェーダーの後に実行され、プリミティブ全体または隣接したプリミティブの情報を持つプリミティブを入力する。例えばトライアングルを処理するとき、3つの頂点がジオメトリシェーダーの入力となる。ジオメトリシェーダーはラスタライズされるプリミティブを出力でき、そのフラグメントは最終的にピクセルシェーダーに渡される。またプリミティブを出力せずにキャンセルすることもできる。

ジオメトリシェーダーのよくある使い方としては、ポイントスプライトの生成、ジオメトリテセレーション、シャドウボリュームの切り出し、キューブマップあるいはテクスチャ配列へのシングルパスレンダリングなどがある。

ざっくりと要約すると、頂点単位ではなく、ポリゴン単位などで値を計算し、さらにはポリゴン枚数すら増やすことができるシェーダ、という感じでしょうか。

テッセレーションは分かりやすい使用例ですね。
その他の使い方としては、頂点郡からポリゴン(Quad)を生成しテクスチャを貼ることでパーティクルのような演出をする、なども使い道がありそうです。

まさにその発想で解説してくれている記事があるので紹介↓

wordpress.notargs.com

コードを見てみる

さて、ジオメトリシェーダのシンプルなコードを見てみましょう。
下の例ではジオメトリシェーダではある意味で「なにもしていません」。
つまり、ポリゴンは変形しません。

が、ジオメトリシェーダがなにをしてくれているのかを見るのにちょうどいいと思います。

appdata vert(appdata v)
{
    // ジオメトリシェーダで処理するため、頂点シェーダへの入力をそのまま返す
    return v;
}

// ジオメトリシェーダでは3頂点のinputと3頂点のoutputを行う=つまり普通の三角ポリゴン
// max vertex count 3が3頂点のoutputであることを伝えている
// また、triangle appdata input[3]が、「三角形」で3頂点のinputが必要であることを伝えている
[maxvertexcount(3)]
void geom(triangle appdata input[3], inout TriangleStream<vsout> outStream)
{
    [unroll]
    for (int i = 0; i < 3; i++)
    {
        // 頂点シェーダからもらった3頂点それぞれを射影変換して通常のレンダリングと同様にポリゴン位置を決める
        appdata v = input[i];
        vsout o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        outStream.Append(o);
    }

    outStream.RestartStrip();
}

fixed4 frag(vsout i) : SV_Target
{
    return float4(1, 0, 0, 1);
}

頂点シェーダやフラグメントシェーダに比べるとやや特殊な感じになっています。
特に、関数に対してアトリビュートが指定されています。
細かな点についてはコメントで記載しました。

MSDNのドキュメント(ジオメトリ シェーダー オブジェクト (DirectX HLSL).aspx))を見ると以下のように書かれています。

ドキュメント

Syntax

[maxvertexcount(NumVerts)]
void ShaderName (
  PrimitiveType DataType Name [ NumElements ],
  inout StreamOutputObject
);

パラメーター

  • [maxvertexcount(NumVerts)]
    • [in] 作成する頂点の最大数の宣言です。
      • [maxvertexcount()] - 必須のキーワードです。正しい構文とするために、山かっこと丸かっこを必ず記述します。
    • NumVerts - 頂点の数を表す整数です。
  • ShaderName
    • [in] ジオメトリ シェーダー関数の一意の名前を含む ASCII 文字列。
  • PrimitiveType DataType Name [ NumElements ]
    • PrimitiveType - プリミティブ型。プリミティブ データの順序を決定します。
    • DataType - [in] 入力データ型。任意の HLSL データ型を指定できます。
    • Name - ASCII 文字列の引数名。引数が配列の場合は、NumElements で配列サイズを任意に指定できます。
プリミティブの種類 説明
point ポイント リスト
line ライン リストまたはライン ストリップ
triangle トライアングル リストまたはトライアングル ストリップ
lineadj 隣接性のあるライン リストまたは隣接性のあるライン ストリップ
triangleadj 隣接性のあるトライアングル リストまたは隣接性のあるトライアングル ストリップ

ストリーム出力オブジェクト

ここで、「ストリーム出力オブジェクト」は出力されるオブジェクトの種類に応じて指定するものが変わります。(例えばポリゴンとして出力するのか、ラインとして出力するのか、など)

ドキュメントから引用すると、以下の3つが定義されているようです。

Syntax
inout StreamOutputObject<DataType> Name;
パラメーター
  • StreamOutputObject Name
    • ストリーム出力オブジェクト (SO) 宣言
ストリーム出力オブジェクトの型 説明
PointStream ポイント プリミティブのシーケンス
LineStream ライン プリミティブのシーケンス
TriangleStream トライアングル プリミティブのシーケンス

違ったコードを見てみる

次に、ちょっとだけ違ったコードを見てみます。
これはポリゴン単位で法線を計算している例です。

通常の頂点シェーダでは頂点ごとの計算しかできないため、実際に生成されたポリゴンに対しての処理は書けません。
頂点だけではポリゴンを形成する「隣接する」他の頂点情報がないために計算が行えないためですね。

しかしジオメトリシェーダでは「ポリゴン単位」で処理が行えるため、こうしたことが可能になっています。

[maxvertexcount(3)]
void geom(triangle appdata input[3], inout TriangleStream<vsout> outStream)
{
    float3 edge1 = (input[1].vertex.xyz - input[0].vertex.xyz);
    float3 edge2 = (input[2].vertex.xyz - input[0].vertex.xyz);
    float3 normal = normalize(cross(edge1, edge2));

    [unroll]
    for (int i = 0; i < 3; i++)
    {
        appdata v = input[i];

        vsout o;

        o.pos = UnityObjectToClipPos(v.vertex);
        o.normal = normal;
        outStream.Append(o);
    }

    outStream.RestartStrip();
}

上記の例では、ポリゴンのエッジから法線を計算しています。
こんな感じで「ポリゴン単位」で計算が行えるのが特徴です。

また上記で紹介した記事のように、inputの数とouputの数を変えることができるので、1枚のポリゴンから数枚のポリゴン、というようにポリゴン数を増やすことも可能です。

実例を見てみる

さて最後に、実際に利用されているシェーダを覗いてみて終わりにします。
以下の例は、凹みさんが書かれている「凹みTips」の記事(HoloLens で使える Near Clip 表現について解説してみた)で紹介されているものです。
本人の承諾をいただいて解説させていただいています。

※ 以下のコードは凹みさんが公開しているものを若干変更したものになります。

凹みさんが作った実際のものを動かすと以下のような感じになります。
(凹みさんの画像を引用させていただきました)

ポリゴン分解の動作イメージ

ジオメトリシェーダの部分だけ抜粋しました↓
ポイントを絞ってコード内にコメントを記載しています。

// 前のサンプル同様、3頂点のinput / outputが指定されています。
[maxvertexcount(3)]
void geom(triangle appdata_t input[3], inout TriangleStream<g2f> stream)
{
    // ポリゴンの中心を計算。
    // ポリゴン単位で計算を行えるため、「ポリゴンの中心位置」も計算可能です。
    float3 center = (input[0].vertex + input[1].vertex + input[2].vertex) / 3;

    // ポリゴンの辺ベクトルを計算し、ポリゴンの法線を計算する。
    // 続いて、前のサンプルでもあった「ポリゴン法線」の計算です。
    float3 vec1 = input[1].vertex - input[0].vertex;
    float3 vec2 = input[2].vertex - input[0].vertex;
    float3 normal = normalize(cross(vec1, vec2));

    #ifdef _METHOD_PROPERTY
    // カメラ視点を利用しない場合はパラメータをそのまま利用する
    fixed destruction = _Destruction;
    #else
    // カメラ視点からの距離によって分解する場合は、中心位置をワールド座標に変換し、
    // カメラとの距離を計算して係数に利用する
    float4 worldPos = mul(Unity_ObjectToWorld, float4(center, 1.0));
    float3 dist = length(_WorldSpaceCameraPos - worldPos);
    fixed destruction = clamp((_StartDistance - dist) / (_StartDistance - _EndDistance), 0.0, 1.0);
    #endif

    // 省略していますが、独自で定義した「rand」関数を使って乱数を生成しています。
    // ここではポリゴン位置などをseedにして乱数を生成しています。
    fixed r = 2.0 * (rand(center.xy) - 0.5);
    fixed3 r3 = r.xxx;
    float3 up = float3(0, 1, 0);

    // コードをループじゃない状態に展開することを明示する(詳細は下の記事を参照)
    [unroll]
    for (int i = 0; i < 3; i++)
    {
        appdata_t v = input[i];
        g2f o;

        UNITY_SETUP_INSTANCE_ID(v);
        UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);

        // 以下では、各要素(位置、回転、スケール)に対して係数に応じて変化を与えます。

        // center位置を起点にスケールを変化させます。
        v.vertex.xyz = (v.vertex.xyz - center) * (1.0 - destruction * _ScaleFactor) + center + (up * destruction);

        // center位置を起点に、乱数を用いて回転を変化させます。
        v.vertex.xyz = rotate(v.vertex.xyz - center, r3 * destruction * _RotationFactor) + center;

        // 法線方向に位置を変化させます
        v.vertex.xyz += normal * destruction * _PositionFactor * r3;

        // 最後に、修正した頂点位置を射影変換しレンダリング用に変換します。
        o.vertex = UnityObjectToClipPos(v.vertex);

        #ifdef SOFTPARTICLES_ON
        o.projPos = ComputeScreenPos(o.vertex);
        COMPUTE_EYEDEPTH(o.projPos.z);
        #endif

        o.color = v.color;
        o.color.a *= 1.0 - destruction * _AlphaFactor;
        o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
        UNITY_TRANSFER_FOG(o, o.vertex);

        stream.Append(o);
    }

    stream.RestartStrip();
}

※ [unroll]については以下の記事がどうなるのか分かりやすいでしょう。

wlog.flatlib.jp

だいぶ駆け足ですが、ジオメトリシェーダの概要と実例を紹介しました。
最後の実例の詳細については凹みさんのブログ記事を参照してください。

tips.hecomi.com

ちなみにこうした動作を頂点シェーダで行ってしまうと、全頂点がむすばれてしまってトゲトゲした感じの見た目になるだけで、こうした動きは得られません。

凹みさんの記事には書いてありますが、ジオメトリシェーダに対応していないモバイルなどの環境では全頂点をポリゴン用に複製し、実際にポリゴンとして分解しておく必要があります。
こうした点でも、事前準備なしにこうした演出が行えるのはメリットですね。

実際に動いている動画

ちなみに以前投稿した以下の動画で動いているポリゴン分解は、凹みさんの記事を参考にさせてもらって実装したものです。SAO感が好きですw