This explanation is currently available in Japanese.
ひとことで
前の状態を読みながら次の状態を計算するとき、読み元と書き先に別々のバッファを用意し、1 回計算するたびに役割を入れ替える方法です。卓球の球のように行ったり来たりするので、ピンポンバッファと呼ばれます。同じバッファを読みながら書くと、途中で書き換わった値を読んでしまい、計算が壊れます。GPU では、テクスチャを読みながら同じテクスチャに書くことはそもそもできないので、流体や波などのシミュレーションで必ず使う基本の技です。
ゲームでの使いどころ
- GPU のシミュレーション:安定流体、反応拡散、高さ場の波、ライフゲーム。どれも「周りのマスの今の値」から次の値を決める。
- 粒子の更新:位置と速度のテクスチャを 2 組持ち、交互に更新する(コンピュートシェーダーのない WebGL での定番)。
- 画面の効果:ぼかしを何回もかける、前のフレームの結果に今のフレームを混ぜる(TAA、残像)。
- ダブルバッファリング:画面の表示でも、表示中のバッファとは別のバッファに描いてから入れ替える。書きかけの画面が見えないようにするための、同じ考え方。
しくみ
2 枚のバッファ A と B を用意します。
- A を読んで次の状態を計算し、B に書きます。
- 役割を入れ替え、B を読んで A に書きます。
- これをくり返します。
全部のマスが、同じ「今の状態」を読んで計算するので、計算する順番によらず正しい結果になります。
1 枚で書き換えるとどうなるか:左上から順に更新すると、あるマスを計算するとき、左や上の隣はすでに次の状態に書き換わっています。「今の状態」と「次の状態」が混ざったものを読むことになり、規則が崩れます。ライフゲームなら、形を保って進むはずのグライダーが壊れます。
GPU は全部のマスを同時に(順番の決まっていない順で)計算するので、1 枚で書き換えると結果が実行のたびに変わります。そのため WebGL や多くの API では、読んでいるテクスチャへの書き込み自体が禁止されています。
実装のポイント
- 最新の結果がどちらにあるか:入れ替えのあとは、最新の結果は「次に読む方」にあります。表示に使うバッファを取り違えないようにします。
- 反復の回数が奇数か偶数か:くり返しの回数によって、最後に書いたのが A か B かが変わります。入れ替えを変数で管理すれば気にしなくて済みます。
- 精度:シミュレーションの値を 8 ビットのテクスチャに入れると、誤差がたまって壊れます。16 ビットや 32 ビットの浮動小数点のテクスチャを使います。
- Three.js では:
GPUComputationRendererが、このピンポンを変数ごとに自動で管理してくれます。
つまずきやすい点
- メモリが 2 倍:状態ごとに 2 枚要るので、大きなシミュレーションではメモリの使用量に注意します。
- 片方だけ初期化する:最初に両方のバッファを同じ初期状態にしておかないと、1 回目の入れ替えで古いゴミを読むことがあります。
- その場で書き換えてよい場合:ガウス=ザイデル法のように、あえて更新済みの値を使って収束を速める計算もあります。ただし GPU で並列に行うと順番が決まらないので、赤黒の市松模様に分けて 2 回に分けるなどの工夫が必要です。
デモで試すこと
- 「2 枚を交互に(ピンポン)」では、たくさんのグライダーが形を保ったまま斜めに進みます。右の小さな図で、読んでいるバッファ(青い枠)と書いているバッファ(黄色い枠)が毎世代入れ替わるのがわかります。
- 「1 枚をその場で書き換える」に切り替えると、更新済みの隣のセルを読んでしまうので、グライダーがすぐに壊れて、でたらめな模様に広がっていきます。
- 「1 秒の世代数」を下げると、入れ替わりの様子をゆっくり見られます。「最初から」で初期の配置に戻せます。