UnityでGeometryShaderの基本実装

GeometryShaderとは
DirectX10のShader Model4.0で加わった新しいステージで
Primitive(ポリゴンの面)ごとに何かしらの処理が出来る便利なShaderです。


GeometryShaderの具体的な使い方としては


・各面ごとに陰影を計算したり
・各面ごとにポリゴンを動かしたり
ワイヤーフレームのような表現も出来ます


GeometryShaderは使い方次第で色々な事が出来ます。
しかし
GeometryShaderとSurfaceShaderの合わせ技は出来ないので
GeometryShaderを使う場合はシェーデイングを自前で実装しなくてはいけなくなります。
なのでUnityで実際に使うとしたら
GeometryShaderを使いたい部分は動的にマテリアルを変更するのが良いのかなと思います。
例えばプリミティブを破壊する時だけGeometryShaderに切り替えるなどなど

SurfaceShaderが使えないのはちょっと痛いですが
それでもGeometryShaderはだいぶ便利なShaderなので色々使い道はあると思います。


もしくはPrimitiveごとに動かしたいだけなのであれば
DCCツールで事前にPrimitiveごとに動くアニメーションを作っておいて
前回解説したVertexAnimationTextureを使う方法も良いと思います。
VertexAnimationTextureはVertexShaderとSurfaceShaderを組み合わせたShaderになっているため
シェーディングの実装がシンプルになります
ので色々な事を考えるとVertexAnimationTextureの遥かにラクなのですが
Shaderを学ぶ事は3DCGの仕組みの本質を理解する事ですので
GeometryShaderの中身がどうなっているのか知っておくのは大切な事なのかなと個人的に思います



それでは手始めに
Microsoftの公式チュートリアルにあるGeometryShaderをUnity用に移植してみます。
チュートリアル 13:ジオメトリ シェーダー


Shader "explosion"
{
	Properties
	{
		_Color("Color", Color) = (1,1,1,1)
		_MainTex("Texture", 2D) = "white" {}
		_Explode("Explode", Range(0., 10)) = 0.2
	}
	SubShader
	{
		Tags{ "RenderType" = "Opaque" }
		Cull Off
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma geometry geom
			#include "UnityCG.cginc"
		
			struct v2g
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float2 uv : TEXCOORD0;
			};

			struct g2f
			{
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
			};
		
			float4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			float _Explode;
			
			v2g vert(appdata_base v)
			{
				v2g o;
				o.vertex = v.vertex;
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				o.normal = v.normal;
				return o;
			}

			[maxvertexcount(3)]
			void geom(triangle v2g IN[3],inout TriangleStream<g2f> tristream)
			{
				g2f o;
				float3 normalFace = normalize(cross(IN[1].vertex - IN[0].vertex, IN[2].vertex - IN[0].vertex));
				float4 explode = float4(normalFace*_Explode,0);
				for (int i = 0; i<3; i++)
				{
					v2g v = IN[i];
					g2f o;
					o.pos = UnityObjectToClipPos(v.vertex + explode);
					o.uv = v.uv;
					tristream.Append(o);
				}
				tristream.RestartStrip();
			}

			fixed4 frag(g2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.uv) * _Color;
				return col;
			}
			ENDCG
		}
	}
}


それでは一つずつ説明していきます。
今回はGeometryShaderの解説なので
VertexShaderとFragmentShaderの基本的な説明は割愛します。

#pragma geometry geom

まずGeometryShaderをgeomに定義します。

struct v2g
{
	float4 vertex : POSITION;
	float3 normal : NORMAL;
	float2 uv : TEXCOORD0;
};

構造体v2gは
VertexShaderからGeometryShaderに渡す情報になります。

  • 各頂点の位置
  • 各頂点の法線
  • 各頂点のUV値

をGeometryShaderに渡しています。

struct g2f
{
	float4 pos : SV_POSITION;
	float2 uv : TEXCOORD0;
};

構造体g2fは
GeometryShaderからFragmentShaderに渡す情報になります

  • 各Primitiveの位置
  • 各PrimitiveのUV

をFragmentShaderに渡しています。


VertexShaderでは何もせず
構造体v2gに渡す値を出力しています。



そして下記の部分がGeometryShaderになります。

[maxvertexcount(3)]
void geom(triangle v2g IN[3],inout TriangleStream<g2f> tristream)
{
	g2f o;
	float3 normalFace = normalize(cross(IN[1].vertex - IN[0].vertex, IN[2].vertex - IN[0].vertex));
	float4 explode = float4(normalFace*_Explode,0);
	for (int i = 0; i<3; i++)
	{
		v2g v = IN[i];
		g2f o;
		o.pos = UnityObjectToClipPos(v.vertex + explode);
		o.uv = v.uv;
		tristream.Append(o);
	}
	tristream.RestartStrip();
}

一つずつ解説していきます。

[maxvertexcount(3)]

maxvertexcountはシェーダーが実行されるたびに出力できる頂点の最大数を示します。一つのPrimitiveは頂点が3個なので3と書いておきます。

void geom(triangle v2g IN[3],inout TriangleStream<g2f> tristream)

triangle v2g IN[3]
頂点を3ずつ取ってくる(ようは頂点を3つずつ処理するので頂点番号が0,1,2を一つのPrimitiveで、3,4,5が二つ目のPrimitiveとして3つずつの頂点で処理出来るようになる)


TriangleStream
3つの頂点からなるPrimitiveを処理しますよという合図


g2f
g2fにGeometryShaderでいじった値をout


tristream
GeometryShaderで新たに生成した頂点をtristreamに流す


ここは難しく考えずとりあえずPrimitiveをいじりたいのであれば上記の設定ししておけば基本問題ないと思います。

g2f o;

GeometryShaderからFragmentShaderにoutします

float3 normalFace = normalize(cross(IN[1].vertex - IN[0].vertex, IN[2].vertex - IN[0].vertex));

色々ありますがとりあえずnormalize関数の引数に着目してみます。
第一引数には
IN[1].vertex - IN[0].vertex
と書いてあります。
これは頂点番号1番の頂点から2番の頂点を減算しているという事になります。
この計算方法で三角形一辺のベクトルを出しています。

第二引数の
IN[2].vertex - IN[0].vertex
も同じで頂点番号2番と0番のベクトルを出しています。

これで三角形を形成する二つのベクトルの値が出せたので
cross関数でこの二つのベクトルの外積を出します。

外積というのは
二つのベクトルの直行するベクトルを出せる関数です。
これが一つのプリミティブの法線になります。

この法線の値をnormalize関数で正規化します。

これで各プリミティブの法線情報がデータ型float3の変数normalFaceに入りました。

float4 explode = float4(normalFace*_Explode,0);

上記で出した法線ベクトル(normalFace)に任意の値を乗算します(_Explode)これをexplodeというfloat4に定義します。

for (int i = 0; i<3; i++)
	{
		v2g v = IN[i];
		g2f o;
		o.pos = UnityObjectToClipPos(v.vertex + explode);
		o.uv = v.uv;
		tristream.Append(o);
	}
	tristream.RestartStrip();

次にfor文の説明をします
このfor文はプリミティブに対して何かの処理を与える為のループになります。
三角形の頂点は3つなので3回繰り返しているということになります。
ループの中身を見ていきます。

v2g v = IN[i];

頂点(IN[i])を変数vに入れる

g2f o;

GeometyrShaderからFragmentShaderにoutします。

o.pos = UnityObjectToClipPos(v.vertex + explode);

変数explodeにはプリミティブの法線ベクトル*任意の値が入っているので
頂点の位置 と プリミティブの法線ベクトル*任意の値を乗算して
各頂点をプリミティブの法線の方向に押し出す
その結果をUnityObjectToClipPosで座標変換してo.posに入れてout

o.uv = v.uv;

プリミティブのuv値をout

tristream.Append(o);

tristreamに頂点を流す

tristream.RestartStrip();

geometryshaderで調理した頂点は
tristreamに流れてきた頂点を
RestartStripで新しい頂点を生成する
GeometryShaderは頂点をプリミティブ単位でいじれるshaderだが、
これは移動させてるというより
一つ一つの頂点を生成しなおしている


以上がUnityにおけるGeometryShaderの基本になります。
今回はGeometryShaderということで
基本的なShaderの知識を持っている前提での説明となってしまいましたが
VertexShaderとFragmentShaderを一通り触って次にGeometryShaderを触ろうと思ってる人に向けて書きました。
(自分自身その段階でUnityのGeometryShaderの基本的な解説が無かったのでちょっと躓いたので)
今回のShaderで基本的な所が分かればDirectX11のコードからUnityのshaderに移植出来ると思います。