ミリオンパーティクル。 パート1

画像 作成した方法を教えてから、独自のパーティクルシステムをGPUに転送します。 私が単純に考えたように、それはただ行われるでしょう(彼らはそこに、動く粒子、チュユと言います)。 実際、実装中に発生するニュアンスについて多くのことを話すことができるため、以下ではボトルネックの問題の解決策についてのみ説明します。



背景



顧客は、スクリプトに従ってdmxコントローラーを介して制御される動的な音楽噴水施設を開発します。 彼は自分でスクリプトエディタを作成しました。 しかし実際には、スクリプトを作成するのは不便であることが判明しました。それがどのようになるかを確認するには、完全に構築されて起動された噴水が必要だからです。 さらに、振付師のデザイナーが噴水にノズルを追加したい場合、これはほとんど不可能です。 したがって、振付師が実際の噴水なしでスクリプトを開発できるように、顧客は噴水をモデリングするためのモジュールを取得したいと考えました。 一般的に、このようなことが私に起こりました:ここにHawaii50.wmvによってモデル化されたもののビデオがありますが、噴水を建設した後に実際に出てきたもの: H5OClip.wmv





必要条件



現時点では、特定の方法で動作するノズルの固定セットとLED光源があります。 実際、私はこれらのノズル/光源を操作する方法で、各タイプのノズルと光源にインターフェースを提供する必要がありました。 独自のウィンドウでマウスを使用して回転できるシーンが必要です(平面上のグリッド、噴水高さマークなど、パーティクルシステムに関連しない小さな要件がまだたくさんありました)。 そしてもちろん、これはすべてリアルタイムで、つまり少なくとも25〜30フレーム/秒で必要です。



最初のパンケーキ



最初にCPUで単純なパーティクルを作成し、それからすべてが頂点バッファーに送られ、レンダリングされました。 テスト中、すべてが完全に機能し、実際には不適切であることが判明しました。 ビデオH5OClip.wmvを見ると、同時に発光する光源の数に注意してください。 多くの場合、数は数百以上に達します。 さらに、多くの場合、1つのソースが複数の噴水の流れを一度に「カバー」し、各流れは本質的にエミッタです。 150-200のジェットが同時に粒子を生成すると想像してください。 1つのジェットを描くのに必要な粒子の数は? 実際には、最大出力で打つ1つのジェットの許容可能な表示には、平均5kの粒子が必要であることがわかりました。 そして、150ジェットの場合、750,000個の粒子が得られます。 少なくとも150のジェットを配置する必要があることは明らかです。



最初のバージョンはそのように機能しました。 最初は、パーティクルを作成するプロセスがありました。 各粒子には、粒子が死んだミリ秒が保存されたフィールドがありました。 エミッタが最後のフレームに対して作成したパーティクルの数を決定し、配列の先頭からすべてのパーティクルを作成するまで実行します。 死んだ粒子に出会った場合(現在の時間>死の時間)、それを新しい死の時間で満たし、初期座標と初期速度を設定します。 実際、パーティクルが作成されます。 配列が終了し、すべてのパーティクルがまだ作成されていない場合、さらに別のメモリを割り当てます。 すべてのパーティクルを作成した場合、バッファの最後まで実行し、最後の生きているパーティクルのインデックスを記憶します。 最後の生きている粒子のインデックスが配列の長さよりはるかに短い場合、配列を短くします。 このインデックスは、バッファ全体を巡回しないように、将来的には便利です。

次に、粒子の移動とVBO(頂点バッファーオブジェクト)の同時処理が行われました。 パーティクルが生きている場合、配列を実行します-移動してVBOに入力し、そうでない場合はスキップします。 配列全体ではなく、最後の生きている粒子のインデックスをチェックします。

VBOの準備ができたので、レンダリングします。 実際には(そして、Athlon 64 x2 3800があり、これは2.0 Hzのコアです)、私の記憶が適切であれば、25-30FPSで約100-150kの粒子が出てしまい、失敗します。

したがって、750kの粒子でピークを何らかの方法で操作するか、別の方法を考え出す必要があることに同意します。 したがって、2番目のパンケーキに進みます。



セカンドパンケーキ



分析


最初に、最終的なFPSを正確に消費するテストを実施しました。 したがって、明らかに見える負荷:

  1. 新しいパーティクルの作成/消滅
  2. 粒子運動
  3. 頂点バッファフィル
  4. パーティクルレンダー


もちろん、粒子の動きは最も「抑制された」ことが判明しました。 2番目は、新しいパーティクルの作成/消滅です。 3番目の塗りつぶし頂点バッファー。 レンダリングについては、すべてが不安定でした。 噴水は3Dであるため、カメラから異なる距離にある可能性があり、粒子の深さをスケーリングする必要があります。 カメラを上から下に向けて、ジェットがカメラに直接当たるようにすると、FPSが落下します。 粒子が巨大で、充填率がそれぞれ巨大だったので、理解できます。 通常の場合、GPUは画像を非同期に表示し、CPU時間中にその作業に対処することができたため、カメラを向けた人は誰もいませんでした(将来、このような場合にも最適化されました)。



最適化の試み


最初に、変位の数学を最適化することにしました。 しかし、計算は非常に単純であったため、最適化するものはほとんどありませんでした。 コンパイラは、asmコードでこの全体を完全に最適化しました。 その後、アレイを2回実行するのではなく、パーティクルを作成、移動し、1回のパスでバッファーを満たすというアイデアが生まれました。 すぐに言ってやった。 しかし、速度の増加は顕微鏡下でのみ見ることができました。 頂点バッファーを埋めるとき、最適化は行われませんでした。 もちろん、VBOと頂点に1つのバッファを使用することもできますが、ビューポートの背後に死んだ頂点を表示する必要があり、このオプションは私にとってさらに抑制的であるように見えました。 また、VBOを完了するオーバーヘッドはわずかでした。 理論的には(フロップで)まだ大きなマージンがありましたが、それらの合成数に追いつくことは不可能でした。

もちろん、額で同じ方法で解決し、4つのスレッドに並列化して、4つのコアと2.5 GHzのコア周波数を備えたCPUプログラムの最小システム要件を設定しましたが、この方法は絶対に好きではありませんでした。



成功した最適化の試み


そのため、パーティクルの数を減らす必要があります。 遠くにある噴水は、大量の粒子を必要としないことは明らかです。 より少ない粒子を少し大きく描くことができますが、カメラが噴水に鋭く近づくと、より多くの粒子を表示する必要があり、すべてが論理的で理解できるように見えますが、問題はこれらの非常に見えない粒子を何らかの方法で移動する必要があることです それ以外の場合、近づいたときに、それらを開始点にします。 また、CPU上のすべてのパーティクルを操作する必要があるという事実に基づいています。 しかし、粒子をまったく動かさず、運動方程式を使用してその位置を単純に計算するとどうなりますか。

最も単純な運動方程式:x = x 0 + v 0 * t + 0.5 * a * t * t もしそうでないなら、それは本当に素晴らしいオプションでしょう。 顧客は「空気に対する摩擦」を望んでいました。地平線に対して低い角度のジェットでは、シミュレーション中の結果が実際の結果とは非常に異なっていたためです。 粘性摩擦の力F = -bV、液滴のサイズと形状が同じ1つの媒体の場合、摩擦による加速はa = kV(kは特定の係数)であるとおおまかに言えます。 その結果、単純な運動方程式はモンスターになります(シェーダーの現在の式:NewCoord =((uAirFriction * aVel + G)*(exp(uAirFriction * dt)-1.0)/ uAirFriction-G * dt)/ uAirFriction + aCoord;)。 そして、ワイルドな公式にもかかわらず、私が実際に描画する頂点のみの位置を考慮したという事実によってのみ、すでに目に見えるパフォーマンスの向上が得られました。 カメラから距離Nにある噴水については、2番目の粒子ごとに、4 Nごとに2 Nの距離に位置する噴水について、などです。 その結果、20-30FPSで500-700k程度の生きた粒子が得られました。これは非常に良いことです。 与えられた数字は実際には非常に浮いていることが判明し、すべてはフレーム内の噴水の位置に依存していましたが、全体的にはパフォーマンスはニーズを完全に満たしました。



第三のパンケーキ



タスクがすでに完了しており、顧客が満足しているという事実にもかかわらず、私自身のスポーツへの関心のために、GPUで計算を書き直すことにしました。 そのため、頂点バッファーへのレンダリングが必要でした。 初期値(初期位置、初期速度、初期時間、最終時間)を持つ頂点バッファーで、現在の座標のみを格納する頂点バッファーにレンダリングします。 次に、結果のバッファを使用して、パーティクル自体をレンダリングします。 ループの簡単な実装(距離に応じてパーティクルの数を減らすことなく)により、GF250で40〜50FPSのパーティクルが1kc生成されました。 CPUでパーティクルを「生成」するだけで、各フレームでかなりの数のパーティクルが取得されます。 しかし、距離に応じて異なる数のパーティクルを作成することは、ここではそれほど簡単ではありません。 結局のところ、粒子の整数配列ではなく、「穴」(死んだ粒子からの穴)を持つ配列を取得します。 このケースにはいくつかの解決策がありますが、時間が足りないため実装できませんでした。 habrasocietyがさらなる実装を検討することに興味がある場合は、自由時間の到来とともに、4番目と5番目のパンケーキ(さらにはデモでも)を焼こうとします。



結論







psコードとデモがないことをおizeびします。 写真を読んで読む方が面白いことを理解していますが、古いコードのバージョンは保存されていません;私の「検索」の記憶から言えることを書きました。 したがって、古いスクリーンショット/動画/デモはありませんが、現在の動画はお客様のウェブサイトで見つけることができます。 次の記事では、自分自身を修正しようとします。



更新しました。 前述のYouTubeビデオをアップロードしました







upd2。 品質を損なわない画面:



上の画面は、油圧ショックを加えて、より美しくリアルにしようとしています。 パフォーマンスが大幅に低下したため、試行が失敗したと考えています。

画面下部-上記の記事の3番目のパンケーキを作成します。



All Articles