Yes Second Life

セカンドライフ向けWebサービスを作ってました。このブログではVR・メタバースのことを書いていきます

透視投影変換を使ってロックオンできるHUDをつくる

ロックオンできるHUDといっても、よくわからないと思うので、まずは動画をみてください。

赤い箱は土地にRezされたオブジェクトで、青い四角はHUDです。赤い箱にタッチすると箱が自分の現在の位置をシャウトするようになってて、それをHUD側がListenして箱が表示されている座標へ移動するようになってます。3Dシューティングなんかで敵の位置へカーソルが移動してロックオン状態になったりしますが、まさにそんな感じのものをつくりました。

使用している技術は透視投影変換で、これは3D上の世界を2次元の画面へマッピングするための手法です。ただ、これを説明するのは、ものすごく大変というか自分も完全には理解してないので、とりあえず赤い箱の位置を渡せばHUDをロックオンする位置に移動させる関数をいきなりお見せします。

// 引数にターゲットオブジェクトの位置を渡すと、そこにHUDをロックオンさせます
lockOn(vector pos)
{
    // Field of View レンズの画角を算出(SLのデフォルトの視野角は60度)
    float fov = 1.0 / llTan(60.0 * 0.5 * PI / 180.0);
    
    // カメラの情報を取得(要パーミッション)
    vector cameraPos = llGetCameraPos();
    rotation cameraRot = llGetCameraRot();
    
    // カメラとオブジェクトの距離を算出
    float dist = llVecDist(pos, cameraPos);
    
    // カメラとオブジェクト間をベクトルで表現
    vector objVec = pos - cameraPos;
    
    // カメラの回転を、X、Y、Zの各ベクトル表現に分解
    vector xVec = <1.0, 0.0, 0.0> * cameraRot;
    vector yVec = <0.0, 1.0, 0.0> * cameraRot;
    vector zVec = <0.0, 0.0, 1.0> * cameraRot;
    
    // オブジェクトへのベクトルとカメラの向いてる方向との角度差を得る
    rotation xRot = llRotBetween(objVec, xVec);
    rotation yRot = llRotBetween(objVec, yVec);
    rotation zRot = llRotBetween(objVec, zVec);
    
    // オブジェクトへの距離をカメラを軸としたX、Y、Z方向の距離に分解
    float xDist = dist * llCos(llRot2Angle(xRot));
    float yDist = dist * llCos(llRot2Angle(yRot));
    float zDist = dist * llCos(llRot2Angle(zRot));
    
    // 距離と視野角から、HUDの位置を算出する
    float posY = yDist / xDist * fov / 2.0;
    float posZ = zDist / xDist * fov / 2.0;
    vector pos = <0.0, posY, posZ>;
    
    // HUDを移動させる
    llSetPrimitiveParams([PRIM_POSITION, pos]);
}

ゲームプログラミングとかしてる人じゃないと分かりませんよね。3Dの世界を2次元にマッピングする場合、遠くにあるものほど真ん中へ寄せて表示しないとそれらしくならないので、そういう処理をしてると思ってください。

f:id:sabro:20131214012302p:plain
図にするとこんな感じかな。

この技術はカソウセカイカメラでも使われています。まあちょっとしたバグがあって、コメントは正確な位置から少しズレるんですが・・・。コメントを空間に表示するとき位置をどう決めればいいのかわからなくて、試行錯誤しててこの方法にたどりついた感じです。

応用範囲は広くて、3Dシューティングゲームを作ったりできますし、なにかモノを指し示すガイドとしても使えるとおもいます。特に後者は、情報が整理されておらずカオスな仮想世界で何か探してもらうときに、かなり役に立つんじゃないでしょうか。

最後に実際に動作確認したスクリプトを載せておきます( ̄∇  ̄ )

赤い箱のスクリプト

// チャット用のチャンネル(適当)
integer CHANNEL = -84527;

default
{
    touch_start(integer total_number)
    {
        // 今の位置をシャウト
        llShout(CHANNEL, (string)llGetPos());
    }
}

青い四角のスクリプト

// Secondlifeのデフォルトの視野角
float ANGLE = 60.0;

// チャット用のチャンネル(適当)
integer CHANNEL = -84527;

// 引数にターゲットオブジェクトの位置を渡すと、そこにHUDをロックオンさせます
lockOn(vector pos)
{
    // Field of View レンズの画角を算出
    float fov = 1.0 / llTan(ANGLE * 0.5 * PI / 180.0);
    
    // カメラの情報を取得
    vector cameraPos = llGetCameraPos();
    rotation cameraRot = llGetCameraRot();
    
    // カメラのとオブジェクトの距離を算出
    float dist = llVecDist(pos, cameraPos);
    
    // カメラとオブジェクト間をベクトルで表現
    vector objVec = pos - cameraPos;
    
    // カメラの回転を、X、Y、Zの各ベクトル表現に分解
    vector xVec = <1.0, 0.0, 0.0> * cameraRot;
    vector yVec = <0.0, 1.0, 0.0> * cameraRot;
    vector zVec = <0.0, 0.0, 1.0> * cameraRot;
    
    // オブジェクトへのベクトルとカメラの向いてる方向との角度差を得る
    rotation xRot = llRotBetween(objVec, xVec);
    rotation yRot = llRotBetween(objVec, yVec);
    rotation zRot = llRotBetween(objVec, zVec);
    
    // オブジェクトへの距離をカメラを軸としたX、Y、Z方向の距離に分解
    float xDist = dist * llCos(llRot2Angle(xRot));
    float yDist = dist * llCos(llRot2Angle(yRot));
    float zDist = dist * llCos(llRot2Angle(zRot));
    
    // 距離と視野角から、HUDの位置を算出する
    float posY = yDist / xDist * fov / 2.0;
    float posZ = zDist / xDist * fov / 2.0;
    vector pos = <0.0, posY, posZ>;
    
    // HUDを移動させる
    llSetPrimitiveParams([PRIM_POSITION, pos]);
}


default
{
    state_entry()
    {
        // ターゲットオブジェクトの発信する位置情報をListen
        llListen(CHANNEL, "", NULL_KEY, "");
        
        // HUDとしてアタッチされてる場合のみ、カメラのパーミッションを取得
        if(llGetAttached() >= ATTACH_HUD_CENTER_2 && llGetAttached() <= ATTACH_HUD_BOTTOM_RIGHT) 
        {
            llRequestPermissions(llGetOwner(), PERMISSION_TRACK_CAMERA);
        }
    }
    
    on_rez(integer start_param) 
    {
        llResetScript();
    }
    
    listen(integer channel, string name, key id, string message)
    {
        // サンプルなのでチェックはなしです

        // Lock on 
        lockOn((vector)message);
    }
    
    run_time_permissions(integer perm) {
        if(perm & PERMISSION_TRACK_CAMERA) {
            // do nothing
        }
    }
    
}