MayaのnParticleで、ある程度シミュレーションして初期状態を作成する時、困ったことが起こります。
「Fields/Solvers > Initial State > Set for Selected」(日本語のメニューは知らん!)を実行し初期状態を保存したのちアニメーション開始フレームに戻ると、位置などは保たれたままですが、ageアトリビュートにおかしな数値が入った状態になります。
2016.5、2017、2018で確認しましたがどれもこの現象が発生します。ageを正しく保持できないという事は初期パーティクルに関して、LifeSpanで消滅させたり、ArrayMapperによる評価が正しくできません。僕だけの問題?ネット検索しても引っかからないんですけど。
以下その検証と解決方法です。
検証用のパーティクルシステム一式の作成は以下のスクリプトです。
import pymel.core as pm myemitter = pm.emitter(pos=(0,0,0),type="omni",r=10,sro=0,nuv=0,cye="none",cyi=1,spd=1,srn=0,nsp=1,tsp=0,mxd=0,mnd=0,dx=1,dy=0,dz=0,sp=0) npList = pm.nParticle() np = npList[1] pm.connectDynamic(np,em=myemitter) np.ignoreSolverWind.set(True) np.ignoreSolverGravity.set(True)
毎秒10個パーティクルを発生させる単純なエミッタです。パーティクルはソルバのWindとGravityの影響を受けません。これを30フレーム分シミュレーションすると以下のような状態になります。ちなみにシーンの開始フレームは1、終了を30としています、フレームレートはntsc、つまり毎秒30フレームです。
ageアトリビュートを表示した状態です。この状態(タイムスライダが30フレームの位置)でinitial stateを保存します。そして開始フレーム(1フレーム目)に巻き戻すと次のようになります。
なんかageが増えました。期待する値は初期状態を保存した時の値です。変わってもらっては困りますよね。
なぜこの値になったか、推測するに以下のような式になっているように見えます。
1フレーム目のage = 初期状態を保存した時点でのage値 + 初期状態を保存した時刻(30フレーム) + 1フレーム秒(1/30秒)
希望としては、
1フレーム目のage = 初期状態を保存した時点でのage値
とならないと困ります。
そもそも、初期状態の保存(Set for selected)の機能は、それを実行した時点でのパーティクルの状態を保存するだけで、その状態を「何フレーム目から動作させるか」のオプション設定がありません。600フレーム計算させた後、200フレーム目からその状態をスタートさせたいとか。
とりあえずこの「開始フレームの問題」は置いておき、1フレーム目へ巻き戻した時にage値が初期状態を保存した時点でのage値になるように修正してみます。
まずは調査、初期状態を保存し1フレーム目へ巻き戻した時のageアトリビュートの値を出力してみます。
pm.Attribute("nParticleShape1.age").get()
上を実行すると、結果は以下になります。
# Result: [1.8366603236886032, 1.7477682039101212, 1.6561824774511746, 1.549161676215631, 1.4460878421672738, 1.3663259335252194, 1.2370737699174363, 1.1459848316570038, 1.0489200562323446] #
まぁおかしいです。ビューポート上の表示と精度は違いますがこの結果とほぼ同じです。
次にage0アトリビュートの値を取得します。初期状態を保存するとアトリビュート名に「0」が付加されたアトリビュートが作成されます、ageの初期値はage0です。
pm.Attribute("nParticleShape1.age0").get() # Result: [0.8699936570219364, 0.7811015372434543, 0.689515810784508, 0.5824950095489642, 0.4794211755006069, 0.39965926685855246, 0.27040710325076966, 0.17931816499033704, 0.08225338956567796] #
これが正解です。
じゃあ、なぜage値がおかしくなるのか、age0を参照して復元しないのか、バージョンを経ても修正されないのは「仕様」ってことなのでしょうか。
気を取り直してage値問題、そもそもageは読取り専用のアトリビュートです。さらに言うとage値はbirthTime値と現在時刻を参照して計算されているようです。「age = 現在時刻 – birthTime」です。
以下が保存されたbirthTime0です。なんとなくageに「-1」かけた数値です。厳密には1/30少ないですが、また後で。
pm.Attribute("nParticleShape1.birthTime0").get() # Result: [-1.8033269903552698, -1.7144348705767878, -1.6228491441178412, -1.5158283428822976, -1.4127545088339404, -1.332992600191886, -1.203740436584103, -1.1126514983236704, -1.0155867228990112] #
ageがbirthTimeと現在時刻から計算されているなら、開始フレームでageが「0.5」のパーティクルはbirthTime0は「開始フレーム(秒) – 0.5」でなければなりません。
例で使用している番号0(index = 0)のパーティクルについていうと、age値が開始フレームで「0.8699936570219364」となるにはbirthTime0は「-0.8699936570219364」であるところが「-1.8033269903552698」となっています。
という事でbirthTime0を修正します。
newBirthTime = [(x * -1) for x in pm.Attribute("nParticleShape1.age0").get()] pm.Attribute("nParticleShape1.birthTime0").set(newBirthTime)
age0の値に「-1」を乗じてbirthTime0を計算しているだけです。上を実行してタイムスライダを適当にスクラブし1フレーム目に巻き戻します。
期待している数値と近いですが、0.033ほど多いです。おそらく1/30秒(1フレーム)です。これ何でしょう?開始が1フレームだから?
とりあえずスクリプトをちょっと修正。
deltaTime= 1.0 / pm.mel.currentTimeUnitToFPS() newBirthTime = [(x * -1) + deltaTime for x in pm.Attribute("nParticleShape1.age0").get()] pm.Attribute("nParticleShape1.birthTime0").set(newBirthTime)
これで、1フレーム目で正しいageが復元されます。
スクリプトをちゃんと書くなら、
# Correct particle age attribute, which has initial state. import pymel.core as pm def correctInitialAge(particleShape): try: deltaTime= 1.0 / pm.mel.currentTimeUnitToFPS() newBirthTime = [(x * -1) + (deltaTime) for x in particleShape.age0.get()] particleShape.birthTime0.set(newBirthTime) except TypeError: print "Initial state not exist" def main(): selection = pm.selected() if len(selection) > 0: if pm.nodeType(selection[0]) == "nParticle": correctInitialAge(selection[0]) elif pm.nodeType(selection[0]) == "transform": shapes = pm.listRelatives(selection[0],shapes=True) ntList = [s for s in shapes if pm.nodeType(s) == "nParticle"] if len(ntList) > 0: correctInitialAge(ntList[0]) if __name__ == '__main__': main()
「初期状態を保存」した後、上のスクリプトを実行することで、age値の補正ができます。
正しくなりました。
ほったらかしの「開始フレームの問題」ですが、30フレームで状態を保存したものを10フレーム目から開始したい場合、birthTimeの補正をした後、所属しているnucleusのstartFrameを10.0に設定します。nucleusは10フレーム目まで初期状態を保存したパーティクルのageを更新しません、またエミッタのアップデートも止まりますので10フレーム目まで新規パーティクルも発生しません。厳密にはbirthTimeに矛盾が出ますけどね、ひとまずこれで解決した事にします。