【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
ベースでもまだまだ複雑で面白い表現ができる可能性があるのではないかと個人的には感じています。