Geometry Shaderのサンプルを解読する(初級編)


はじめに

  • Unityでかっこいいエフェクトを作りたい!!
  • keijiroさんのエフェクトがめっちゃカッコ良いから参考にしたい!!
  • でもテクニックが上級すぎてソース見ても全然わからない!!!!(ごめんなさい)

という経験、Unity勢のみなさんなら一度は通ったことがあるのではないでしょうか…?(暴論)

この記事では、(主に自分が)そんな状態を脱するべく
エフェクトの中でよく使われている Geometry Shader というシェーダーを理解して、
カッコいいエフェクトを作れるようになるための一歩を踏み出します!

※ 備忘録的に書いているので、説明を一部省略してしまっているところがあります。ご了承ください。

Geometry Shader とは

  • 簡単に言うと、プリミティブ(メッシュを構成する基本形状)の増減や変換ができるシェーダーです。
  • Vertex Shader の後、Fragment Shader の前に実行されます。
  • メッシュの頂点数をいじれたり、頂点を増やしてポリゴンにしたり、増やした頂点の位置や回転などを制御することができるため、エフェクトの幅をかなり広げることができます。

以下の記事や本の説明がとてもわかりやすいので、ぜひご一読ください。

サンプル

コード

改変したコードを以下にアップしています。
https://github.com/genkitoyama/StandardGeometryShader

解説

上記のサンプルにおいて、特にGeomrtry Shaderでは大きく3つのことが行われています。

1. 球を構成している三角形の頂点を取得する
2. 取得した三角形の頂点を法線方向に押し出す
3. 押し出した上面と側面のプリミティブを出力する

これらについて、他のシェーダーも合わせて、解説をしていきます。

--

事前準備など

まず事前に、各シェーダに渡す構造体や、CPU側から渡すプロパティなどを設定します。

プロパティ

エフェクトをアニメーションさせるための変数を設定します。
(これをスクリプトやタイムラインからシェーダーに送る)

_LocalTime("Animation Time", Float) = 0.0

構造体 (Struct)

  • Vertex Shader → Geometry Shader に渡すための構造体 Attributes
  • Geometry Shader → Fragment Shader に渡すための構造体 Varyings

の2種類の構造体を用意します。(今回はどちらもpositionnormalのみ)


struct Attributes
{
    float4 position : POSITION;
    float3 normal : NORMAL;
};

struct Varyings
{
    float4 position : SV_POSITION;
    float3 normal : NORMAL;
};

Vertex Shader

Vertex Shaderでは、設定した構造体の各パラメータに関して、
Unityのオブジェクト座標からワールド座標系への変換だけを行います。


Attributes Vertex(Attributes input)
{
    // Only do object space to world space transform.
    input.position = mul(unity_ObjectToWorld, input.position);
    input.normal = UnityObjectToWorldNormal(input.normal);
    return input;
}

Geometry Shader

Geometry Shaderでは、最初に「入出力するプリミティブの型」と、「最大で出力する頂点の数」を設定する必要があります。

今回は、三角形を押し出して三角柱(ただし底面なし)をつくりたいので、

  • 入力のプリミティブの型は triangle で 長さ3の配列
  • 出力のプリミティブの型は TriangleStream
  • 最大で出力する頂点の数 maxvertexcount3 * 1(上面) + 4 * 3(側面) = 15

とします。

また、今回はプリミティブごとにそれぞれ異なった押し出し量にしたいので、
引数に各プリミティブのID uint pid : SV_PrimitiveID も入れています。


[maxvertexcount(15)]
void Geometry(triangle Attributes input[3], uint pid : SV_PrimitiveID, inout TriangleStream<Varyings> outStream)
{
    ...
}

1. 球を構成している三角形の頂点を取得する

三角形で構成されている球(Icosphere)に対して、それぞれの三角形を取得します。
Geometry Shaderはプリミティブ(今回でいうと三角形)ごとに実行されるので、
引数で設定した配列を順番に設定していけばOKです。


float3 wp0 = input[0].position.xyz;
float3 wp1 = input[1].position.xyz;
float3 wp2 = input[2].position.xyz;

このように、Vertex Shaderではできなかった隣接した頂点を参照できるのがGeometry Shaderの強みだったりします。

2.取得した三角形の頂点を法線方向に押し出す

まず、押し出す量を設定します。
saturate() は、引数を[0,1]でクランプして出力してくれる便利関数です。
(0未満の場合は0に、1より大きい時は1に、その間ならそのまま)
※ マジックナンバーに関しては、実際にいじりながら動きを見てみて、気持ち良いところになればOKだと思います。


float ext = saturate(0.4 - cos(_LocalTime * UNITY_PI * 2) * 0.41);
ext *= 1 + 0.3 * sin(pid * 832.37843 + _LocalTime * 88.76);

上記で設定した押し出し量と、 ConstructNormal() という関数で求めた法線ベクトルとを
掛け合わせたものを各頂点に加算することで、押し出し後の頂点を設定していきます。


float3 offs = ConstructNormal(wp0, wp1, wp2) * ext;
float3 wp3 = wp0 + offs;
float3 wp4 = wp1 + offs;
float3 wp5 = wp2 + offs;

3. 押し出した上面と側面のプリミティブを出力する

Geometry Shaderでは、Vertex Shaderで設定した頂点も改めて全て出力し直す必要があります。
そのため、押し出した上面と、押し出したことで出現した側面x3の頂点情報を出力していきます。

出力の仕方はメッシュを作るときと似ていて、

  • outStream.Append() で現在のストリームに頂点情報を出力
  • outStream.RestartStrip() で現在のストリームを終了

を繰り返して出力していきます。

VertexOutput() は、Geometry Shaderの出力用の構造体を用意するための関数です。
具体的には、positionは Fragment Shaderに渡すためにクリッピング座標系に変換し、
normalはそのまま出力しています。


// Cap triangle
float3 wn = ConstructNormal(wp3, wp4, wp5);
float np = saturate(ext * 10);
float3 wn0 = lerp(input[0].normal, wn, np);
float3 wn1 = lerp(input[1].normal, wn, np);
float3 wn2 = lerp(input[2].normal, wn, np);
outStream.Append(VertexOutput(wp3, wn0));
outStream.Append(VertexOutput(wp4, wn1));
outStream.Append(VertexOutput(wp5, wn2));
outStream.RestartStrip();

// Side faces
wn = ConstructNormal(wp3, wp0, wp4);
outStream.Append(VertexOutput(wp3, wn));
outStream.Append(VertexOutput(wp0, wn));
outStream.Append(VertexOutput(wp4, wn));
outStream.Append(VertexOutput(wp1, wn));
outStream.RestartStrip();

Fragment Shader

最後にFragment Shaderで色をつけていきます。

本来はここでDeferred Renderingの処理を書いて質感を高めていますが、
ここでは簡単のためにnormalの値だけRGBに乗算するようにします。
(一色だと動いているかがわかりにくいので)


float4 Fragment(Varyings input) : COLOR
{
    float4 col = _Color;
    col.rgb *= input.normal;
    return col;
}

実行結果

実行すると、こんな感じになります。
タイムラインから送られている _Localtime の値に合わせて、
表面の三角形がボコボコ動くことが確認できます。

(最初の状態と色味以外何も変わっていないように見えますが、Geometry Shaderでこのようなエフェクトが作れることがわかったかなと思います…!)

おわりに

  • Geometry Shaderの簡単なサンプルを解読してみました。
    • (初級編と言いつつボリュームが多くてすみません…)
    • (そして勝手に解説してしまってkeijiroさんすみません…)
  • 次回は、中級編としてもっと複雑なGeometry Shaderのエフェクトを解説する予定です!

参考にしたリンク集