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に矛盾が出ますけどね、ひとまずこれで解決した事にします。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です