2010年10月12日火曜日

[LSL] 位置と回転について ~ ルートプリムと子プリムたち(2) ~

前回はルートプリムからのローカル座標を使った子プリムの相対位置の算出までご紹介しました。今回は llSetLinkPrimitiveParams の PRIM_ROTATION で指定するローテーションの指定方法をご紹介します。

ですが、、、最初にいっておきます。2010年10月の段階では llSetLinkPrimitiveParams の PRIM_ROTATION の指定には Bug (本当は Bug じゃないかもしれませんが、実装上の不具合というか混乱) があって、適切だろうというローテーション型の数値を渡してもうまく動かないのです。

[追記] コメントいただいたように PRIM_ROT_LOCAL を使うことですべてうまくいきます。    

[JIRA] llSetPrimitiveParams PRIM_ROTATION and llSetRot incorrectly implemented for child prims

回避方法はあります。といっても上記でいう incorrectly implemented (正しくない実装)を使うわけです。ですから将来的にこの回避方法が動かなくなる可能性(期待しない動きにならないこと)があることをご了承ください。


ポイント4 PRIM_ROTATION にはクセがあります

まぁ、、2007年からある問題ですので、ちょっと放置気味ですが、、。(いまさら仕様変更できないのかもしれませんし、JIRA を見るかぎりでは、ほぼみんなあきらめ気味です、、、)

そもそも仕様通りに動かないので、本来あるべき方法を説明しても仕方ないのですが、本来は、llSetLinkPrimitiveParams の PRIM_ROTATION で設定されるべきクォータニオンはルートから見た場合の相対的な回転です。その計算の仕方はグローバル座標軸上における設定すべきクォータニオンから、ルートプリムのローテーションを引いたもの、つまり除算したものになります。
[追記] ルートおよび子プリムのグローバルローテーションによる除算のため、グローバル基準を元にした相対位置となります。そのためもう一度ルートのローテーションで除算しています。これを回避するために、PRIM_ROT_LOCAL を使い、ルートプリムを基準とした相対ローテーションを取得することができるようになりました。

ですが、それを設定しても期待とおりに回転しません。

回避方法は、設定すべき子プリムのローテーションを、ルートプリムのローテーションで2回除算するのです。

なぜか、、、って考えない、考えない。(笑   このエントリーの最後のほうに私なりに考えた「こういう意味?」をまとめました。
また、この方法は llSetPos の wiki の説明でも記載されています。

簡単な例を使ってみてみましょう。
 rotation1
下の平べったい直方体がルートプリムです。直方体の上方向に立方体をリンクしました。これが子プリムになります。この子プリムの立方体を llSetLinkPrimitiveParams の PRIM_ROTATION を使ってまわしてみます。このときに、ルートプリムの直方体を傾けても、期待する動き(回転)をするようにスクリプトを組むのが目的です。

まず、失敗例から。スクリプトは以下になります。
タッチするとローカル Z 軸 45 度立方体が回る、というものです。子プリムのローカル Z 軸なので、期待する動きとしてはルートプリムの直方体が傾いても、直方体にのった状態でくるくる回る感じです。


回転する度に子プリムには自分のローカルローテーションを取得して、llMessageLinked を使い送信、ルートではそのレスポンスをもらったら、子プリムの上に計算したローカルローテーション と llGetLocalRot の値の両方を表示するというものです。

rotation q;
default{
    state_entry(){
        q = ZERO_ROTATION;
    }
    touch_start(integer total_number){
        rotation childRot = llList2Rot(llGetLinkPrimitiveParams(2,
                 [PRIM_ROTATION]),0);     
        vector targetEluer= DEG_TO_RAD*<0.0,0.0,45.0>;
        rotation targetQ = llEuler2Rot(targetEluer);
        q = targetQ*childRot/llGetRootRotation(); //ダメなサンプルですよ!
        llSetLinkPrimitiveParams(2,[PRIM_ROTATION,q]);
        llMessageLinked(LINK_SET,10,"","");
    }
    link_message(integer snd_num,integer num,string str,key id){
        if(num==100){
            llSetLinkPrimitiveParamsFast(2, 
                [PRIM_TEXT,(string)q+"\n"+str,<1.0,1.0,1.0>,1.0]);
        }
    }
}

結果は、ルートに傾きがなければうまくうごきますが、ルートが傾くと以下のようになります。
rot10

そこで、q の計算を


q = targetQ*childRot/llGetRootRotation()/llGetRootRotation();

のようにルートプリムのローテーションで2回除算する、に変えると、ルートが傾いても 45度 ローカルZ軸を中心に回ります。

rot11

画像では小さいですが、ローカルZ軸でまわっていない方の計算したローカルローテーションと、子プリムからもらったローカルローテーションは一致しています。うまくまわっている方は子プリムからもらったローカルローテーションと計算したローカルローテーションが違うのです。たぶん、これが多くのユーザーから Bug と言われる所以でしょう。

つまり、llSetLinkPrimitiveParams で設定したローカルローテーションと、子プリムが回転後に取得したローカルローテーションが違うのです。この例だと「何が問題?」になりますが、たとえば、ある子プリム A の向きと同じ方向を他の子プリム B に適用しようとしたとき、子プリム A のスクリプトでローカルローテーションを取得し、それを使って llSetLinkPrimitiveParams の 子プリムB の PRIM_ROTATION に受け渡してもダメ、、、ということなんです。これは相当悩みます。この場合は、取得したローカルローテーションを、ルートプリムのローテーションで1度だけ除算する必要があります。

ただ、、、これまでの球体で考える空間と位置指定の概念を使ってみると、この2回ルートのローテーションで子プリムのグローバルローテーションを割る、という意味が見えてきます。
子プリムのローテーションをルートプリムのローテーションで除算すると、子プリムとルートプリムの移動量の差分が算出されます。この差分をさらにルートプリムのローテーションで除算するということは、移動量の「基準」をグローバル座標軸に合わせる(ルートを無回転状態にする)、ということです。


つまり、東京からニューヨークの移動量を、東京を緯度0・経度0にしたときの移動量に変換しているのですね。こうするとルートがどの地点にあっても(どんな回転をしていても=東京でなくても)、同じような方向、距離であるルートからの移動量は絶対値的に表現することができます。そうすると、この2回除算するというのは非常に意味のあることだと考えられます。
まぁ、、、最大のハマリポイントでもありますね。

さて、次回はプリムではなくて、椅子(もしくはプリム)に座ったアバターを動かしてみます。

[追記] PRIM_ROT_LOCAL を使うことで上位のように2回の除算をする必要がなくなります。
サンプル スクリプトは以下になります。


rotation q; 
rotation childRot;
default{
    state_entry(){
        q = ZERO_ROTATION;
    }
    touch_start(integer total_number){
        childRot = llList2Rot(llGetLinkPrimitiveParams(2,
                 [PRIM_ROT_LOCAL]),0);      
        vector targetEluer= DEG_TO_RAD*<0.0,0.0,45.0>;
        rotation targetQ = llEuler2Rot(targetEluer);
        q = targetQ*childRot;
        llSetLinkPrimitiveParams(2,[PRIM_ROT_LOCAL,q]);
        llMessageLinked(LINK_SET,10,"","");
    }
    link_message(integer snd_num,integer num,string str,key id){
        if(num==100){
            llSetLinkPrimitiveParamsFast(2, 
                [PRIM_TEXT,(string)q+"\n"+str,<1.0,1.0,1.0>,1.0]);
        }
    }
}


プリム上に表示されるローカル ローテーションも同じものになります。
子プリムのサンプル スクリプトは以下になります。


default
{
    state_entry(){
    }
    link_message(integer send_num,integer num,string str,key id){
        if(num==10){
            llMessageLinked(LINK_SET,100,(string)llGetLocalRot(),"");
        }
    }
}

3 件のコメント:

  1. ご無沙汰してます。
    回転について以前から大変悩んでたのですが今回の連載でいろいろ勉強になりました。
    丁寧な解説記事ありがとうございました。

    ところで、2回除算の件は、Wiki等からワタシなりの解釈ですが、
    (1) 子プリムの場合「ローカル座標系/ルートの回転」で設定する
    (2)ローカル座標=「グローバル座標/ルートの回転」で変換する
    ということではないでしょうか?
    こう考えるとPRIM_ROTATIONで取得した値はグローバルなので、設定のとき2回除算が必要ってことになります。
    さっき、Wikiを読み返したら「PRIM_ROT_LOCAL」っていうのを見つけましたが、これって前からありました?
    これを使うと
    rotation rot = llList2Rot(llGetLinkPrimitiveParams(2,[PRIM_ROT_LOCAL]),0);
    でローカル回転が取得でき、
    llSetLinkPrimitiveParams(2,[PRIM_ROTATION, rot / llGetRootRotation()]);
    または、
    llSetLinkPrimitiveParams(2,[PRIM_ROT_LOCAL, rot]);
    で設定するといいみたいです。

    返信削除
  2. Jinkoさん、おひさしぶりです^^
    あれれ、PRIM_ROT_LOCAL ってありますね。
    ありがとう~。

    返信削除
  3. 追加です~
    PRIM_ROT_LOCAL ってスクリプトのエディタで青い文字にならないから新しいものですね。きっと。
    注意しなきゃいけないのは、PRIM_ROT_LOCAL を使ってllRot2FwdやllRot2Leftには使えないことかも。
    グローバルのローテーション(PRIM_ROTATION)を取得して使わないとだめ、、、。そのときの子プリムの前後、だから当たり前なんですけどね。

    返信削除