[Unity] ComputeShaderでモブを動かす【その7:if文削減で高速化/イマイチ】


経緯

コンピュートシェーダー(ComputeShader)を学ぶため、自動車を動かす交通シミュレーターもどきを作ってみようと思いました。個々の自動車がそれぞれ衝突を回避しつつ適切な経路で目的地に移動できるようになるのが目標です。

前々回(その5)にて、ごく単純な衝突回避はできましたが、時々カクつくのが気になりました。

road06_gpu_run.gif

そこで、前回は二次元スレッド化を試してみましたが、思うような効果は得られませんでした。今回はコードの改良でパフォーマンス向上が出来ないか試してみました。

◀【その6:二次元スレッド・複カーネル化/効果なし


if文削減で効率化?

Shader書くときは、「if文などの制御文を使うと遅くなる」ってよく聞くのですが、衝突判定では気軽に多用してました。if文を減らしたら高速化できるかもしれないと思ってやってみました。

早期 return を多用していたので、方針としては条件変数 condition を用意して、複数条件

condition!=0 なら true となる式をどんどん掛け算していくことにします。

一つでも0 (つまりfalse) になる式があれば、condition == 0 になるようにしました。


DrivingComputeShader06.compute

@@ -68,54 +68,62 @@

[numthreads(8,8,1)]
void Scan (uint3 id : SV_DispatchThreadID)
{
uint id1 = id.x;
uint id2 = id.y;

- if(id1 == id2) return;
+ // condition == 0 なら衝突予想更新しない
+ float condition = abs(id1 - id2);

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

CarD carD1 = CarsDynamic[id1];
CarD carD2 = CarsDynamic[id2];
- if(carD1.ticks <= 0) return; // 時すでに遅し
+ // 時すでに遅し
+ condition *= clamp(carD1.ticks, 0, 1);

// 別車線は無視
- if (carD1.lane != carD2.lane)
- {
- return;
- }
+ condition *= 1 - clamp(abs(carD1.lane - carD2.lane), 0, 1);

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

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

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

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

// 二つの車のサイズを考慮した距離を求める
// 同一車線なので基本的に両車の長さの半分を足したもの
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; // もっと近い相手が既にいる
- }
+
+ // もっと近い相手が既にいる
+ condition *= step(t, carD1.ticks);
+
+ // 0/1補正
+ condition = 1 - step(condition, 0);
+
+
+ // なぜか carD1.colider = carD1.colider でも変更したことになるのでリターンするしかない
+ if(condition == 0) return;

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

CarsDynamic[id.x] = carD1;
}


悔しいけど、力不足で最後のif文は削れませんでした

本当なら下記のようにして最後のif文もなくせる目論見だったんですが、判定結果がおかしくなって意図通りに動かなくなりました。

シェーダームズカシイ

   carD1.ticks = lerp(carD1.ticks, t, condition);

carD1.colider = lerp(carD1.colider, id2, condition);

正直 if文を削っても総計算量が増えたらあんまり意味ないかも・・・と思わないでもないですが、カクつく原因であるGPU負荷の局所ピークを平滑化できれば、平均的な負荷が多少あがっても意味はあるのではないかと思いました。


結果

気持ち、GPUトゲトゲが減ったように見えなくもないが、誤差の範囲と言われても仕方がないレベルです。

それと、時々現れる大きなピークは結局消せなくて、見た目にも時々カクっとなるのは改善できませんでした。

今回(road07)

prf_r08_64Refactor.jpg

前回(road06) 比較用

prf_r07_64.jpg


効率化の余地はあるのか?衝突判定なし

ここにきて、コンピュートシェーダーの効率化で本当にパフォーマンスアップできるのか、怪しく思えてきたので、衝突判定フェーズ("Scan" Kernel) をスキップさせてみました。

案の定というべきか、平均的に負荷は落ちたものの、時々発生する大きなピークはなくなりませんでした。カクつきに関しては、どうやらシェーダーのコード以前に、もっと根本的なところで負荷がかかってるようです。

衝突判定なし

prf_r08_64_noScan.jpg

前回(road06) 比較用

prf_r07_64.jpg

振り返ってみれば、その3の頃からGPU負荷の局所ピークは存在していたようです。なんなんですかね、これ?

prof03.jpg


まとめ・感想


  • パフォーマンス向上を期待して、コンピュートシェーダ中のif文を減らしてみた(8→1)。

  • しかし目立った効果はなかった。if文は増えても計算量は増えたからかも?

  • カクつきの原因のGPUピークはコンピュートシェーダ以外の原因の可能性が高そう(だが原因不明)。

  • (ということは、そこまで無茶なコードを書いてたわけではなかったようだ)

カクつきがコンピュートシェーダのせいじゃないなら、ひとまず追及は置いておいて、次は車線変更とかできるようにしてみたいですね。


おまけ(うまく動かないコード周辺のダイジェスト)

何かお気づきの点があればコメントいただければ幸いです。

カーネルは以下の順序で実行されます。

Init -> Scan -> Drive


初期化

[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;
}


衝突予測

[numthreads(8,8,1)]

void Scan (uint3 id : SV_DispatchThreadID)

CarD carD1 = CarsDynamic[id.x];

// 衝突判定色々・・・全体は上の方に貼った通り
float condition = ...
// 衝突予想時刻
float t = ...

// if (condition == 0) return; // これだなら上手く行く
// 最小値更新
carD1.ticks = lerp(carD1.ticks, t, condition);
carD1.colider = lerp(carD1.colider, id.y, condition);

CarsDynamic[id.x] = carD1;
}



加減速と移動

[numthreads(64,1,1)]

void Drive (uint3 id : SV_DispatchThreadID)
{
CarD carD = CarsDynamic[id.x];
if(carD.colider == id.x) { // 衝突の可能性の高い車はない
// 加速
}
else {
// 減速
}

// それぞれの位置情報に移動ベクトルを加算
carD.pos += carD.dir * carD.velocity * DeltaTime * 0.28;
CarsDynamic[id.x] = carD;
}