SPARKとは
3Dグラフィックスの世界の用語の1つに、パーティクルシステムというものがあります。これはパーティクル(粒子あるは小片)と呼ばれる小さなオブジェクトを大量に取り扱うプログラムで、主にゲームなどで光や炎・煙なんかのエフェクトに使われているものです。Mayaや3dsMaxなどの商用3Dグラフィックスソフトや、ゲームエンジンのUnityなどにも組み込まれています。
私は前々からちょっとこのパーティクルシステムというものを試したいと思っていましたが、Mayaなどの商用ソフトはお値段が趣味でやるにはちょっと暴力的で、Unityは良さそうなのですが、新たに覚えなきゃいけないことが多そうで(偏見で申し訳ない)、最終的にOSSを漁ることにしました。
結果見つけたのがSPARKというC++のライブラリで、zlib/libpngライセンスで公開されているものでした。これでどんなものが作れるかというと、デモの画像をご覧いただいた方が分かりやすいです。
もうちょっと特徴を述べますと、
- OpenGLでの描画をサポートしている
- 簡単な物理演算が行える
- パーティクルどうしの干渉をシミュレートできる(もちろん干渉しないようにもできる)
- 面や球といったオブジェクトによる移動制限ができる
- 重力(というより、任意の方向への加速度)を設定できる
- 反発係数と摩擦係数をサポート
- メモリの管理をやってくれる
- 最終リリースは7年前
ぶっちゃけ他のライブラリを試していないのでその良し悪しは語れませんが、更新がすでに止まっていることを除けば、ぱっと目に付くバグもなく、よく練られたクラス設計と十分な数のデモプログラムが用意されていたので、結構簡単に扱えるようになりました(使いこなせているとは言っていない)。C++プログラマの方で、CGに興味があるという方にはおススメです。
それじゃあいろいろ作ってみようかなとも思ったのですが、最近はC++でコーディングするのが大分つらく感じてきてやる気が出ない……(C++11以降のおかげで以前より100倍書きやすくなったと思いますがそれでもだるい)
というわけで、Pythonへのバインディングを作ってみました。それが今回ご紹介するpyspkです。
pyspkとは
C++のライブラリであるSPARKを、同じくC++のライブラリであるBoost.Pythonを用いてPythonから使えるようにラッピングしたものです。
SP AR Kという感じのネーミングです。pysparkは某Apacheなメジャープロダクトと被るため、回避せざるを得ませんでした。
特徴としては
- C++で使えるクラスと関数はほぼエクスポートしてある
- 使用感もだいたいそのまま
- デモプログラムは93%(13/14)移植済み
- ライセンスはSPARKと同じzlib/libpngライセンス
- できたてほやほや超Unstable
本当にただのラッパーなので、単体での使い勝手は特によくなってはいませんが、他のOpenGL系ライブラリと連携は非常にやりやすくなっています。pyopengl, SDL, pyglet等々、お好みのライブラリと組み合わせてお使いいただけます。
サポート範囲
現状、下記の範囲では概ね動作することを確認しました。
- Pythonのバージョンは3.5以上
- OSはWindows10とLinux(Ubuntu系)
- x64アーキテクチャのみ
もしこれ以外の環境で試したい方は応相談となります。ご連絡ください。Macとかは持っていないのでなかなか厳しいのですが。
インストール方法
さて、ここまでお読みいただいて、ちょっと試してみようと思った方が-e^iπ人以上いるかどうか分かりませんが、導入方法を書いておきます。
純粋なPythonライブラリではpip install なんちゃら
で済むところなのですが、元がC++なので、手順が余分に必要になります。
まず、依存するライブラリとして以下のものが必要です
- 本当に最低限必要なもの
- Boost(C++ ライブラリ)
- デモを動かすために必要なもの
これらを自分でビルドできるC++の剛の者はどんどんやっちゃっていただいて構いませんが、私を含む一般人はとてもやっていられないので、ビルド済みパッケージを導入するのが楽です。
今回はAnacondaまたはMinicondaを導入しての、Condaリポジトリからのインストール方法をご紹介します。(というかすみませんが、これ以外の方法はろくに試しておりませんです)
まずAnacondaあるいはMinicondaをどちらも導入されていない方はこちらの記事をもとにインストールしてみてください。便利ですよ。特に今回のように素性の知れないライブラリを試すような場合、うまくいかなかったら仮想環境ごとポイすればよく、メイン環境を汚さずに済みます。
Linuxの場合、先に以下のコマンドが必要になります。これはOpenGLのライブラリが入っていない場合にそれを導入するものです。
sudo apt install libglu1-mesa-dev mesa-common-dev
次にconda-forgeチャネルからパッケージをダウンロードできるようにします。conda-forgeチャネルはデフォルトで使用可能なチャネルではありませんが、今回は必要なライブラリのうち、SDL2を簡単に導入するために使用することにします。
conda config --add channels conda-forge
そして以下のコマンドで仮想環境とBoostをインストールします
conda create -n spk python=3.6
activate spk # Linuxならsource activate spk
conda install boost=1.66
Pythonのバージョンは3.6(3.5でもいいです)とします。
Boostのバージョンは1.66とします。この記事を書いている時点での最新は1.67ですが、Windowsでバグがあってビルドと動作検証が大変だったので、回避しました。
そしてこちら(私のウェブサイト)からpyspkのビルド済みパッケージをダウンロードし、pipでインストールします。
pip install pyspk-0.1.0-cp36-cp36m-win_amd64.whl # WindowsでPython3.6の場合
エラーが出ないことを願いますが、正直どうなるかだいぶ不安です。
最後に他のライブラリを入れて完了です。
conda install sdl2 pysdl2 pyopengl
あと、途中のconda config
のコマンドの効果は永続なので、以降conda-forgeのパッケージをインストールしたくない場合は
conda config --remove channels conda-forge
でconda-forgeチャネルの登録を削除するようにしてください。
デモの実行
奇跡的にインストールに成功した前提で書きますが、デモは以下の手順で入手・実行できます。
git clone https://github.com/chromia/pyspk
cd pyspk/demos
python explosiondemo.py # これは冒頭の爆発エフェクトのやつ
同じディレクトリにいろいろあるので遊んでみてください。
使い方の簡単な説明
自分でもSPARKの詳細なところまでは理解できているわけではないので、部分的な解説になると思いますが、説明しておきたいと思います。以下の説明はC++でSPARKを直接使用する際にも役立ちます。
SPARKにおけるパーティクルシステムの構造は、以下のようになっています。
- 一番上がSystemで、Systemは1個以上のGroupを持ちます
- Groupは1個のModelと1個のRenderer、そして0個以上のModifierと0個以上のEmitterを持ちます
- この図に出てくる要素以外にZoneと呼ばれる図形クラスがあります
下位の要素から順番に説明していきます
ゾーン
3D空間上で図形を表すプリミティブなクラスです。以下の7種類があります。
Zone | 説明 |
---|---|
Point | 点 |
Line | 線 |
Ring | 円 |
Plane | 平面 |
Sphere | 球 |
Cylinder | 円柱 |
AABox | 軸に平行な直方体 |
エミッタ
パーティクルを生成するものです。以下の5種類があります。
Emitter | 説明 |
---|---|
StaticEmitter | 止まった状態のパーティクルを生み出す |
StraightEmitter | 決められた方向に進むパーティクルを発射する |
RandomEmitter | 全球360°完全にランダムな方向にパーティクルを発射する |
SphericEmitter | 指定した投射角の範囲でランダムに打ち出す(汎用) |
NormalEmitter | よくわかんにゃい |
初期状態では原点(0,0,0)から発射しますが、setZone関数でZoneを指定することにより、そのZone内の任意の位置(ランダム)から発射させることができます。
さらにsetFlow関数で打ち出す勢い[particle/sec]を、setForce関数で打ち出す速度を調整できます。
モディファイア
パーティクルの動きに干渉する概念要素です。以下の8種類があります。
Modifier | 説明 |
---|---|
Collision | 使うとパーティクルどうしが衝突するようになる |
Destroyer | 条件を満たすパーティクルを消滅させる。条件はZoneの内/外など6種類から選択できる |
LinearForce | 条件を満たすパーティクルに指定の力を加える |
ModifierGroup | 複数のModifierをまとめる(使用するModifierの数が多い場合は、CPUコスト軽減に効果があるかもないかも) |
Obstacle | パーティクルに干渉する障害物を置ける |
PointMass | パーティクルを引き寄せるあるいははじき返す場として働く |
Rotator | パーティクルの向き(ベクトルではなく描画上の向き)を回転させる(らしいがまだ試していない) |
Vortex | 渦状の流れを形成し、パーティクルを引き寄せる |
Collision, Rotator, Vortex以外はZoneを指定することができ、その効果範囲はZone依存になる。
なかなか難しいですが、別に使わなくてもいいので、無理に覚える必要はないです。
レンダラ
実際にパーティクルをお使いの画面に表示するための要素。
OpenGL向けのレンダラはpyspk.GLパッケージにまとめてあります(そしてそれ以外には無い)。
4種類あります。
Renderer | 説明 |
---|---|
GLPointRenderer | 点を描画するためのレンダラ。ただしPoint Spriteを利用してテクスチャ付きのパーティクルを使用することもできる |
GLQuadRenderer | ポリゴンを利用してパーティクルを描画するためのレンダラ。もちろんテクスチャも使える |
GLLineRenderer | 点ではなく線を描画するためのレンダラ |
GLLineTrailRenderer | 一定時間の移動軌跡を線として描画するためのレンダラ |
GLPointRendererとGLQuadRendererはほぼ同じように扱えますが、GLPointRendererはサイズや向きが固定になってしまうので、パーティクルによって大きさをバラつかせたいという場合や向きをクルクル変えたいという場合には、GLQuadRendererを使用します。一方、大きさ向き均一のパーティクルを大量に扱う場合には、GLPointRendererの方がパフォーマンス的にはいいはずです(未測定ですが)。
モデル
SPARKの根幹を成しているのはここだと思います。
全てのパーティクルが大きさも形も色も同じという場合はなくはないでしょうが、微妙に違う方が自然っぽさを感じることができるでしょう。それぞれの属性を可変/固定にしたり、可変であれば初期値は指定値を使うのかそれともランダムに決めるのか、変化のさせ方は線形に変化させるか、それとも補間を使って変化させるか ---- 等々を決めるのがモデルの役割です。
モデルとして、パーティクルに持たせられる属性は以下の種類があります。
MODEL_FLAG | 説明 |
---|---|
FLAG_RED | パーティクルの赤チャネル値[0.0-1.0] |
FLAG_GREEN | パーティクルの緑チャネル値[0.0-1.0] |
FLAG_BLUE | パーティクルの青チャネル値[0.0-1.0] |
FLAG_ALPHA | パーティクルのアルファチャネル値[0.0-1.0] |
FLAG_SIZE | パーティクルの大きさ |
FLAG_ANGLE | パーティクルの(ビルボードとしての)方向 |
FLAG_MASS | パーティクルの質量(摩擦係数との兼ね合いで落下速度の変化に影響) |
FLAG_TEXTURE_INDEX | テクスチャ番号。1枚のテクスチャ画像を複数に分割した際の内部番号 |
FLAG_ROTATION_SPEED | よくわかんにゃい |
これらのうち、使用したいものをEnable, 変化させたいものをMutable, ランダムに変化させたいものをRandom, 補間変化させたいものをInterpolatedとして、それぞれ各FLAG_*の論理和を設定します。
例えば、赤緑青の色を固定にしつつ、アルファ値だけをだんだん減らしてフェードアウトしたいような場合は、
enable = FLAG_RED | FLAG_GREEN | FLAG_BLUE | FLAG_ALPHA
mutable = FLAG_ALPHA
model = Model.create(enable, mutable) # 省略したrandomとinterpolatedはFLAG_NONEがセットされる
model.setLifeTime(4.0, 4.0) # 生存時間は4[秒]とする
model.setParam(PARAM_RED, 1.0) # 赤い色にする
model.setParam(PARAM_GREEN, 0.0)
model.setParam(PARAM_BLUE, 0.0)
model.setParam(PARAM_ALPHA, 1.0, 0.0) # 誕生時(0秒)のAlpha⇒消滅時(4秒後)のAlpha
このようにフラグを設定してモデルを作り、setParam関数でそれぞれのパラメータ毎に固定値、あるいは変化させたい範囲などを指定することにより、いい感じにブレる動きを作ります。なおsetParam関数の第一引数にはPARAM_*という定数でパラメータの種類を指定しますがFLAG_*とごっちゃになりがちなので注意(値が違うので混在するとうまく動きません。危険)
グループ
配下のモデル・レンダラ・エミッタ・モディファイアを管理し、大量のパーティクルを操る、できる系中間管理職みたいな存在がグループです。
コードとしてはsetRendererやaddEmitterといった、部下をセットする関数を呼び出したり、重力や摩擦係数などの物理パラメータを設定したりする程度です。
システム
SPARKとしては最上位の概念です。1個以上のグループを持ちます。
update関数でシステム全体の時間を進め、全パーティクルの状態を更新することができます。
render関数で全パーティクルを描画することができます。
コーディング例
おおよその使い方を学ぶには各デモはいい例なのですが、大体300行程度とちょっと長い(キー入力とかの処理が多い)ので、100行程度のサンプルを用意しました(テクスチャ読み込みの関数は別ファイルで)。
雪の結晶のテクスチャを貼ったパーティクルをちらほら降らせるサンプルです。
なお、この画像はこちらのものを加工しました。
システム構成はモディファイアがなく、エミッタも1個という、ごくシンプルな構成になっています。
以下のように初期化します。
# init SPARK
# create Renderer
renderer = spk.GL.GLQuadRenderer.create() # type: spk.GL.GLQuadRenderer
renderer.setTexturingMode(spk.TEXTURE_2D)
renderer.setTexture(texture)
renderer.setTextureBlending(gl.GL_MODULATE)
renderer.setBlending(spk.BLENDING_ADD)
renderer.enableRenderingHint(spk.DEPTH_WRITE, False)
spk.GL.GLPointRenderer.setPixelPerUnit(45.0 * math.pi / 180.0, self.height) # adjust to screen size
# create Model
model_enable = spk.FLAG_RED | spk.FLAG_GREEN | spk.FLAG_BLUE | spk.FLAG_ALPHA | spk.FLAG_ANGLE
model_mutable = spk.FLAG_RED | spk.FLAG_GREEN | spk.FLAG_ANGLE
model_random = spk.FLAG_RED | spk.FLAG_GREEN | spk.FLAG_ANGLE
model = spk.Model.create(model_enable, model_mutable, model_random)
model.setParam(spk.PARAM_RED, 0.2, 0.5, 0.2, 0.5)
model.setParam(spk.PARAM_GREEN, 0.5, 0.8, 0.5, 0.8)
model.setParam(spk.PARAM_BLUE, 1.0)
model.setParam(spk.PARAM_ALPHA, 1.0)
model.setParam(spk.PARAM_ANGLE, 0.0, 2.0 * math.pi, 0.0, 2.0 * math.pi)
model.setLifeTime(4.0, 4.0)
# create Emitter
emitter = spk.StaticEmitter.create()
emitter.setZone(spk.Sphere.create(spk.Vector3D(), 5.0), True)
emitter.setFlow(10) # particle production speed [particle/sec]
emitter.setForce(1.0, 2.0) # emission power[min, max]
# create Group
group = spk.Group.create(model, 100) # max particle
group.addEmitter(emitter)
group.setRenderer(renderer)
group.setGravity(spk.Vector3D(0.0, -1.0, 0.0)) # falling acceleration[unit/sec^2]
# create System
system = spk.System.create()
system.addGroup(group)
self.system = system
そして一定周期でupdateを呼び出します
self.system.update(deltatime)
updateで更新したらrenderを呼び出します。OpenGLのコマンドと合わせて以下のようにします。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
# camera configuration
gluLookAt(10.0, 0.0, 0.0, # pos
0.0, 0.0, 0.0, # to
0.0, 1.0, 0.0) # up-vector
# draw particles
glDisable(GL_DEPTH_TEST)
glEnable(GL_TEXTURE_2D)
self.system.render()
# refresh display
glFlush()
実行すると以下のような画面が表示されます。
各パラメータを大きく変えてみて何が変化するかを確認してみると、理解が深まっていいかと思います。
その他
Python用のドキュメントはありませんが、頑張ってC++のドキュメントをコピペしてdocstringを作ったのでそちらをご覧ください。ちなみにC++のドキュメントはこちらのSPARK-1.5.5_SDK.zip
というファイルに含まれています。
SPARKをBoost.Pythonでラップするにあたり、作業量が多すぎて諦めかけましたが、pyppというステキなライブラリ(C++のヘッダからBoost.Pythonのコードが大体生成できる)に出会うことができたため、なんとか完成にこぎつけられました。感謝。
何かご意見ご要望不具合苦情等あれば、コメント欄あるいはgithubのissueなりメールなりでお願いします。