[Unity] ComputeShaderでモブを動かす【その６：二次元スレッド・複カーネル化／効果なし】

# 経緯
コンピュートシェーダー(ComputeShader)を学ぶため、自動車を動かす交通シミュレーターもどきを作ってみようと思いました。個々の自動車がそれぞれ衝突を回避しつつ適切な経路で目的地に移動できるようになるのが目標です。
![road07_gpu_run.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/d8169a4f-d359-f86d-6245-d17505bf6d88.gif)
前回組み込んだ[衝突予測と減速](https://qiita.com/Shinoda_Naoki/items/57ccfed41ab1e915164) が、200台に満たない程度でも時々カクつくのが気になりました（上のキャプチャは170台前後で推移）。そこで**シェーダーのスレッドグループをチューニングしてパフォーマンス向上できないか**試してみました。

なお、コードはいままでの分も含め全て[Github](https://github.com/ShinodaNaoki/learnComputeShader)にあります。

◀【[その５：衝突予測・リベンジ](https://qiita.com/Shinoda_Naoki/items/e57ccfed41ab1e915164)】
【[その７：if文削減で高速化／イマイチ](https://qiita.com/Shinoda_Naoki/items/584d2bce9411afaf1849)】▶

# 修正方針
ずっと気になってたのはこの部分です。

```GLSL:DrivingComputeShader04.compute
[numthreads(8,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
  uint idMin = id.x;
  int timeMin = 1000; // このままだとあと何回で衝突するか
  
  for(uint i = 0; i < count; i++) {
    if(i == id.x) continue;
    if(CarsStatic[i].size.z == 0) break; // 削除済みのデータ＝末端に到達
    FindMostDangerCar(id.x, i, timeMin, idMin);
  }
```

id.x を車のindexとして使用していますが、下のfor文で全部の車に対して衝突予測を繰り返しています。
ここはこんな風に↓for文をやめて **二次元スレッドのグループ** で回したら、シェーダーが本領発揮できるんじゃないかと思いました。

```GLSL
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
  uint idMin = id.x;
  int timeMin = 1000; // このままだとあと何回で衝突するか

  if(id.x == id.y) return;
  if(CarsStatic[id.x].size.z == 0) break;
  FindMostDangerCar(id.x, id.y, timeMin, idMin);
```

※↑あくまでイメージです。実際は **このままではいろいろ問題ある** ので、詳細は後述します。

なお、複数カーネル化の修正にあたって、下記記事を参考にさせていただきました。
【凹みTips: Unity で Compute Shader を使ったスクリーンスペース衝突有りの GPU パーティクルを作ってみた】
http://tips.hecomi.com/entry/2016/05/08/160626

## 構造体の修正

カーネル間で情報を渡すため、フィールドを追加しました。
`colider` はデバッグ目的で以前から入れてましたが、ロジックで利用するのは今回が初めてです。

```csharp:CarTemprate05.cs
public struct Car05d : ICarDynamicInfo
{
    public Vector2 pos { get; set; }
    public Vector2 direction { get; set; }
    public float velocity { get; set; }
    public int lane { get; set; }
    public int colider { get; set; } // 衝突予想相手
    public float ticks { get; set; } // 衝突予想時間

    public override string ToString()
    {
        return string.Format("[{0},{5}({1:0.0},{2:0.0})>>({3:0.0},{4:0.0})]", 
            typeof(Car05d).Name, 
            pos.x, pos.y, 
            direction.x * velocity, direction.y * velocity,
            lane
        );
    }
}
```

## シェーダーの修正
for文の部分は二次元スレッドにするとして、それ以外の部分は一次元で回したいです。そのため既存のコードを **複数カーネルに分割する** 必要があります。

まず、for文の前にあった変数の初期化処理を第一のカーネルとして抽出しました。

```GLSL:DrivingComputeShader06.compute[1]
// (64, 1, 1)のスレッドで回す
[numthreads(64,1,1)]
void Init (uint3 id : SV_DispatchThreadID)
{
  CarD carD = CarsDynamic[id.x];
  carD.colider = id.x;
  carD.ticks = 100000;

  CarsDynamic[id.x] = carD;
}
```

次は、for文に相当する二次元スレッド部分。最初から6行目あたりまでは `CSMain` から、残りは前回までの　[FindMostDangerCar](https://github.com/ShinodaNaoki/learnComputeShader/blob/master/Assets/Shader/DrivingComputeShader05.compute#L53) 関数の内容ほぼそのままです。

```GLSL:DrivingComputeShader06.compute[2]
// (8, 8, 1)のスレッドで回す
// 最も衝突の可能性の高い車のid(index)を返す
[numthreads(8,8,1)]
void Scan (uint3 id : SV_DispatchThreadID)
{
  uint id1 = id.x;
  uint id2 = id.y;
  
  if(id1 == id2) return;

  CarS carS1 = CarsStatic[id1];
  CarS carS2 = CarsStatic[id2];
  if(carS1.size.z == 0 || carS2.size.z == 0) return; // 削除済みのデータ＝末端に到達

  CarD carD1 = CarsDynamic[id1];
  CarD carD2 = CarsDynamic[id2];
  if(carD1.ticks <= 0) return; // 時すでに遅し

  // 別車線は無視
  if (carD1.lane != carD2.lane)
  {
      return;
  }

  // 相対位置ベクトル
  float2 diffPos = carD2.pos - carD1.pos;

  // 背後から接近してくるものは回避しない（相手任せ）
  if (dot2d(carD1.dir, diffPos) <= 0) return;

  // 相対速度ベクトル
  float diffVel = (carD1.velocity - carD2.velocity) * 0.28;
  if(diffVel < 0.00001){
    return; // 接近していない
  }

  float absPos = length(diffPos);
  float countAssume = absPos / diffVel;
  if(countAssume > 100000){
    return; // 遠い未来過ぎるので無視
  }

  // 二つの車のサイズを考慮した距離を求める
  // 同一車線なので基本的に両車の長さの半分を足したもの
  float distance = (carS1.size.z + carS2.size.z) * 0.5; 
  // どちらかが高速で移動しているなら停止距離には余裕を持つ
  distance += max(carD1.velocity, carD2.velocity) * 0.28f;

  float t = max(0, (absPos - distance) / diffVel);
  // このままだと近い将来衝突しそう
  if(t > carD1.ticks){
    return; // もっと近い相手が既にいる
  }

  // 最小値更新
  carD1.ticks = t;
  carD1.colider = id2;

  CarsDynamic[id.x] = carD1;
}
```

最後に for文の後のブロック、衝突予測結果を用いて加減速と、速度に応じた移動を行うブロックです。

```GLSL:DrivingComputeShader06.compute[3]
// (64, 1, 1)のスレッドで回す
[numthreads(64,1,1)]
void Drive (uint3 id : SV_DispatchThreadID)
{
  
  CarD carD = CarsDynamic[id.x];
  CarS carS = CarsStatic[id.x];

  if(carD.colider == id.x) { // 衝突の可能性の高い車はない
    if(carD.velocity < carS.idealVelocity) {
      carD.velocity = min(carD.velocity + carS.mobility, carS.idealVelocity);
    }
  }
  else {
    if(carD.velocity > 0) {
      carD.velocity = max(0,  carD.velocity - carS.mobility * 2.0);
      if( length(CarsDynamic[carD.colider].pos - carD.pos) < 5 ){
        carD.velocity = 0;
      }
    }
  }

  // それぞれの位置情報に移動ベクトルを加算　(0.28はkm/hをm/sに変換する係数)
  carD.pos += carD.dir * carD.velocity * DeltaTime * 0.28;
  CarsDynamic[id.x] = carD;
}
```

おっと、増やした＆リネームした関数をファイルの先頭でカーネルとして宣言するのも忘れずに。

```GLSL:DrivingComputeShader06.compute[0]
#pragma kernel Init
#pragma kernel Scan
#pragma kernel Drive
```

## コントローラー(C#)の修正

カーネルが複数になったので、Start時に FindKernel を使って正しい index を取得、フィールドに保持しておきます。

```csharp:CarsController06.cs
   private int[] Kernels;

    /// <summary>
    /// コンピュートバッファの初期化(Startから呼ばれる)
    /// </summary>
    void InitializeComputeBuffer()
    {
        factory = new CarRepository(MAX_CARS, CarTemplate05.dictionary);
        factory.AssignBuffers();


        StartCoroutine(WatchLoop(OnEachScan, OnEachElement));

        Kernels = new int[]
        {
            carComputeShader.FindKernel("Init"),
            carComputeShader.FindKernel("Scan"),
            carComputeShader.FindKernel("Drive")
        };
        
        factory.ApplyData();
    }
```

Updateでカーネルを呼び出す部分も修正します。
コンピュートバッファーはカーネルが変わるたびにセットし直す必要があるようです。

```csharp:CarsController06.cs
    void Update()
    {
        carComputeShader.SetInt("count", factory.ActiveCars);
        carComputeShader.SetFloat("DeltaTime", Time.deltaTime);
        var carnum = factory.ActiveCars;
        foreach (int index in Kernels)
        {
            carComputeShader.SetBuffer(index, "CarsStatic", factory.StaticInfoBuffer);
            carComputeShader.SetBuffer(index, "CarsDynamic", factory.DynamicInfoBuffer);
            if(index == 1)
            { // Scanフェーズだけ二次元
                carComputeShader.Dispatch(index, carnum / 8 + 1, carnum / 8 + 1, 1);
            }
            else
            {
                carComputeShader.Dispatch(index, carnum / 64 + 1, 1, 1);
            }
        }
    }
```

`if(index == 1)` は横着しました。FindKernel使う意味が無くなるような決め打ちです。すみません。
せっかく `FindKernel("Scan")` 使って、今後のindex変動に影響がないようにしてるのに、ここで固定値使ったら片手落ちですね。
　
# パフォーマンス比較
冒頭に貼った画像の状態でプロファイリングしました。車の数は 160〜170台で落ち着きます。
![road07_gpu_run.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/d8169a4f-d359-f86d-6245-d17505bf6d88.gif)

## 多次元グループ化

|スレッドグループ、一次元と二次元|
|:---|
|`[numthreads(64,1,1)]` ![prf_r06_64.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/b575c263-dd84-b59e-40f5-a6c07a73c417.jpeg)|
|`[numthreads(8,8,1)]` ![prf_r07_64.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/2036d00e-abc6-512a-f79a-91f49afe6ea7.jpeg)|

ご覧の通り、顕著な差は見られません。ちょっと期待してたのですが残念な結果に。
参考にしたサイトには「二次元配列を一つのindexで回すような計算の無駄を省ける」とか書いてあったが、逆に**計算量が変わらないのならば効果はない**ということか。

時々カクつくのは、GPU Usage に時々ピークがあるせいでしょう。（CPU Usage にもばらつきありますが、fps的に無視できるレベル。）

## スレッド総数

最初に参考にしたソースが`[numthreads(8,1,1)]`だったので、ずっとそれでやってきましたが、8というのは**どうやって決めたのかふと疑問に思った**ので、スレッド数を変えることでどんな効果があるのか4と128という極端なスレッド数で比較してみました。

|グループ内スレッド数、4スレッドと128スレッド|
|:---|
|`[numthreads(4,1,1)]` ![prf_r06_4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/aa91165b-0a32-b2dc-069e-bef9ef27dc5f.jpeg)|
|`[numthreads(128,1,1)]` ![prf_r06_128.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/c2231864-a6fa-0b01-0697-6bf00e5f7d22.jpeg)|

こちらも顕著な差は見られませんでした。
~~ちょっとつまらない気もしたけれど、~~ 最適値にチューニングする必要がないと考えれば気が楽ですね。
**スレッド数はデータを扱いやすい単位で好きに指定すればよさそう**です。

# まとめ・感想
* パフォーマンス向上を期待して、**二次元スレッド・多段カーネル化**してみた。
* しかし**パフォーマンスに顕著な差はみられなかった**。
* 計算量が変わらない限り、スレッドの次元やカーネル数変えてもあまり意味はないようだ。
* 一度に回すスレッド数も、全体の計算量が変わらない限り意味はなさそうだ。
* カクつきは GPU Usage の偏りが原因と思われる。⇒ **if文を減らすべきかも**。