web-dev-qa-db-ja.com

Three.jsプロジェクターとRayオブジェクト

衝突検出のデモを行うために、ProjectorクラスとRayクラスを使用しようとしています。マウスを使用してオブジェクトを選択またはドラッグしようとしています。オブジェクトを使用する例を見てきましたが、ProjectorとRayのメソッドのいくつかが正確に何をしているのかを説明するコメントはないようです。誰かが簡単に答えられることを望んでいる質問がいくつかあります。

Projector.projectVector()とProjector.unprojectVector()の正確な違いと違いは何ですか?プロジェクターとレイオブジェクトの両方で、レイが作成される前にunprojectメソッドが呼び出されます。 projectVectorはいつ使用しますか?

このdemoで次のコードを使用して、マウスでドラッグするとキューブを回転させています。 mouse3Dとカメラで投影を解除してからRayを作成すると、正確に何が起こっているのかを簡単に説明できますか?レイはunprojectVector()の呼び出しに依存しますか

/** Event fired when the mouse button is pressed down */
function onDocumentMouseDown(event) {
    event.preventDefault();
    mouseDown = true;
    mouse3D.x = mouse2D.x = mouseDown2D.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse3D.y = mouse2D.y = mouseDown2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
    mouse3D.z = 0.5;

    /** Project from camera through the mouse and create a ray */
    projector.unprojectVector(mouse3D, camera);
    var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize());
    var intersects = ray.intersectObject(crateMesh); // store intersecting objects

    if (intersects.length > 0) {
        SELECTED = intersects[0].object;
        var intersects = ray.intersectObject(plane);
    }

}

/** This event handler is only fired after the mouse down event and
    before the mouse up event and only when the mouse moves */
function onDocumentMouseMove(event) {
    event.preventDefault();

    mouse3D.x = mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse3D.y = mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
    mouse3D.z = 0.5;
    projector.unprojectVector(mouse3D, camera);

    var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize());

    if (SELECTED) {
        var intersects = ray.intersectObject(plane);
        dragVector.sub(mouse2D, mouseDown2D);
        return;
    }

    var intersects = ray.intersectObject(crateMesh);

    if (intersects.length > 0) {
        if (INTERSECTED != intersects[0].object) {
            INTERSECTED = intersects[0].object;
        }
    }
    else {
        INTERSECTED = null;
    }
}

/** Removes event listeners when the mouse button is let go */
function onDocumentMouseUp(event) {
    event.preventDefault();

    /** Update mouse position */
    mouse3D.x = mouse2D.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse3D.y = mouse2D.y = -(event.clientY / window.innerHeight) * 2 + 1;
    mouse3D.z = 0.5;

    if (INTERSECTED) {
        SELECTED = null;
    }

    mouseDown = false;
    dragVector.set(0, 0);
}

/** Removes event listeners if the mouse runs off the renderer */
function onDocumentMouseOut(event) {
    event.preventDefault();

    if (INTERSECTED) {
        plane.position.copy(INTERSECTED.position);
        SELECTED = null;
    }
    mouseDown = false;
    dragVector.set(0, 0);
}
38
Cory Gross

基本的に、3Dワールド空間と2Dスクリーン空間から投影する必要があります。

レンダラーはprojectVectorを使用して、3Dポイントを2Dスクリーンに変換します。 unprojectVectorは基本的に、2Dポイントを3Dワールドに投影する逆投影を行うためのものです。どちらの方法でも、シーンを表示しているカメラを渡します。

したがって、このコードでは、2D空間で正規化されたベクトルを作成しています。正直に言うと、z = 0.5ロジック。

mouse3D.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse3D.y = -(event.clientY / window.innerHeight) * 2 + 1;
mouse3D.z = 0.5;

次に、このコードはカメラ投影行列を使用して3Dワールド空間に変換します。

projector.unprojectVector(mouse3D, camera);

Mouse3Dポイントを3D空間に変換すると、方向を取得するためにそれを使用し、カメラ位置を使用して光線を投げることができるようになりました。

var ray = new THREE.Ray(camera.position, mouse3D.subSelf(camera.position).normalize());
var intersects = ray.intersectObject(plane);
50
mrdoob

サンプルコードの範囲外で作業するには、画面の下にもう少し深く行く必要があることがわかりました(画面を満たさないキャンバスや追加の効果があるなど)。私はそれについてのブログ記事を書きましたここ。これは短縮版ですが、私が見つけたほとんどすべてをカバーするはずです。

どうやってするの

次のコード(@mrdoobによって既に提供されているものに類似)は、クリックされるとキューブの色を変更します。

    var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1,   //x
                                    -( event.clientY / window.innerHeight ) * 2 + 1,  //y
                                    0.5 );                                            //z
    projector.unprojectVector( mouse3D, camera );   
    mouse3D.sub( camera.position );                
    mouse3D.normalize();
    var raycaster = new THREE.Raycaster( camera.position, mouse3D );
    var intersects = raycaster.intersectObjects( objects );
    // Change color if hit block
    if ( intersects.length > 0 ) {
        intersects[ 0 ].object.material.color.setHex( Math.random() * 0xffffff );
    }

最新のthree.jsリリース(r55以降)では、pickingRayを使用して物事をさらに簡素化し、次のようにすることができます。

    var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1,   //x
                                    -( event.clientY / window.innerHeight ) * 2 + 1,  //y
                                    0.5 );                                            //z
    var raycaster = projector.pickingRay( mouse3D.clone(), camera );
    var intersects = raycaster.intersectObjects( objects );
    // Change color if hit block
    if ( intersects.length > 0 ) {
        intersects[ 0 ].object.material.color.setHex( Math.random() * 0xffffff );
    }

フードの下で何が起こっているかについてのより多くの洞察を与えるので、古いアプローチに固執しましょう。この動作を確認できますここ、キューブをクリックして色を変更します。

何が起こっていますか?

    var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1,   //x
                                    -( event.clientY / window.innerHeight ) * 2 + 1,  //y
                                    0.5 );                                            //z

event.clientXは、クリック位置のx座標です。 window.innerWidthで割ると、ウィンドウの全幅に比例したクリックの位置が得られます。基本的に、これは、左上の(0,0)から始まる画面座標から、右下の(window.innerWidthwindow.innerHeight)までを中心(0,0)のデカルト座標に変換します。 )および以下に示すように(-1、-1)から(1,1)の範囲:

translation from web page coordinates

Zの値は0.5であることに注意してください。これは、z軸に沿って3D空間に投影しているカメラから離れたポイントの深さであると言うことを除いて、この時点でz値についてあまり詳しく説明しません。これについては後で詳しく説明します。

次:

    projector.unprojectVector( mouse3D, camera );

Three.jsコードを見ると、これは実際に3Dワールドからカメラへの投影マトリックスの反転であることがわかります。 3Dワールド座標からスクリーン上の投影に到達するには、3Dワールドをカメラの2D表面に投影する必要があることに注意してください(これはスクリーンに表示されるものです)。基本的に逆を行っています。

Mouse3Dには、この投影されていない値が含まれることに注意してください。これは、関心のある光線/軌跡に沿った3D空間内のポイントの位置です。正確なポイントは、z値に依存します(これについては後で説明します)。

この時点で、次の画像を見ると便利です。

Camera, unprojected value and ray

計算したばかりのポイント(mouse3D)は、緑色の点で示されています。ドットのサイズは純粋に例示であり、カメラまたはmouse3Dポイントのサイズには影響しないことに注意してください。ドットの中心の座標にもっと興味があります。

ここで、3D空間に単一のポイントが必要なだけでなく、オブジェクトがこの光線/軌道に沿って配置されているかどうかを判断できるように、光線/軌道(黒い点で表示)が必要です。レイに沿って表示されるポイントは任意のポイントであることに注意してくださいレイはカメラからの方向であり、ポイントのセットではありません

幸いなことに、光線に沿った点があり、カメラからこの点まで軌道が通過する必要があることがわかっているため、光線の方向を決定できます。したがって、次のステップは、mouse3Dの位置からカメラの位置を減算することです。これにより、単一の点ではなく方向ベクトルが得られます。

    mouse3D.sub( camera.position );                
    mouse3D.normalize();

カメラから3D空間のこのポイントへの方向があります(mouse3Dにはこの方向が含まれています)。次に、これを正規化することで単位ベクトルに変換します。

次のステップは、カメラ位置から開始し、方向(mouse3D)を使用して光線を投射する光線(Raycaster)を作成することです。

    var raycaster = new THREE.Raycaster( camera.position, mouse3D );

残りのコードは、3D空間内のオブジェクトがレイと交差するかどうかを決定します。幸いなことに、intersectsObjectsを使用して、舞台裏ですべての面倒を見てくれます。

デモ

それでは、私のサイトここからデモを見てみましょう。これらの光線は3D空間に投射されています。任意の場所をクリックすると、カメラがオブジェクトの周りを回転して、光線がどのように投射されるかを示します。カメラが元の位置に戻ると、1つのドットしか表示されないことに注意してください。これは、他のすべてのドットが投影の線に沿っているため、フロントドットによって視界が遮られているためです。これは、あなたから直接向いている矢印の線を見下ろすときと似ています-見えるのはベースだけです。もちろん、あなたに向かって直接進んでいる矢印の線を見下ろす場合にも同じことが当てはまります(頭だけが見えます)。これは一般に悪い状況です。

Z座標

そのz座標をもう一度見てみましょう。 このデモを参照して、このセクションを読み、zのさまざまな値を試してください。

OK、この関数をもう一度見てみましょう:

    var mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1,   //x
                                    -( event.clientY / window.innerHeight ) * 2 + 1,  //y
                                    0.5 );                                            //z  

値として0.5を選択しました。前述のように、z座標は3Dへの投影の深さを決定します。したがって、zのさまざまな値を見て、zの効果を確認してみましょう。これを行うには、カメラのある場所に青い点を、カメラから投影されていない位置に緑の点の線を配置しました。次に、交差点が計算された後、カメラを後ろに移動して光線を表示します。いくつかの例でよく見られます。

まず、0.5のz値:

z value of 0.5

カメラ(青点)から未投影値(3D空間の座標)までの緑の点線に注意してください。これは銃の銃身のようなもので、光線を放つ方向を指しています。緑色の線は、基本的に正規化される前に計算される方向を表します。

OK、値0.9を試してみましょう。

z value of 0.9

ご覧のとおり、緑色の線が3D空間にさらに広がっています。 0.99はさらに拡張されます。

Zの値の大きさに関して重要性があるかどうかはわかりません。値が大きいほど(銃身が長いほど)より正確になるようですが、方向を計算しているので、短い距離でもかなり正確です。私が見た例では0.5を使用しているので、特に断りのない限り、これはそのままです。

キャンバスがフルスクリーンでないときの投影

何が起こっているのかをもう少し知ったので、キャンバスがウィンドウを埋めずにページ上に配置されたときに値がどうあるべきかを理解できます。たとえば、次のように言います:

  • three.jsキャンバスを含むdivは、左からoffsetX、画面の上部からoffsetYです。
  • キャンバスの幅はviewWidthに等しく、高さはviewHeightに等しくなります。

コードは次のようになります。

    var mouse3D = new THREE.Vector3( ( event.clientX - offsetX ) / viewWidth * 2 - 1,
                                    -( event.clientY - offsetY ) / viewHeight * 2 + 1,
                                    0.5 );

基本的に、私たちがしているのは、キャンバスに対するマウスクリックの位置を計算することです(xの場合:event.clientX - offsetX)。次に、キャンバスがウィンドウを埋めたときと同様に、クリックが発生した場所(x:/viewWidth)を比例的に決定します。

それだけです。うまくいけばうまくいきます。

72
acarlon

リリースr70以降、Projector.unprojectVectorおよびProjector.pickingRayは非推奨です。代わりに、raycaster.setFromCameraこれにより、マウスポインターの下にあるオブジェクトを簡単に見つけることができます。

var mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; 

var raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(scene.children);

intersects[0].objectはオブジェクトをマウスポインターの下に、intersects[0].pointは、マウスポインターがクリックされたオブジェクト上のポイントを示します。

19
Prabu Arumugam

Projector.unprojectVector()は、vec3を位置として扱います。プロセス中にベクトルが変換されるため、。sub(camera.position)を使用します。さらに、この操作の後、正規化する必要があります。

この投稿にいくつかのグラフィックを追加しますが、今のところ、操作のジオメトリを説明できます。

カメラはジオメトリの観点からピラミッドと考えることができます。実際には、左、右、上、下、近、遠の6つのペインで定義します(チップに最も近い面はニアです)。

3Dに立ってこれらの操作を観察すると、空間内の任意の回転を伴う任意の位置にこのピラミッドが表示されます。このピラミッドの原点が先端にあり、負のz軸が下に向かって走っているとしましょう。

マトリックス変換の正しいシーケンスを適用すると、これらの6つのプレーンに含まれる結果はすべて、画面にレンダリングされます。 openglは次のようになります:

NDC_or_homogenous_coordinates = projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

これにより、メッシュがオブジェクト空間からワールド空間、カメラ空間に取り込まれ、最終的にすべてが小さな立方体(-1〜1の範囲のNDC)に配置される透視投影行列が投影されます。

オブジェクト空間は、xyz座標のきちんとしたセットにすることができます。この空間では、何かを言うことから得られる建築モデルとは対照的に、アーティストが対称を使用してモデル化し、座標空間にきちんと整列する3dモデルなどを生成しますREVITまたはAutoCAD。

ObjectMatrixは、モデルマトリックスとビューマトリックスの間に発生する可能性がありますが、これは通常、事前に処理されます。たとえば、yとzを反転したり、Originから遠く離れたモデルを境界に移動したり、単位を変換したりします。

フラットな2Dスクリーンに奥行きがあると考えると、NDCキューブと同じように説明できますが、わずかに歪んでいます。これが、アスペクト比をカメラに供給する理由です。画面の高さの正方形を想像すると、残りはx座標を拡大縮小するために必要なアスペクト比です。

3D空間に戻りましょう。

3Dシーンに立っていると、ピラミッドが見えます。ピラミッドの周りのすべてを切り取り、そこに含まれるシーンの一部と一緒にピラミッドを取得し、先端を0,0,0に置き、底を-z軸に向けると、ここになります。

viewMatrix * modelMatrix * position.xyzw

これに射影行列を乗算すると、先端を取り、x軸とy軸でappartを引き出してその1点から正方形を作成し、ピラミッドを箱に変えた場合と同じになります。

このプロセスでは、ボックスは-1と1にスケーリングされ、透視投影が得られ、ここで終了します。

projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

この空間では、2次元のマウスイベントを制御できます。画面上にあるため、2次元であり、NDCキューブ内のどこかにあることがわかります。 2次元の場合、XとYは知っているがZはわかっていないため、レイキャスティングの必要性があると言えます。

したがって、レイをキャストするとき、基本的にキューブの側面の1つに垂直に、キューブを通る線を送信します。

次に、その光線がシーン内の何かに当たるかどうかを把握する必要があります。そのためには、この立方体からの光線を計算に適した空間に変換する必要があります。ワールド空間の光線が必要です。

光線は空間内の無限の線です。方向があり、空間内のポイントを通過する必要があるため、ベクトルとは異なります。そして実際、これがレイキャスターの議論の仕方です。

そのため、線に沿ってボックスの上部を絞ってピラミッドに戻すと、線は先端から始まり、ピラミッドの下部と下に交差します-mouse.x * farRangeと-mouse.y * farRange。

(最初は-1と1ですが、ビュースペースはワールドスケールで、回転および移動しただけです)

これは、いわばカメラのデフォルトの位置(オブジェクト空間)であるため、レイに独自のワールドマトリックスを適用すると、カメラとともに変換されます。

光線は0,0,0を通過するため、方向のみがあり、THREE.Vector3には方向を変換するメソッドがあります。

THREE.Vector3.transformDirection()

また、プロセス内のベクトルを正規化します。

上記のメソッドのZ座標

これは基本的にどの値でも機能し、NDCキューブの動作方法により同じように機能します。ニアプレーンとファープレーンは、-1と1に投影されます。

あなたが言うとき、次の場所で光線を放ちます:

[ mouse.x | mouse.y | someZpositive ]

ポイント(mouse.x、mouse.y、1)を介して(0,0、someZpositive)の方向に線を送信します

これをボックス/ピラミッドの例に関連付けると、このポイントは下部にあり、ラインはカメラから発生しているため、そのポイントも通過します。

しかし、NDC空間では、この点は無限に引き伸ばされ、この線は左、上、右、下の平面と平行になります。

上記の方法で投影を解除すると、これは本質的に位置/ポイントに変わります。ファープレーンはワールド空間にマップされるため、ポイントはz = -1のどこかにあり、Xの-camera aspectと+ cameraAspect、yの-1と1の間です。

これはポイントであるため、カメラのワールドマトリックスを適用すると、それが回転するだけでなく、平行移動します。したがって、カメラの位置を差し引くことにより、これをOriginに戻す必要があります。

1
pailhead