【12/28 追記】pscaleに関する部分を微修正しました。
はじめに
@minoge1001 と申します。3年連続のアドカレ参加です(昨年の記事、一昨年の記事)。
さて、TouchDesignerでパーティクル表現を行う場合には、主に次の3つの方法があります。
1. Particle SOP
2. GPGPU(GPUによる汎用計算)
3. Nvidia Flex Solver
GPGPUのメリットは数百万という大量のパーティクルを扱うことができる点ですが、細かい制御を行うためには自分でシェーダーを書かなければならず(逆に上級者にとっては、全て自分で制御できるというメリットでもあるのですが)難易度が高い作業です。また、Nvidia Flex Solverのメリットは同じくGPUベースで粒子間の相互作用もノード側で計算してくれるため、流体等のシミュレーションをコードを書かずに行うことができますが、設定項目が多くこちらも初心者には少しハードルが高い方法です(ちょうど12/8に@v_kaoruさんが記事を書かれています!)。
これらと比較し、Particle SOPはCPUベースのためせいぜい数千個程度のパーティクルしか操れませんが、力・風・質量・媒質からの抵抗・衝突などのさまざまパラメータが既に準備されており、GPGPUやNvidia Flex Solverに比べると格段に扱いやすいです。
Particle SOPに関する解説記事は既にWebにたくさんありますので、本記事では既にParticle SOPをなんとなく使ったことがある方向けに、なるべく被らなそうな内容を紹介してみようと思います。基本的な事項を学びたい方は田所さんや比嘉さんの講義資料がオススメです。
Particleの動作を決めるもの
まずは、以下のネットワークを構築します。Grid SOPでパーティクルの放出点を設定した後、パーティクルがランダムな順序で出るようにSort SOPでPoint SortをRandomにします。その後、Particle SOPをつなげることで、x軸方向にパーティクルが放出されます。以降、本記事ではこの状態のことを元の3ノードと記載します。
初速・放出方向
パーティクルの初速と放出角度は、放出点(この場合はGrid SOPの各点)の法線ベクトル(Normal)によって決まります。つまり、Normal(N)を変更することで、パーティクル放出時の振る舞いを制御できることになります。例えば、Grid SOPとSort SOPの間にPoint SOPを挿入してPoint>Add Normalのnxをme.inputNormal[0] * 2のように変更します(入力されている法線ベクトルのx成分を2倍にするという意味です)。その結果、法線の長さが2倍になることでパーティクルの初速も2倍になり、飛距離も長くなることがわかります。
次に、Sort SOPとPoint SOPの間にNoise SOPを挿入し、Noise>AttributeをPoint Normalsにします。こうすることで、法線の方向がランダムに変化し、それに伴ってパーティクルの放出方向も変化することがわかります。この際、法線ベクトルのみが変化しているので、元のグリッドの形状は変化していません(但し、シェーディングは変化します)。
パーティクルの初速と方向が法線ベクトルで決まるということは、法線が無くなれば(かつ、後述するForceやWind、Attractorが一切無い状態であれば)パーティクルは動かないことになります。Point SOPのPoint>No Normalを選択すると、パーティクルはグリッドの各点に張り付いた状態になります。これは、ある形状の表面上で生成・消滅するような点を生成するのに利用できます。
パーティクルに初速を与える方法には、法線ベクトルを調整する以外に放出点に対してVelocityアトリビュートを用いる方法もあります(放出されたパーティクルには自動的にVelocityアトリビュートが付与されますが、今回設定するのはその前段のSOPに対してです)。No Normalの状態のまま、Point SOPのParticle>Add Velocityをvx, vy, vz = 1, 0, 0とすると、再びパーティクルにx軸方向の初速が与えられます。
この状態で先ほどのAdd Normalに戻しても(=法線ベクトルの方向がランダムに変わっても)、パーティクルの動作は真っすぐに飛んだまま変わりません。ここでParticle>No Velocityとすると、以前のような勢いと揺らぎのあるパーティクルになります。これはつまり、パーティクルの放出点にVelocityアトリビュートが設定されている時にはパーティクルの初速と放出方向を決めるアトリビュートの優先度はVelocity>Nであることを示しています。これを利用することで、物体表面のシェーディングは変えないまま(=法線ベクトルを保ったまま)でパーティクルの初期条件を制御することができます(そのような場面が必要になるのかはわかりませんが…)。
質量、媒質からの抵抗
一旦、元の3ノードの状態に戻してみます。初期状態なので一切ForceやWindの影響がないはずなのに、パーティクルの速度が減速しています。わかりやすく見てみるために、Particle SOPのParticle>Life Expectを10に変更してみると、概ねx = 1の平面でパーティクルが止まっています。
実は、Particle SOPでは常に(ParticleのAdd Mass AttributeやAdd Drag AttributeがOFFでも!)質量(Mass)と媒質による抵抗(Drag、空気や水などから受ける抵抗)が計算されています。試しにParticles>Massを2とすると、パーティクルは大体x = 2の平面まで飛ぶようになります。これは、質量の大きい物質は動かしにくく止まりにくいという慣性の法則に直感的に適合します。逆にMass = 0.5とすると、パーティクルはx = 0.5あたりで止まるようになります。
次に、Mass = 1に戻した上でDrag = 0.5とすると、この場合もパーティクルは大体x = 2の平面まで飛ぶようになります。パーティクルが受ける抵抗力が減ったため、より遠くまで飛ぶようになったためです。逆に、Dragを大きくするとパーティクルは飛ばなくなります。Drag = 0(真空状態)とすると、一切の抵抗を受けなくなるためパーティクルは初速を保ったまま寿命が尽きるまで(この場合は10秒間)初速のまま飛び続けます。このように媒質からの抵抗の度合いを決めるパラメータがDragです。
ここで、Drag = 1に戻した上で、今度はForces>External Forceにてexternalx = 0.1としてみます。これは、x軸方向に場の力(重力)を加えることに相当します。こうすることで、パーティクルはx = 1の平面に静止せずに、最終的には一定の速度で(寿命が尽きるまで)進み続けます。SOP to CHOPやSOP to DATでParticle SOPのvアトリビュートを見てみると、v(0)が概ね0.1に収束していく様子がわかります(注:このv(Velocity)アトリビュートは、送出点のもつVelocityではなく、パーティクルひとつひとつが持つVelocityです)。物体形状等の複雑な条件を無視して単純化した場合、動く物体が媒質から受ける抵抗力は速度に比例する(例えば、高速道路を移動する車は低速で動く車よりも大きな空気抵抗を受ける)ため、パーティクルの速度が大きくなると受ける抵抗力も大きくなって、最終的には一定の速度になります。この速度のことを終端速度といいます。上空から落ちてくる雨粒が時速数百キロにはならず、もっと遅い速度で落ちてくる原理と一緒です。
さて、ここで疑問なのは、先ほど触れたAdd Mass AttributeやAdd Drag Attributeのトグルスイッチです。これは一体何なのでしょうか?結論から言いますと、Particle SOPの前段(パーティクルの送出点)でMassアトリビュートやDragアトリビュートを設定した際に、その設定値を使用するかどうかのフラグになります。実例で確認しましょう。
まず、Particle SOPのExternal Forceは初期値、つまりゼロに戻しておきます。次にGrid SOPとSort SOPの間にPoint SOPを挿入し、Particles>MassをAdd Mass/Dragとした上でMass = 3に設定します。これだけではパーティクルの動作は変わりませんが、Particle SOPのParticles>Add Mass AttributeをONにしてStateのReset PulseをONとすると、パーティクルが遠くまで飛ぶようになります。この状態でParticle SOPのParticles>Massの値を変えてもパーティクルの動作は変わりません。逆に、Point SOPでMassの値を変更すると、パーティクルの動作にリアルタイムに反映します。つまり、Massアトリビュートの優先度は送出点に設定したMassアトリビュート > Particle SOPでのMassの設定値となります。Dragアトリビュートについても同様です。
ただ、これだけではあまりメリットが感じられません。事前にMassアトリビュートを設定できるメリットは、放出点ごとに異なった質量を持つパーティクルを放出できることです。つまり、一つのパーティクルシステムの中で重い粒子と軽い粒子を簡単に混在させることができるようになります(後ほど具体例で説明します)。
場から受ける力
Particle SOPでForce(力)を制御するには
1. Forces>External Forceを使用する
2. 3番目の入力を使用する(Metaball SOP-Force SOPを接続)
の2通りがあります。前者が場全体にかかる力を制御できるのに対し、後者はMetaball SOPの領域の周辺にのみ力を加えることができます。もちろん、両方を組み合わせることでより複雑な力場を作り出すことができます。
いずれの場合せよ、パーティクルは運動の第2法則、つまりF(力)= m(質量)× a(加速度)に基づいた加速度を得ます。つまり、同じ力場に質量の異なるパーティクルが存在する場合、質量が大きいパーティクルほど得られる加速度は小さくなります。そして、終端速度は質量に比例するため、最終的な速度は速くなります。
Forcesタブの中でWind(風)を扱うこともできますが、こちらは少し複雑です。Particle SOPのページには、以下の記述があります。ざっくり言いますと、風は付加的な力であり、風によってパーティクルの速度は大きくなりますが風速以上の速さにはなりません。
Discussion - Wind vs External Force
The application of External Force directly affects a particles' acceleration, the rate of which is determined by the mass (F = Mass Acceleration). Wind is an additional force, but one that is velocity sensitive. If a particle is already travelling at wind velocity, then it shouldn't receive any extra force from it. This implies a maximum velocity when using Wind on its own.
さらに、4番目の入力にSOPを入力することでSurface Attractor(パーティクルが吸い寄せられる対象)を設定することもできます(本記事では割愛します)。
Forceのかっこいい使い方につきましては、TDSW Vol.30のKaoru Tanakaさんの回がとても参考になります。Patreonで公開されているので、有料ですが興味のある方は是非!
衝突
Particle SOPで他のオブジェクトに衝突した際の振る舞いを制御するには、以下の2つの項目を設定します。
1. Limitsタブで基本的な衝突判定の平面や衝突時の振る舞いを設定する
2. 必要に応じて、2番目の入力で衝突対象のSOPを追加する
基本的な動作を確認するために、元の3ノードに戻した上でGrid SOPとSort SOPの間にPoint SOPを挿入し、各ノードのパラメータを以下の画像のように設定します。これで、パーティクルがy軸の下方向に落ちていくような動作となります。
次に、Limits>Limit Planeにてlimitnegy = 0とし、ZX平面を地面と見立てます。さらに、Hit Behavior(衝突時の振る舞い)をBounce on Contactとすることで、パーティクルが地面で跳ね返る様子が見られると思います。ここで、Gain Normal = 0.5とすると反発が小さくなります。Gain Normalは垂直方向への跳ね返り度合いを表す係数です。Drag = 0に設定した上でGain Normal = 1とすると、パーティクルは元の高さまで跳ね返ります。大きな値を設定することで段々跳ね返りが大きくなるような挙動にすることも可能です。
一方、Gain Tangentでは接線方向(この場合はパーティクルのZX平面成分)のゲイン、別の見方をすれば地面から受ける摩擦を調節できます。Drag = 0に設定した上でGain Tangent = 1とすると、同じ間隔で跳ね返り続けます。また、0にすると同じ場所で跳ね返り続けるようになり、負の値を入れると跳ね返る度に進行方向が反転するような(不自然な)動きになります。ここでは、Gain Normal・Gain Tangentともに1のままにしておきます。
前述の通り、2番目の入力に衝突対象のSOPを入力することもできます。その際、SOPの法線方向は跳ね返りの挙動に影響しません。ポリゴンの裏面でも問題なく跳ね返ります。
なお、Split以下を変更することで、パーティクルが衝突した時や消失した時に分裂させることもできます(本記事では割愛します)。
Point Attributeの利用
ここまでパーティクルの動作に影響を与える要素について説明してきました。ここからは、送出点に対してポイントアトリビュートを設定する作例を通して、パーティクルにバリエーションを与える方法を解説します。
まず、スタートラインとしてText SOP・Sprinkle SOP・Particle SOPからなる以下のネットワークを作成します。
pscale
個々のパーティクルの大きさを変えたい場合、Geometry Instancingを用いてSphere SOPを複製してそのscaleを設定する方法がありますが、別にパーティクル形状が立体的でなくても良いならばpoint spriteとpscaleアトリビュートを併用する手法も有効です。
先ほどの状態のParticle SOPの後段にGeometry COMPを繋ぎ、Camera COMPとRender TOPを出してレンダリングします。次に、Line MATのパラメータを図のように設定し、Geometry COMPのMaterialに割り当てます(Line>Draw LinesはONのままでも表示は変わりませんが、気持ち的にOFFにしました)。このように、Point Spriteに円形のテクスチャを張り付ける場合には、Point Sprite MATとCircle TOPを使わなくてもLine MATひとつで代用できます。
次に、放出元であるグリッドの各点にpscaleアトリビュートを設定します。Houdiniを使われる方にはお馴染みのアトリビュートですが、TouchDesignerでも利用できます(Attributeのページに記載があります)。
Pointアトリビュートを設定するには、主にPoint SOPを使う方法とCHOP to SOPを使う方法があります。Script SOPでコードを書く事によって設定することもできますが、一般的な用途ではそこまでするメリットはあまり無いでしょう。
Point SOPを用いた設定方法
Sort SOPの後ろにPoint SOPを繋げて、Customタブを以下の図のように設定しますPoint>Keep ScaleをAdd Scaleに変更した上で、pscaleパラメータを図のように入力します。ここで、tdu.rand()は引数(Seed値)の値に応じて0~1のランダムな値を返す関数、me.inputPoint.indexは入力ポイントのポイント番号(インデックス)を表すPython表現式です。つまり、tdu.rand(me.inputPoint.index)とすることで、各ポイントごとに0~1のランダム値を持つpscaleアトリビュートを持たせることができます。Point SOPにSOP to DATを繋げてみると、pscaleアトリビュートが持つ値を確認できます。この方法は、ノード数が少ないシンプルなネットワークにすることができますが、数式で表現する必要があるため複雑なことをしようとすると結構大変です。
CHOP to SOPを用いた設定方法
Noise CHOPを作成し、以下の図のように設定します。これで、グリッドの点の数と同じサンプル数を持ち、0~1のランダムな値を取るpscaleというチャンネルができます。Sort SOPとParticle SOPの間にCHOP to SOPを挿入し、作成したpscaleをSOPのpscaleというアトリビュートに割り当てます。この際、既存のアトリビュートに無い新しい名前を入れてあげると自動的にその名前のアトリビュートが作成されます。ネットワーク作成時にパーティクルの送出が止まってしまった場合には、Particle SOPのState>ResetのPulseボタンを再押下してください。この方法は、CHOPベースでアトリビュートの値を設定できるため、直感的に調節しやすいというメリットがあります。本記事では、こちらの方法を使って進めていきます。
なお、Line MATを用いる場合、pscaleではなくwidthという名前のアトリビュートでも動作します。本来、widthアトリビュートはLine MATで(文字通り)線の幅を調節する機能を持ちますが、点に対しても有効です。今回はお好きな方で構いません。
この段階で、全体のネットワークとレンダリング結果は以下のようになっているはずです。
mass
大きさの異なるパーティクルが出るようになったので、massアトリビュートを使って大きなパーティクルほど質量が大きい(重い)状態にしてみます。
先ほど作成したMath CHOPを分岐し、Rename CHOPでチャンネル名をmassとし、Merge CHOPでマージします。次にCHOP to SOPで作成したmassをSOPのmassアトリビュートに割り当てます。その状態でParticle SOPのParticles>add Mass AttributeをONにして、Reset Pulseを押せば完了です。これで、パーティクルの大きさ(0~1)に対応した質量(0~1)を持つことができました。
この状態でレンダリング結果を見てみると、「場から受ける力」に記載した通り、大きなパーティクルほど早く落ちることがわかります。今回作成したRename CHOPの前段にさらにMath CHOPを置いてmassのレンジを変えることで、落ちるスピードの具合を調整することも可能です。
なお、本記事の前半でmassアトリビュートと同時に説明したdragアトリビュートですが、これは空気や水などの媒質から受ける力に関する係数であり、個々のパーティクルで異なる値を設定するのは自然ではない(全ての粒子が同じ値であるのが普通)であるため、ここでは設定しません。
Cd
放出点に対して頂点カラー(Cd)を与えることで、そこから射出されるパーティクルに色をつけることができます。
Ramp TOPでグラデーションを作成し、Null TOPを介してTOP to CHOPでCHOPデータに変換します。今回はアルファチャンネルを使わないので、TOP to CHOPのImage>Alphaは空白にしておきます。
次に、作成したTOP to CHOPとMath CHOPを図のようにLookup CHOPに接続し、そのOUTをMerge CHOPに入力します。そして、CHOP to SOPで新たに加わったr, g, bチャンネルを、それぞれCd(0), Cd(1), Cd(2)に割り当てます。 こうすることで、小さいパーティクルほどRampの左側の色(暗い青)に、大きいパーティクルほどRampの右側の色(白)になります。
ここで、Ramp TOPとNull TOPの間にHSV Adjust TOPを挿入し、HSV Tweak>Hue OffsetにLFO CHOPの値を参照させることで色合いに時間変化を加えてみます。
こうすることでレンダリング結果でのパーティクルの色が少しづつ変化しますが、パーティクルが落ちている間は色が変わりません。つまり、送出点に頂点カラーを設定した場合、(後段で上書きしない限りは)パーティクルが送出される瞬間の頂点カラーを保持し続けるという動作になります。今回の実装ではpscaleとmassは固定、つまり同じ送出点からは常に同じ大きさ・質量のパーティクルが発生し続けますが、これらにも同様の時間変化を与えることが可能です。
最終的なネットワークは以下のようになります。
まとめ
今回の記事では、パーティクルの送出点に関するポイントアトリビュートを中心に、いくつかのTIPSを紹介しました。一方で、Particle SOPの後段でパーティクルそのものに直接ポイントアトリビュートを付与することももちろん可能です。どちらにするのが良いかはアトリビュートの種類にも依存するため、一概には言えません。今回のケースでは、pscaleはどちらでも大差ありませんが、massは送出点に付与しないと想定通りの動作にはなりません。また、Cdに関しては、行いたい演出によってどちらでアトリビュートを設定するべきかが変わってくるでしょう。
パーティクルは大量に扱った方が華があるため、GLSLが書ける人はついGPGPUでの実装に目が行きがちなところがありますが、今回の記事では触れられなかったForce・Surface Attractor・パーティクルの分裂(Split)・アトリビュートのGLSL内での利用等を組み合わせると、Particle SOPベースでもまだまだ複雑で面白い表現ができる可能性があるのではないかと個人的には感じています。























