2010年10月16日土曜日

[LSL] 位置と回転について ~ アバタ―にスクリプトはいれられない ~

前回までで、llSetLinkPrimitiveParams を使った子プリムの位置と向きの設定方法をご紹介してきました。また、Link された子プリムを扱うには、与えられた「リンク番号」が重要だ、ということもお伝えしました。
そこで、リンク番号の取得ですが、プリム同士のリンクであれば、それぞれのプリムにスクリプトを入れ、リセット後のタイミングなどで llMessageLinked を使って子プリムのリンク番号をルートのスクリプトに渡すことで、リンク番号管理をしたりします。
llGetLinkNumber() を使って自分のリンク番号をとるわけです。

ここで問題になるのが、プリムにアバタ―が座った時のリンク番号です。ん?と思われるかもしれませんが、アバタ―がプリムに「座る」という行為は、アバタ―がルートプリムにリンクされた、というイベントで管理されるのです。
スクリプトでは CHANGE イベントCHANGED_LINK を使って、アバタ―が座ったかどうかを管理しているのですね。

アバタ―には他の子プリムのようにスクリプトを入れることができません。座った後のアバタ―の位置や回転を操作するのは、リンクされた子プリムの位置と回転を操作するのと同様ですが、リンク番号の取得がプリムのようにできないのです。今回はその対応方法と、椅子に座ったアバタ―の位置や向きを変えるスクリプトを考えてみます。

5. アバタ―にスクリプトはいれられない


まぁ、、、既知、既出といえる比較的有名な話題になります。wiki にも紹介されていますし、過去にも多くの先輩諸氏がブログなどで紹介しているのですが、ポイントをまとめると

a) 多くの場合は CHANGE イベント発生時のリンク番号の最大値がアバタ―のリンク番号
b) 複数のアバタ―が座ったり立ったりする場合は、アバタ―のリンク番号は CHANGE イベントのたびに変わる可能性がある


上記の2つに注意してリンク番号を取得するサンプル スクリプトを書くと以下のようなものになります。
なお、一人しか座らせない場合は、llSitTarget と llAvatorOnSitTarget を使って、アバタ―の key の取得、llGetNumberOfPrims をリンク番号として扱うのが一番簡単です。このスクリプトはオブジェクトに複数のアバタ―が Sit する、という前提で、key や リンク番号などの情報を取得するサンプルになっています。

//--------------------------------------------------------------------------------
// Sit した複数のアバタ―のリンク番号や Key, 名前、ポジション、ローテーションを
// それぞれのリストに取得するサンプルスクリプト
//--------------------------------------------------------------------------------
list keyList;     //Sit しているアバタ―の key をいれます
list nameList; //Sit しているアバタ―の名前をいれます
list posList;    //Sit しているアバタ―のグローバルポジションを入れます
list rotList;     //Sit しているアバタ―のグローバルローテーションを入れます
list numList;   //Sit しているアバタ―のリンク番号をいれます
default{
    state_entry(){
        llSay(0, "Hello, Avatar!");
    }
   
    changed(integer change){
        if(change & CHANGED_LINK){
            integer max = llGetNumberOfPrims();
            integer i = max;
            keyList = nameList = posList = rotList = numList = [];
            for(;i>0;--i){
                list tmp = llGetLinkPrimitiveParams(i,[PRIM_NAME,PRIM_POSITION,PRIM_ROTATION]);
                key tKey= llGetLinkKey(i);
                list objDetails = llGetObjectDetails(tKey,[OBJECT_CREATOR]);
//OBJECT_CREATOR が NULL_KEY の場合はプリムではなくアバタ―です
                key cKey = llList2Key(objDetails,0);
                if(cKey==NULL_KEY){ //リンクされたものがアバタ―かどうかのチェック
                    //アバタ―だったら各情報をリストに格納します

                    tmp = llGetLinkPrimitiveParams(i,[PRIM_NAME,PRIM_POSITION,PRIM_ROTATION]);
                    keyList += tKey;
                    nameList+= llList2String(tmp,0);
                    posList += llList2Vector(tmp,1);
                    rotList += llList2Rot(tmp,2);
                    numList += [i];
                }else{
                    i=0; //アバタ―じゃなくなったら、あとはリンクされたプリムなので終了
                }
            }           
        }
        //アバタ―が座っていれば、その情報を確認用に表示
        integer len = llGetListLength(keyList);
        if(len>0){
            integer i = 0;
            for(;i<len;++i){
                llOwnerSay(llList2String(numList,i)+","+
                           llList2String(nameList,i)+","+
                           llList2String(keyList,i)+","+
                           llList2String(posList,i)+","+
                           llList2String(rotList,i));
            }
        }else{
            llOwnerSay("誰も座っていません。");
        }
    }
}
//------------------------------------------------------------------------

上記スクリプトの入った直方体に2人のアバターが Sit すると llOwnerSay で以下のようなメッセージをオーナーは受け取ることができます。

[00:28]  Object: 2,Snuma Whitfield,d19d8aef-530a-41cc-bc1c-xxxxxxxxxxxx,<210.193100, 234.464900, 2498.686000>,<0.000000, 0.000000, 0.477574, 0.878592>
[00:28]  Object: 3,Alt Beck,a45ad906-fa8e-4fe2-bb3d-xxxxxxxxxxxx,<210.193100, 236.036500, 2498.686000>,<0.000000, 0.000000, -0.934414, 0.356190>


リンク番号3番の Alt さんは一番最後に Sit したアバタ―なのですが、ここでリンク番号 2 番の私が立ち上がったとき、Alt さんのリンク番号がどうなるか。。。答えは 3番ではなく即座に2番になるのです。そのため、changed イベントが発生する毎に、アバタ―のリンク番号の再設定をしなくてはいけません。

リンク番号さえわかっていれば、あとはこれまでお話ししてきた設定方法を使って llSetLinkPrimitiveParams によるアバタ―の位置や回転の指定を行えばいいのです。
llSitTarget で指定するような初期の座る場所を指定したい、、などは上記の考えを拡張する(座った直後に移動させる)か、wiki の llSitTarget にある「着座ポイントを更新」などの応用をためしてみるといいでしょうね。

それでは複数のプリムからなる台座(オブジェクト)に、複数のアバターが座り、台座にタッチすることで表示されるダイアログで自分の位置を変える(前後、左右、上下、回転)、というスクリプトのサンプルが以下です。
ポイントは、

1) CHANGE イベント毎にアバターのリンク番号を更新する
2) ダイアログで移動させるときに、その時点での位置情報を取得し、使用する
3) グローバル座標による差をローカル座標上の差に変換する
4) アバターを回すときは、ルートのローテーションで2回除算する
(または PRIM_ROT_LOCAL を使う)
5) llSitTarget と llAvatarOnSitTarget を使わず、座りたい場所にアバターを座らせ、微調整を可能にする

といったところでしょう。サンプル スクリプトとしてわかりやすくするために一部冗長なコードがあったり、情報確認用の llOwnerSay があったり、ダイアログの扱いについてもあまり汎用的にしていませんので、その辺は臨機応変に変更してみてください。
blogSittingScript

2010年10月14日木曜日

[LSL] 位置と回転について ~クォータニオンと移動量~

前回からの続き、、、ではなくて、ちょっと閑話休題。といっても、今回の一連のお話しの中で、私が勝手に造語的に使っている「移動量」って何?の補足みたいなお話しです。

[LSL] 位置と回転について ~移動量という考え方~ のエントリーで、球体で考える空間と位置指定の概念を説明するのに、地球の話を持ってきて「移動量」の話をしていますが、この移動量っていうのが、やっぱりよくわかんない、、、と感じる方も多いと思います。

移動量(正直、これ私が勝手にそう呼んでいるので、一般的な用語ではないことをご了承ください。)はクォータニオンそのものなんですが、イメージとしては地球上のある地点から他の地点までの最短距離(大圏航路)で、ぐるっ!と地球をまわして、一度の回転でA地点からB地点にいって、回転させる軸の傾きも含めたもの、、、としつこく書いています。

ふ~ん、そうなんだ、と思ってくれるといいのですが、「ん~、わかんない」となると、実は先に進めないのも事実。それが2年前の私の状態ですから(笑
イメージ、イメージっていってもしょうがないので、もうちょっと具体的に説明してみたいと思います。ただし、なぜこれまで具体的に説明していないか、、、といえば、最終的には必要になる場合もありますが、最初のほうでは具体的に理解する必要があまりない(というか邪魔な?)知識だと思ったためです。あ、もちろん、ここでも行列計算、虚数といった数学的な話にはなりません。

前のエントリーでも「羽田空港とJFK空港を結ぶ大圏航路でグルッと回す」、それが移動量って言い方をしていますが、一気に回すための傾いた軸って存在しますよね。

クォータニオンからは、まず、その軸の方向を導き出すことができます。その関数が llRot2Axis なんです。そして軸を使ってまわした角度が llRot2Angle なんです。

Rot2AxisAngle2
( 絵心無いわ・・・わかってくれるといいけどw )

この軸の傾き(方向)と回る角度が移動量つまりクォータニオンを構成する要素なんですよね。
実際、Axis のベクターと、Angle の float からクォータニオンを計算する関数が llAxisAngle2Rot になるわけです。これでクォータニオン(移動量)を算出できるのです。

ネットや書籍でクォータニオンを調べると、そもそも LSL で使われる関数などでは説明されませんから、四元数で、実数部分と虚数部分にわかれ、、、、と説明されます。クォータニオンはローテーション型で <x, y, z ,s> の四元数にて LSL でも表現されますが、現実にその x, y, z, s の要素を直接いぢることは、最初の頃はほぼ皆無です。ちなみに2年前に私は ウィキペディアの四元数や、クォータニオンの説明を見てしまい、頭痛&挫折しそうになりましたし、wiki.secondlife.com での llRot2Axis や llRot2Angle の説明では、それらの関数と同じ動きをする llAcos や llFabs, llSqrt, llVecNorm で四元数の要素をいじるユーザー関数を紹介しています。ですが、その特記事項を見ても何を言っているのかわからなかったわけです。

実際はウィキペディアにある内容をもとにクォータニオンは計算され、四元数の各要素となって表現されるのですが、それを理解するためには数学の勉強は必須で、ちょっと敷居が高かったのです。(私にとっては、、、ですが)

このクォータニオンと軸の傾きと回転量の関係がわかっていれば、他のところでも応用可能なような気がしませんか?

回す軸の方向はわかっていて、何度まわしたい、、、ってあるでしょう?おおよそローカル軸が「回す軸の方向」なので、あまり Axis と Angle の関係を理解せずにz軸で回すなら <0.0, 0.0, PI/2> とかにして、llEuler2Rot を使ってクォータニオン(移動量)にして、現在の Rotation に左からかけたりする、、、。でも、ローカルの軸、もしくはグローバルの軸じゃなくても、llAxisAngle2Rot で任意の軸で回すことができる、、、と考えると、ちょっとどこかで使えそうな気がしません?

クォータニオンについては、最初の回で紹介したように、また、何度もしつこくいっているように「球体で考える空間と位置指定」の概念をきちんと持つことで、行列計算や三角関数を深く知らなくても、LSL の関数だけでほとんどの操作は克服できると思っています。その上のステップに行くときに、そこで必要な知識を吸収すればいいわけですよね。

さて、次はオブジェクトに座っているアバタ―を動かす(それも複数のアバタ―)です。

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(),"");
        }
    }
}

2010年10月11日月曜日

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

前回ではリージョン座標軸で表現される位置をクォータニオンと球体の中心からの距離で置き換える方法をご紹介しました。空間の概念は X, Y, Z 軸による立方体だけでなく、球体での移動量と球体の中心からの距離で表現できる「球体で考える空間と位置指定」の概念の重要性がご理解いただけたかな、と思います。

ここまででオイラー角による回転について1度も触れていないのは面白いですよね。移動量という言葉を使って「角度」という言葉を使っていないわけです。

プリムをまわす、回転させるときに一番最初に覚えるのは軸を中心とした回転ですよね。わたしもそうでした。Z軸で90度回転させる、なんてことをやるわけで、そのときラジアンによる角度表現が必要である、とか、llEuler2Rot という関数を知ったり、右ねじの法則、グローバル軸を基にした回転と、ローカル軸を基にした回転の違い、その方法を学ぶわけです。

もちろんオイラー角によるプリムの回転で事足りことも多いのですが、スクリプトでいろいろな操作をやろうとすると、はじめから角度がわかっていることって、、、少ないと思います。llGetPosllGetRotllGetObjectDetails, llGetLinkPrimitiveParams などで取れる数値は位置を表すベクター型であり、移動量をあらわすローテーション型のクォータニオンです。なので、位置ベクトルとクォータニオンを駆使して操作することになり、あらかじめ各軸の角度がわかっていれば使えるオイラー角による回転指定は、、、、実は、かぎられるような気がします。

前置きが長くなりましたが、今回は llSetLinkPrimitiveParams などで必須となるルートプリムと子プリムの関係、とくに位置(ローカルポジション)と向き/回転(ローカルローテーション)について考えてみたいと思います。

4. ルートプリムと子プリムたち

複数のプリムをリンクさせるとき、スクリプトを組み込む必要がなければ、あまりルートプリムがどれかを意識しないかもしれませんが、スクリプトをいれてなんらかの操作をしようとすると、ルートプリムがどれか、子プリムでもリンク番号は何番かが非常に重要になります。

基本的なリンク、リンクのさせかたについては wiki の記事や先輩諸氏のブログなどを参照してもらって、ルートプリムに入っているスクリプトから子プリムたちをどのように扱うかを見てみます。

ポイント1 リンクされた子プリムは「ルートだけ」とつながっている

ちょっといきなりハードルが高いかもしれませんが、プリムをリンクさせたとき、子プリムはルートプリムに対してリンクされている、ということを忘れるときがあります。1対1なら直感的ですが、子プリムの数が多くなり、造形的に階層構造のようなオブジェクトになったとき、見た目で土台になっているプリムを動かすと、それに「見た目」でのっているプリムが動きそうな気になるんですよね。

linkedprims

上の絵の緑色のルートプリムを動かせば、リンクされている子プリムたちはすべて追随するのですが、青いプリムを動かしても、見た目その上にある赤や黄色のプリムは動かない、、ということです。上のような単純なオブジェクトだと「当たり前じゃん」と思うでしょうが、造詣をしていって、見た目が子プリム同士関連しているように見えれば見えるほど、ルートだけにしかリンクしていないことを忘れます(笑

それが顕著にでるのが、プリムに座ったアバターを動かすときです。

どうしても座っているプリムを動かせば、座っている(=リンクしている)アバターも動くような気がしてしまいますが、アバターはルートにリンクしているであって、見た目座っているプリムとはリンクしていないのです。

ポイント2 子プリムの位置・回転はルートプリムが「基準」です

ですよね~、と思われるかもしれませんが、わたしも最初にはまった落とし穴は、ルートプリムのローカル座標軸(前後、左右、上下)がグローバル座標軸(東西、南北、天地)と重なっているのがおおよそ Rez した直後の初期値なので、グローバル座標軸を基準とした回転や位置指定を子プリムに対して行い、最初うまく動いている!と誤解してしまうことです。

うまく動いている状態だったのに、オブジェクトの向きを変えたら、予想もしない方向、位置に子プリムが動いた、、、となり、それが予想もつかない動きをするので悩んでしまう、、というものです。

sittingOnChair0

sittingOnChai1

ポイント3 ルートを基準とした位置の算出方法は球体で考える

正直、、、わたしは相当苦労しました。。。前に「位置と回転」について書いたブログが 2008 年のもの、、、当時はクォータニオンを理解しようとして行列計算や、関連しそうなネットの情報、書籍を読んでみて、興味深い内容が多かったものの、それぞれを理解したとはいえませんでした。

最近は wiki.secondlife.com のコンテンツも有志の方による日本語訳が充実してきているので、そこからある程度「こんなものかな」で理解してしまうほうがいいかもしれません。

ルートと子プリムの関係は、これまでご説明してきた「球体で考える空間と位置指定」の概念がわかっていれば比較的簡単に必要な数値の算出が可能になります。(なると思っています、、、)

まず、ルートから見た子プリムの相対位置です。
llGetLinkPrimitiveParams もしくは llGetObjectDetails で子プリムのグローバル座標を取得することができます。ルートプリムのグローバル座標もこの関数以外に llGetPosllGetRootPosition で取得できるので、ルート、子プリムそれぞれのグローバル座標をもとに、ルートプリムからルートのローカル座標(前後、左右、上下)による子プリムまでの「軸上の移動距離」を算出することになります。ただし、この算出の場合も「球体で考える空間と位置指定」を使うことになります。

え?子プリムのグローバル座標からルートプリムのグローバル座標を引けばいいんじゃない?と思いたくなるのですが、その差分はグローバル座標軸における差(東西、南北、天地)であって、ルートプリムからのローカル座標軸(ルートプリムからの前後、左右、上下)での差分ではないのです。グローバル軸とローカル軸が重なっていれば問題ないですが、ずれた(ルートが回転した)瞬間から予期しない動きをしてしまいます。

ルートプリムから見た子プリムのローカル座標上の移動距離を算出する考え方と手順が以下になります。

a) 球体で考える空間と位置指定の概念を使ってルートプリムを球体の中心にもってくるイメージをしてみます

そのときのルートプリムの向きは llGetRot でとったそのままの傾きで、子プリムと一緒に動いてくるイメージです。球体の中心にきた時は、ルートプリムのローカル座標軸とグローバル座標軸はずれている感じです。そしてそのずれは移動量であり、ルートプリムのローテーションになります。

//ルートプリムのクォータニオン
rotation rootRot = llGetRot(); または llGetRootRotation();

b) 中心(ルート)から子プリムまでの直線距離とグローバル座標軸上の移動距離を算出します

グローバル座標軸における2つの位置ベクトル間の距離です。

vector rootPos  = llGetPos(); または llGetRootPosition();
integer linkNum = 2; //ルートは1、それ以降は2~ 
list pList  = llGetLinkPrimitiveParams(linkNum,[PRIM_POSITION]);
vector childPos = llList2Vector(pList,0); 
float dist = llVecDist(rootPos, childPos);

グローバル座標上の子プリムの位置とルートプリムの位置の差分。つまり、ルートプリムが中心のときにグローバル座標上の子プリムの位置ベクトルになります。

vector childVec = childPos-rootPos;

c) ルートプリムをぐるりと回転させて、ルートプリムのローカル座標軸とグローバル座標軸を一致させるイメージを持ってみます

この時の移動量は a) の rootRot だけ「戻る」感じですよね。
ただし、このとき「移動」するのは実は子プリムなんです。ルートプリムは中心にいるので一切「移動」しないわけです。
なので、動かす前の子プリムの移動量を算出します。これは子プリムの向きを表すローテーションではありません。球体で考える空間の話を思い出してください。球体の中心(=ルートプリム)とX軸基準 <1.0, 0.0, 0.0> と自分のグローバル座標上の位置ベクトルから算出する移動量です。自分の位置ベクトルはグローバル座標上の XYZ であり、ルートプリムは球体の中心にきていますから、b) で算出した childVec になります。

rotation childRot = llRotBetween(<1.0,0.0,0.0>,childVec);

そして、ルートのローカル軸をグローバル軸にあわせるために、子プリムの移動量からルートの傾き分の移動量を引きます。(ルートプリムがぐるりとまわって元に戻る感じです。)ローテーションでの計算では除算します。ここがルートと子プリムの関係の最大のポイントです。

rotation q = childRot/rootRot;

これで移動量 q は、ルートのローカル軸、グローバル軸が重なった時点での球体上の子プリムの移動量となります。

d) ローカル座標の位置ベクトルを算出します

ローカル座標とグローバル座標は一致しているので、<1.0, 0.0, 0.0> と llRot2Fwd と使ったこれまでと同じ方法で算出します。

vector offset = llRot2Fwd(q)*dist;

この offset ベクターがルートプリムからの相対位置になります。
どうです?三角関数とか意識しなくても出せるような気がしませんか?
重要なのはイメージなんですよね。

と、、、ちょっと長くなったので、ルートプリムから見た子プリムの向きについては次回にします。

2010年10月10日日曜日

[LSL] 位置と回転について ~ 基準はどこにあるの? ~

前回では移動量(クォータニオン)と高さを使えば空間のあらゆる位置を表現できる、という方法を説明してみました。ただし、その場合でもある地点を「基準」として考えたら、、、という前提があったことを思い出してください。

この基準、、、というのが難しいのです。

3. 基準はどこにあるの?

地球上の位置を考えたとき、よく例にでるのがイギリスにあるグリニッジ天文台の子午線ですが、前回ではそれに赤道も加え、経度0度の子午線と緯度0度の赤道の交点がギニア湾上にある、ということを紹介しました。その位置は「緯度0度、経度0度」の位置です。

そして、その位置は地球の中心から見て「地球の半径」の高さ(あえていえば、海抜 0 m もしくは標高 0 m)を加えることで「ピンポイント」で地球の表面上の位置の特定が可能になります。

そして以下の絵でご説明したように、回転であらわした「移動量」が同じでも、地球の半径に相当する R の距離(もしくは高さ)を変えることで違う位置をさすことが可能だということも理解できたと思います。

rotation

Secondlife のインワールドで移動量と半径Rで空間内のすべての位置を表現するには、考えなくてはならない「基準」が球体の中心 <0.0,0.0,0.0> だけではないのがミソなんです。

地球でいう緯度0度、経度0度、標高 0m の地点はどこにあるのでしょうか。

これ、、、実は明確には無いのです。(今のわたしの理解では^^;)

複数の SIM からなる SecondLife なので、他の SIM をぜーんぶひっくるめて「地球」のような感覚になりがちですが、位置の特定にかぎっていえば 1 つの SIM がすべてです。この1つの SIM で、球体の中心を <0.0, 0.0, 0.0> におき、さらに、LSL で扱いやすくするために、地球上のギニア湾上にある緯度0度、経度0度、標高0m(地球の半径)のような「基準」をグローバル座標で以下の地点だと「仮定」します。

<1.0, 0.0, 0.0>

上の絵で、<0.0, 0.0, 0.0> の球体の中心点からグローバル座標の X 軸上で 1m いった地点。これを緯度0度、経度0度、標高0m と同じように扱ってみるのです。ここが絵でいうピンクの線の基準。

ええ、、、でも、たとえば自分が <100.0, 82.0, 36.0> のグローバル座標の位置にいて、それで <1.0, 0.0, 0.0> の基準点(もしくは基準ベクトル)をつかって何がわかるの?となりますよね。ただ、もう一度思い出してください。同心円(もしくは同じ球面)にいなくても、移動量(クォータニオン)は 上の図でいえば A1, A2, A3 は同じ、B1, B2, B3 も同じ、、、ということを。そうすると、半径 1.0 m の球面上で、上の絵の中の線で言えば赤い直線、青い直線上の地点への移動量がわかれば、あとはどれだけ中心から離れているかの組み合わせで、<100.0, 82.0, 36.0> の地点を示すのと同じことができるのです。

そのための便利な関数が llRotBetween() なのです。

wiki の説明では llRotBetween の引数には単位ベクトル的な数値(x や y や z が 1.0) しか入れていませんが、この関数が本領発揮してくれるのは単位ベクトル以外のベクトルをいれても、2つの位置間の「移動量」を計算してくれるのです。

上の絵でいうと、A2 と B3 の間の移動量を出してくれる、ということです。同心円上の A2 と B2 の移動量も、同心円上にない A2 と B3 の移動量は同じことはもう理解できますよね。違うのは中心からの距離、半径 R なのです。

なので、グローバル座標上の <100.0, 82.0, 36.0> を移動量(クォータニオン)と <0.0,0.0,0.0> からの距離で表すと

移動量(クォータニオン)r は、
rotation r = llRotBetween(<1.0, 0.0, 0.0>,<100.0,82.0,36.0>);

<0.0, 0.0, 0.0>から<100.0, 82.0, 36.0>までの距離は
float d = llVecMag(<100.0, 82.0, 36.0>);
または
float d = llVecDist(<0.0,0.0,0.0>,<100.0,82.0,36.0>);

となります。この r と d でグローバル座標上の位置の計算は以下で可能です。

vector v = llRot2Fwd(r)*d; //注1) なぜ llRot2Fwd か理解できない人は最後の(注1)を参照してみてください。

これで、v の値は自分の位置の <100.0, 82.0, 36.0> になるのです。

この基準の考え方を応用すると、リンクされた子プリムのルートプリムからの相対位置を計算することができます。ルートに対する相対とは「ルートから見て(ルートを中心として)」どのくらい差分があるか、ということで、特に llSetLinkPrimitiveParams() で子プリムの位置や傾きを設定する PRIM_POSITION や PRIM_ROTATION で使用できます。子プリムがルートからみてどの位置にあるか、どんな傾きになっているかを算出してはじめて llSetLinkPrimitiveParams が使えるようになるわけです。そのときルートプリムは自分が傾いているなんて考えていません。また自分がグローバル座標のどの位置にいても、「自分からみてどのくらいの距離にいるか、自分から見て前後・左右・上下のどのくらいの位置にいるか」が重要となります。

次はここまでの知識を元にルートプリムと子プリムの関係について考えてみたいと思います。

(注1)llRot2Fwd を使ったのは、基準となる位置(ベクトル)を <1.0, 0.0, 0.0> と仮定したためです。X 軸は東西であると同時に前後になります。<0.0, 0.0, 0.0> から前に1歩進んだ状態で、移動量によってくるりと回転した状態からさらにベクトルを伸ばそうとするので前に進む、なので llRot2Fwd を使います。

もし基準となる位置(ベクトル)を <0.0,1.0,0.0> としたら、<0.0,0.0,0.0>から左に一歩分横に移動したようなものです。この状態で移動量分くるりとまわし、ベクトルを伸ばそうとすると、今度は llRot2Fwd ではなく llRot2Left を使って左に進むことでベクトル算出をすることになります。

2010年10月9日土曜日

[LSL] 位置と回転について ~ 移動量という考え方 ~

前回、位置を表すには2つの方法があって、そのうちのひとつに地球を例にとって球体における位置の指定についてご紹介しました。

今回はその球体における位置指定について具体的に見ていきたいと思います。

2. 移動量という考え方

前回では成田空港と JFK 空港の位置は「緯度」と「経度」で指定できることをご紹介しました。緯度、経度が何か、、、はここでは詳しく説明しませんが、数値データで表現されるこの緯度、経度、、、それぞれの数値が 0 のところってどこでしょう?そしてそれはどんな意味を持つのか考えてみます。

lat0longi0

緯度0度(つまり赤道上)で、経度0度(つまり、イギリスのグリニッジ天文台を通過する子午線)が交わる地球上の位置は残念ながら海上です。アフリカのガーナの下、ギニア湾上にありました。(上の Google Map の 青いマーカーです)

この緯度・経度が 0 の地点から「ある移動量で動いた地点」をクォータニオンで表すことができるのです。クォータニオンはプリム/オブジェクトの「回転」で出てくる重要な要素ですが、前回ご紹介した大圏航路をつかってある地点、たとえば成田空港まで(球面上で)緯度0度、経度0度の地点から線を引く時、地球を「ぐるっ」と斜めになっている軸で回すと、それも1回の回転だけで青のマーカーを成田空港のある位置まで「移動させる」ことが想像できると思います。(今は軸が何度傾いて、、、とかは考えないようにします。地球を回すと成田空港も逃げちゃうよ、、という突っ込みはなしで^^; 気になる人は成田空港があった位置まで、、、とでも置き換えてください。)

googlemap
緯度0度、経度0度から成田空港へは地球の裏側にいくので、成田とJFKで大圏航路を。。。。

この斜めの軸でぐるっと回し移動させる傾きも含めた「量」を移動量として理解します。そして、その移動量は回転なのでクォータニオンで表現できる、、、と覚えます。
そうすると、緯度0度、経度0度を基準点とした成田空港地点への移動量(クォータニオン)は、成田空港の位置そのものをあらわしていると言えます。

なんとなく、基準点さえ決まっていれば移動量を用いて場所の特定ができそうかな、、、と思ってもらえればいいのですが。

ただ、ここで疑問が湧きます。Seondlife のインワールドではグローバル座標による位置の指定は直観的で、<24.0, 120.0, 30.0> であれば <0.0, 0.0, 0.0> から X 軸のプラス方向に 24 m 進んで、Y 軸のプラス方向に 120m 進んで、Z 軸のプラス方向に 30m 上がれば、そこが指定されている位置だということはわかります。クォータニオンを使った場合、球体の中心は、、、えっと、SIM が 256m X 256m X 256m の立方体だから、その中心は 128m x 128m x 128m、、、あ、でも高さはもっと何千メートルもあるから、、、あれ、中心ってどこだろう????と思います。私も最初のころは、これでつまづいていたんです。

クォータニオンを使った位置指定の重要なポイントは、上の疑問にあるように「球体の中心」をどこにもってくるか、、、なのですが、グローバル座標(リージョン座標)で考えるときは、そのものズバリ <0.0, 0.0, 0.0> を球の中心として考えるのです。
ん~、それですべての位置の表現ができるの?と思いますよね。クォータニオンによる位置の指定は直観的ではないので、もうすこし我慢してお付き合いください。

いきなり3次元だとつらいので、ちょっと高さは考えないで平面で移動量(クォータニオン)と位置についての関係を見てみたのが以下の絵です。

rotation 

いわゆる同心円のお話しですが、ピンクの線を基準として青い線分だけ回ったとします。青い線の回転量は R1 の円上でも、R2 の円上でも、R3 の円上でも同じですよね。ただし、R に相当する半径の長さが違うので、A1, A2, A3 は違う位置をさしている、、、ということです。赤い線上の B1 ~ B3 についても同じことが言えます。回した角度(移動量)は同じでも、半径が違えば指し示す位置が違う、ということで、地球上の高さ/標高が違う同じ緯度・経度の位置の指定方法をイメージするとわかりやすいと思います。

移動量、つまりクォータニオンと、上述の半径 R、地球上でいえば標高の組み合わせで、空間上のすべての位置を指し示すことができる、ということになります。

グローバル座標では <0.0,0.0,0.0> を球体の中心として考えてもよさそう、、、と思いますが、上の例でのピンクの線の「基準」はいったいどれか、、、、基準とした中心が <0.0,0.0,0.0> 以外の場合は、、、など、もう少しこのあいまいそうな「基準」について考えることで、グローバル(絶対)位置とローカル(相対)位置の理解が深まるはずです。

次回は「基準」について考えてみます。

2010年10月6日水曜日

[LSL] 位置と回転について ~ 位置を表す二つの方法 ~

何年やってもなかなか理解し難いのがオブジェクトの位置と回転の扱い方という方は多いように思います。
多くの先輩諸氏がブログなどに Secondlife や LSL での「位置」と「回転」に関する情報を残してくれていますが、前提とする知識のレベルが高かったり、専門的であったりして、自分のレベルに合う情報をなかなか見つけることが難しいエリア、話題ですよね。
私もちょっと避けてきたのですが、この数ヶ月やらなきゃいけない課題があり、ようやく、というか、なんとなく、、理解できたような気がしたので、自分の勉強も兼ねてまとめてみようと思います。
今のところ以下の6回にわけて、Secondlife と LSL による位置と回転(向き)の考え方をまとめてみようと思います。最終的な目標は複数プリムからなるオブジェクトにアバタ―が座った時に、そのアバタ―を自分の思い通りにスクリプトで動かすことができるようにしたいと考えています。

1. 位置を表す二つの方法
2. 移動量という考え方
3. 基準はどこにあるの?
4. ルートプリムと子プリムたち
5. アバターにスクリプトはいれられない
6. スクリプト連携による方法

これらの投稿の課題として、なるべく数学的な要素を省き、三角関数や行列計算、虚数などの深い理解を必要としない形でオブジェクトやアバタ―を動かすことができればいいな、その内容を伝えられたらいいな、と思っています。

1. 位置を表す二つの方法
3次元空間における位置と向きの指定が難しいのは、もしかすると最初の出だしに問題がありそうな気がしました。それは空間の概念です。簡単すぎてあまり深く考えていませんでしたが、二つの方法 (グローバルとローカルじゃないですよ) があることをしっかり理解したほうがあとあと楽になりそうです。

私たちが最初に理解するのは XYZ 軸を使った位置の指定方法です。SIMの中で東西南北天地直交した軸に沿っていきたい場所、今いる場所を特定できます。
同じように XYZ 軸を使って東西、南北、天地に関係なく、いま自分が向いている方向で前後左右上下直交する軸に沿っていきたい場所を指定することもできます。それぞれをグローバル座標軸(もしくはリージョン座標軸)、ローカル座標軸を使った位置の指定と呼びます。
このあたりは、この話題にご興味を持たれた方はすでにご存知かと思います。
また、私のブログでもかなり前に触れています。(混乱を避けるために、以下の私のブログは読まなくてもいいです。)

位置と回転を理解したい1
位置と回転を理解したい2
位置と回転を理解したい3

過去に書いたこの上記3つのブログでも意識していなかった空間の概念というが、位置の指定方法における別のやり方です。それがクォータニオンを使った位置の指定です。えっ?クォータニオンって回転じゃないの?と思われるでしょう。私もつい最近までそうでした。(^_^)

クォータニオンが何かを数学的に理解するのは正直時間がかかります。行列計算、三角関数、複素数、虚数、ジンバルロック、、、などなど。でも、数学的な理解が目的ではないので、ここではクォータニオンをある点からある点への「移動量」として理解してみようと思います。ここでは移動量が何か、何で構成されるのかは考えず、とにかく位置 A から位置 B に移動するのにクォータニオンという「移動量」を使えば移動できる、と考えるわけです。また位置 A や位置 B についてもクォータニオンで表現できる、と覚えます。

この移動量による位置指定の概念を理解するのに一番良い、第一歩として「地球」上の位置を考えてみます。

たとえば、東京からニューヨークに移動するとします。よりわかりやすくするために、成田空港から JFK 空港に直行便があって、それらの空港間を移動すると考えます。一番最短な距離で成田から JFK にいくには「大圏航路(たいけんこうろ)」という最短距離を飛ぶのが理想とされています。途中 LAX (ロサンゼルス)やアンカレッジに寄り燃料補給せずに直接行けるなら、この大圏航路を使えばいいのです。とにかく、大圏航路は地球上の2点を結ぶ最短距離で、それは1つしかない、と理解してみます。
googlemap
また成田空港という場所の指定を地球上でやろうとすると、、、日本の住所もありますが、地球規模で考えると緯度と経度を使いますよね。(緯度35.763983, 経度140.384644) JFK 空港も同様に緯度・経度で位置を指し示すことが可能です。(緯度40.6444122, 経度 -73.782745)つまり、、、地球上の「どこ」でも緯度と経度で位置を指定できます。

ちょっと、まって、、、「地表」ならともかく、地球上には「高さ」があるわけだから、富士山の頂上と、道路の上では「位置」は違うじゃん、と思いますよね。でも、その違いは「標高」や「高度」、「海抜」で表現すればいいのです。
よって、地球上では「緯度」「経度」「高さ」の3つの数字があればどんな位置でも特定が可能なのが、なんとなく想像つくと思います。

位置を表す二つの方法の言い換えると、「立方体で考える空間と位置指定」「球体で考える空間と位置指定」と考えることもできます。

次回はとくに球体で考える空間における「移動量」とクォータニオンをインワールドに適用したときの考えについてまとめてみようと思います。