【Unity】ComputeShaderによるVectorField計算

この記事はカヤックUnityアドベントカレンダー2018の7日目の記事になります。


こんにちはカヤックエンジニアのながた(永田 俊輔 | 面白法人カヤック)です。


さて、Unityで大量のオブジェクト計算を行う場合、
ComputeShaderが有力な選択肢になることが多いかと思います。

 
GPUの処理によって、

膨大な数の並列計算が可能なため100万個以上のパーティクルを表示するような
表現もWEB上ではたくさん見つけられますよね。

 

ただ、実際に3Dのコンテンツを作っていると
10万〜100万といった単位でGPUの計算コストを全投資することはなかなか難しく、
大量のオブジェクト演出をしたいときは数や計算内容とのトレードオフを常に強いられることになると思います。

 

そんなとき空間上に一定サイズのベクトル場(https://en.wikipedia.org/wiki/Vector_field)を事前に作成し、その力場を参照するVectorField技法が比較的計算コストを抑えつつ、
パーティクルなどの自然なオブジェクト制御を行う上で有用な場合があるのでその作成方法をご紹介します。
 

まず、ベクトル場の単位情報を作成し、そのバッファ変数を宣言します。

struct Force
{
float3 direction;
float power;
};

RWStructuredBuffer<Force> vectorFieldBuf;

 

続いて、力場の計算です。

今回はスレッドの構成をxyzの立方体に見立てて、
単位スレッドの数を(8, 8, 8)にしてみます。
8 x 8 x 8 の立方体が空間に敷き詰められていくようなイメージです。

このブロック一つ一つで計算が行われることになります。


f:id:nagata-shunsuke:20181202225158p:plain
単位スレッドのイメージ
 

仮に16 x 16 x 16のベクトル場を作成したいとします。
すると8 x 8 x 8の立方体が 8つ入ることで全体の計算が可能なので、
スレッドのグループ数は(16/8, 16/8, 16/8) = (2, 2, 2)となり、これでベクトル場全体の計算範囲を確保できます。

あとは、各グリッドの中身のベクトル場を計算します。
今回は流体的なパーティクル表現をするために、xyzで相関関係を持ったNoiseフィールドを作成してみました。

 

#define NUM_THREAD_X 8
#define NUM_THREAD_Y 8
#define NUM_THREAD_Z 8

#include "UnityCG.cginc"

float noiseScale;
float timeScale;
float3 groupNum;

[numthreads(NUM_THREAD_X, NUM_THREAD_Y, NUM_THREAD_Z)]
void Update(
 uint3 id: SV_DispatchThreadID,
 uint3 groupID: SV_GroupID,
 uint3 groupThreadID: SV_GroupThreadID
)
{
 uint groupThreadIndex = groupThreadID.z * NUM_THREAD_X * NUM_THREAD_Y + groupThreadID.y * NUM_THREAD_X + groupThreadID.x;
 uint groupIndex = groupID.z * groupNum.x * groupNum.y + groupID.y * groupNum.x + groupID.x;
 uint bufferIndex = NUM_THREAD_X * NUM_THREAD_Y * NUM_THREAD_Z * groupIndex + groupThreadIndex;

Force force = vectorFieldBuf[bufferIndex];
 
 uint3 cubicPos = uint3(
 NUM_THREAD_X * groupID.x + groupThreadID.x,
 NUM_THREAD_Y * groupID.y + groupThreadID.y,
 NUM_THREAD_Z * groupID.z + groupThreadID.z
 );
 
 float fixedTime = _Time.y * timeScale;
 float3 noiseSeed = float3(cubicPos.x, cubicPos.y, cubicPos.z) * noiseScale;
 float noiseXY = snoise(float3(noiseSeed.xy, fixedTime));
 float noiseXZ = snoise(float3(noiseSeed.xz, fixedTime));
 float angleXY = -PI + noiseXY * TWO_PI;
 float angleXZ = -PI + noiseXZ * TWO_PI;
 float dirX = sin(angleXY) * cos(angleXZ);
 float dirY = sin(angleXY) * sin(angleXZ);
 float dirZ = cos(angleXY);
 force.direction = float3(dirX, dirY, dirZ);
 
 vectorFieldBuf[bufferIndex] = force;
}

 

Unity上で表示するとこのような結果になります
キューブが表示されている部分が、各ベクトル場の方向を表しています。
空間をうごめく風のように見えますね。

f:id:nagata-shunsuke:20181202225437g:plain
可視化したベクトル場
 

さてあとは、パーティクルであれば頂点の現在位置が、ベクトル場のどの範囲に入っているかを
座標ベースで逆探知してベクトルの強さを自分に掛けてあげれば、基本的な流れは終了です。
広大な地面の大量の草の頂点を揺らめかせたりすることにも応用ができるかもしれません。


f:id:nagata-shunsuke:20181202225624g:plain
ベクトル場を参照させて動かしたパーティクル
 

今回は16 * 16 * 16 = 4096 のベクトル場を処理しました。
パーティクルはあくまでもこのベクトル場のちからを参照しているだけなので、
個別にNoiseなど高負荷の計算を独立して行う場合よりも、
パーティクルの数量が増えるほどに相対的な負荷が下がっていくことになります。


CurlNoiseなどの疑似流体関数を、独立して用いた場合にくらべると、
精細さには劣りますが、計算量の低減に加えて、
力の加わり方を分離して作成できるため、動きのバリエーションを拡張する際など作りやすい構造になるかと思います。

 

お読みいただきありがとうございました!


明日は(趙 子龍 | 面白法人カヤック)による「 Unity x Slack x Githubで効率の良いワークフローを作ろう!」です!おたのしみに!