CustomRenderTextureだけでボリュームレンダリングしてみる!!

# はじめに
本記事では、VRChatでボリュームレンダリングするための根底技術を記載する。
VRChatに持ち込むためスクリプト等は用いずCustomRenderTexture+Shaderのみで実装する。
また、実装に伴いCustomRenderTexture内で任意のメッシュを描画する方法を記載する。

# 書いてるテクニックの要約
1. CustomRenderTexture内で任意のメッシュを描画する方法
2. 3DのCustomRenderTextureに対して、パーティクルを書き込む方法
3. ボリュームレンダリングの一例 (レイキャスティング)

# ボリュームレンダリングとは
[wikipedia(ボリュームレンダリング)](https://ja.wikipedia.org/wiki/ボリュームレンダリング)
要は、3Dのボリュームテクスチャに対して、何かしらのShaderでレンダリングを行う技術。
いわゆるCTやMRIの様なデータを表示する技術。

# 「1. CustomRenderTexture内で任意のメッシュを描画する方法」
### CustomRenderTexture
CustomRenderTextureのサンプルの多くは、頂点シェーダーに"UnityCustomRenderTexture.cginc"の"CustomRenderTextureVertexShader"を使っている。
頂点シェーダーにこれを適応すると、フラグメントシェーダーで容易に"v2f_customrendertexture"構造体を入力として得ることができ、本質的に「あるピクセルの色を決める」というシェーダーが書ける。
「あるピクセルの色を決める」際には、下記のようないくつかの方法が考えられる。
 - 1フレーム前の自身と同じピクセルを使う; 徐々に暗くすることで残像のようなエフェクトなど
 - 1フレーム前の自身とその周辺のピクセルを使う; ライフゲームの実装など
 - 単純に時間から計算する; 時間で回転する色相環など
 - その他; テクスチャを合成するなど
基本的には「あるピクセルの色を決める」ために、複数のテクスチャの任意のピクセル等を参照して色(ないしデータ)を計算し続けることになる。
### CustomRenderTextureでGeometryShaderを書く
CustomRenderTextureは対象テクスチャのRenderTargetを用いて、対象テクスチャ全域に板ポリを1枚描画しているに過ぎない。
RenderTargetにバインドされているためシェーダーを書けば任意のメッシュを描画することができる。
これによりできることは下記のようなことである。
 - 任意のメッシュを描画することができる; メッシュベースでのテクスチャ生成
 - GPUでパーティクル等の計算を行う際、任意の座標にデータを書き出せる; 例えばあるパーティクルの寿命が尽きた時に、複数のパーティクルを追加する等の処理がGeometryShaderだけで完結できる(予め書き出す座標等は取り決めておく必要があるが)
この様な汎用性の高いGeometryShaderをCustomRenderTexture内で書くために、いくつか乗り越える障害があるため、段階に分けて記載する。

**1. 頂点シェーダーを書く**
"UnityCustomRenderTexture.cginc"の"CustomRenderTextureVertexShader"が行っていることは、SV_VertexIDをもとにSV_POSITIONとTEXCOORD0を計算しているだけである。
SV_POSITIONは対象テクスチャ全域になるような板ポリ1枚の座標になる。
今回、GeometryShaderステージを多数(最大131,072個分)呼ぶため、テッセレーションを行う。テッセレーションでは頂点座標のみ必要なため、下記に最小の頂点シェーダー例を示す。

```c


VS_OUT mainVS(VS_IN In)
{
	float4 vertex[6] =
	{
		float4(+1, -1, 0, 1),
		float4(+1, +1, 0, 1),
		float4(-1, -1, 0, 1),
		float4(-1, -1, 0, 1),
		float4(+1, +1, 0, 1),
		float4(+1, -1, 0, 1),
	};

	VS_OUT Out;
	Out.pos = vertex[In.vertexID % 6];
	return Out;
}
```
**2. テッセレーションを行う**
[wikipedia(テッセレーション)](https://ja.wikipedia.org/wiki/%E3%83%86%E3%83%83%E3%82%BB%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3)
テッセレーションを用いる基本概念は、板ポリ1枚からGeometryShaderステージに大量のプリミティブを与えるために、メッシュの細分化を行うためである。
上記wikipediaにある画像のように、板ポリに対し正しくquadでテッセレーションを行うことで、きれいな格子状に分割できる。
分割した格子の座標(厳密にはuv座標)から、連続一意なidを生成することが可能である。
 - ハルシェーダー; 分割する数を指定 (TESS=64が上限であるため、最大4096(64^2)分割できる)
 - ドメインシェーダー; 分割後に、格子を構成するuv座標をもとに、連続一意なidを発行しGeometryShaderに与える
下記に、CustomRenderTextureで使用する連続一意なidを発行するためのハルシェーダー・ドメインシェーダーの例を示す。

```c

CONSTANT_HS_OUT mainCHS()
{
	CONSTANT_HS_OUT Out;
	int t = TESS + 1;
	Out.Edges[0] = t;
	Out.Edges[1] = t;
	Out.Edges[2] = t;
	Out.Edges[3] = t;
	Out.Inside[0] = t;
	Out.Inside[1] = t;
	return Out;
}

[domain("quad")]
[partitioning("pow2")]
[outputtopology("point")]
[outputcontrolpoints(4)]
[patchconstantfunc("mainCHS")]
HS_OUT mainHS()
{
}

[domain("quad")]
DS_OUT mainDS(CONSTANT_HS_OUT In, const OutputPatch<HS_OUT, 4> patch, float2 uv : SV_DomainLocation)
{
	DS_OUT Out;
	Out.pid = (uint)(uv.x * (TESS - 1)) + ((uint)(uv.y * (TESS)) * (TESS - 1));
	return Out;
}
```
**3. ジオメトリシェーダーを書く**
ジオメトリシェーダー内でidをもとにメッシュを描画する。
TriangleStreamを使えばメッシュの描画を行うことができる。
PointStreamを使えば特定の1ピクセルにデータを書き出すことができる。
 → 特にGPUを使ってパーティクル等の計算を行う際に便利

下記にジオメトリシェーダーの例を示す。
なお、同じプリミティブに対してGeometryShaderを複数回呼ぶ機能がinstanceであり最大32まで指定可能で、gsidは0~31の値を取る。
テッセレーションと合わせると、最大4096*32=131,072個分となる。
(ジオメトリシェーダーはmaxvertexcountの上限の関係上、独立したポリゴンを最大で85ポリゴン分作れるので、板ポリ1枚から11,141,120ポリゴン描画することができる)

```c

Texture2D<float4> _DataTex; // (rgba Float 想定)

[maxvertexcount(3)]
[instance(NUM_INSTANCE)]
void mainGS(point DS_OUT input[1], inout TriangleStream<GS_OUT> outStream, uint gsid : SV_GSInstanceID)
{
	uint id = input[0].pid * NUM_INSTANCE + gsid;
	// 以降idを用いた処理をなんでも

	// 例えばidから_DataTexをロードして、dataをもとに描画するなど
	float4 data = _DataTex.Load(int3(id % TEX_W, id / TEX_W, 0));
```

# 「2. 3DのCustomRenderTextureに対して、パーティクルを書き込む方法」
3Dのテクスチャは通常の2Dのテクスチャが何枚も重なったものである。(通常のxとyに対して、"何枚目"と考えると理解しやすい)
下記に3Dテクスチャにパーティクルを書き込む方法を記載する。
なお、書き込むのは"ボリュームレンダリングに用いるパーティクルの密度"であり、描画するものは"パーティクルの座標周囲数ピクセルに球状"に書き込む。
**1. 2Dテクスチャを介して3Dテクスチャに書き込む**
3DのCustomRenderTextureに直接データを書き込もうとしたが、下記の通りいくつかの問題が発生したため、一度2Dのテクスチャに書き込んでしまう方法を採用する。
 - Shader側で書き込むスライスを指定したが全てのスライスに描画された
 - InitializationModeをRealtimeにしたが機能しなかった

パーティクルを一度2Dのテクスチャに書き出す場合は、上記の問題は関係ない。
例えば3Dを256*256*256にするのであれば2Dは4096*4096のテクスチャを介す必要がある。
下記の様なシェーダー例で3Dテクスチャに書き込む。

```c
#define TEX_SIZE 256 // 3Dテクスチャの解像度
#define ARRAY_W 16 // 2Dテクスチャ内の横方向に、スライスをいくつ格納するか; 16*16で256スライス

Texture2D<float> _MainTex;
	
float mainFS(v2f_customrendertexture input) : SV_Target
{
	// 書き込もうとしている座標
	int3 lpc = int3(input.localTexcoord.x * (TEX_SIZE - 1), input.localTexcoord.y * (TEX_SIZE - 1), input.localTexcoord.z * (TEX_SIZE - 1));
	uint id = lpc.z;
	uint w = id % ARRAY_W;
	uint h = id / ARRAY_W;

	int3 lp = int3(lpc.x + w * TEX_SIZE, 4095 - (lpc.y + h * TEX_SIZE), 0);

	return _MainTex.Load(lp).r;
}

```
**2. 2Dテクスチャにパーティクルの密度を書き込む**
そもそも3Dテクスチャにメッシュを描画するときの方法は「xy座標で構成されるプリミティブを何枚目のスライスに描画する」という指定になる。
例えば直径5ピクセルで球を書き出す際は、
1. パーティクルのz座標から何枚目のスライスに描画するか決定する
2. 描画するxy座標での直径を決める (1.のスライスであれば直径5ピクセル)
3. 隣のスライスにも同様に描画する (この時、2.の直径はz座標がずれることを想定した数値にする)
2Dテクスチャに描画する際も同様に、何枚目のスライスに書き出すプリミティブか計算した上で、書き込めば問題ない。
一例として、下記のような形のテクスチャを作る。(256ピクセル*256ピクセルを16*16並べて、256スライス分)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/414266/defdf464-343c-8460-2ee6-878acd0bee6b.png)

# 「3. ボリュームレンダリングの一例 (レイキャスティング)」
生成した3Dテクスチャをもとにボリュームレンダリングを行う。
一例としてレイを飛ばして、一定値以上になった時の深度値を描画するフラグメントシェーダーを下記に示す。
なお、通常のCubeメッシュに対して割り当てるマテリアルを想定している。

```c



Texture3D<float> _MainTex;
SamplerState sampler_MainTex;

float4 mainFS(VS_OUT In, float vface : VFACE) : SV_Target
{
	float3 cpos = _WorldSpaceCameraPos; // カメラ座標
	float3 start = vface >= 0 ? In.wpos : cpos; // レイを飛ばす座標
	float3 v = normalize(In.wpos - cpos); // レイのベクトル

	float3 t = start;

	float step = 0.035f;
	int loop = 0;
	int loop_max = 100;
	bool hit = false;

	// なにかにヒットするまで、レイを進め続ける
	while(!hit && loop < loop_max)
	{
		// レイをすすめる
		t += v * step;
		loop++;

		// 3Dボリューム外であれば、無視する
		hit = !(t.x < -1 || t.x > 1 || t.y < -1 || t.y > 1 || t.z < -1 || t.z > 1);
		if (hit)
		{
			// 3Dテクスチャをサンプリングして、一定値以上あれば何かしらあるとして、止める
			float3 tex = float3(t.x + 1, t.y + 1, t.z + 1) / 2.0f;
			hit = _MainTex.Sample(sampler_MainTex, tex) > 0.5f;
		}
	}

	// 何もヒットしなかったのでdiscard
	if (loop == loop_max) discard;

	// とりあえず[カメラ-ヒットした座標]の距離を返す
	return distance(t, cpos );
}
```
一例
 - パーティクルをCustomRenderTexture内で計算
 - カメラからの距離で色付け
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/414266/4c8e33cd-5578-28d0-6ea7-4306cd0c2acf.png)




# 終わりに
CustomRenderTexture内でGeometryShaderが使うことができれば、いろいろできる？