39

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

Projector.projectVector() と Projector.unprojectVector() の違いは何ですか? プロジェクターと光線オブジェクトの両方を使用するすべての例で、光線が作成される前に unproject メソッドが呼び出されているように見えます。projectVector はいつ使用しますか?

このデモでは、マウスでドラッグしたときにキューブを回転させるために次のコードを使用しています。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);
}
4

4 に答える 4

81

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

どうやってするの

次のコード (@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.innerWidth, window.innerHeight) までの画面座標から、中心が (0,0) で範囲が (-1,-1) のデカルト座標に変換されます。 ) を (1,1) に変換:

ウェブページ座標からの翻訳

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

次:

    projector.unprojectVector( mouse3D, camera );

three.js コードを見ると、これが実際には 3D ワールドからカメラへの射影行列の反転であることがわかります。3D ワールド座標からスクリーンへの投影を取得するには、3D ワールドをカメラの 2D サーフェス (スクリーンに表示されるもの) に投影する必要があることに注意してください。私たちは基本的にその逆を行っています。

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

この時点で、次の画像を見ると役立つ場合があります。

カメラ、非投影値、光線

計算したばかりの点 (mouse3D) は緑色の点で示されています。ドットのサイズは純粋に説明のためのものであり、カメラやマウスの 3D ポイントのサイズとは無関係であることに注意してください。ドットの中心の座標にもっと関心があります。

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

幸いなことに、光線に沿った点があり、軌道がカメラからこの点を通過する必要があることがわかっているため、光線の方向を決定できます。したがって、次のステップは、カメラの位置を 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 の値を試してください。

では、この関数をもう一度見てみましょう。

    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 値 0.5

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

では、0.9 の値を試してみましょう。

z 値 0.9

ご覧のとおり、緑色の線が 3D 空間にさらに伸びています。0.99 はさらに伸びます。

z の値の大きさが重要かどうかはわかりません。値が大きいほど (砲身が長いなど) 精度が高くなるようですが、方向を計算しているので、短い距離でもかなり正確なはずです。私が見た例では 0.5 を使用しています。

キャンバスが全画面でない場合の投影

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

  • three.js キャンバスを含む div は、画面の左からのオフセット X と上からのオフセット Y です。
  • キャンバスの幅は 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)。/viewWidth次に、キャンバスがウィンドウいっぱいになったときと同様に、クリックが発生した場所 ( x: ) を比例的に特定します。

それだけです、うまくいけばそれは役に立ちます。

于 2014-05-06T10:58:35.540 に答える
53

基本的に、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);
于 2012-06-14T17:41:49.010 に答える
21

リリース r70 以降、Projector.unprojectVectorProjector.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あるオブジェクトとマウスポインタがクリックされたオブジェクト上の点を与える.

于 2015-03-22T10:54:30.190 に答える
1

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

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

カメラは、幾何学的に言えばピラミッドと考えることができます。実際には、左、右、上、下、近距離、遠距離の 6 つのペインで定義します (near は先端に最も近い面です)。

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

これらの 6 つのプレーンに含まれるすべてのものは、マトリックス変換の正しいシーケンスを適用すると、最終的に画面にレンダリングされます。私はこのように何かをopenglします:

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

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

オブジェクト空間は、プロシージャルに何かを生成したり、アーティストが対称性を使用してモデル化した 3D モデルなどを生成したりした xyz 座標のきちんとしたセットにすることができます。 REVIT または AutoCAD。

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

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

3次元空間に戻ります。

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

viewMatrix * modelMatrix * position.xyzw

これに射影行列を掛けることは、先端を取り出して x 軸と y 軸に引き離し始め、その 1 点から正方形を作成し、ピラミッドを箱に変えるのと同じです。

このプロセスでは、ボックスが -1 と 1 にスケーリングされ、透視投影が取得され、次のようになります。

projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

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

したがって、光線を投射するときは、基本的に、立方体の側面の 1 つに垂直に、立方体を通る線を送ります。

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

レイは空間の無限の線です。方向があり、空間内の点を通過する必要があるため、ベクトルとは異なります。実際、これが Raycaster がその引数を取る方法です。

したがって、線に沿ってボックスの上部を絞ってピラミッドに戻すと、線は先端から始まり、下に走り、マウス.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 アスペクトと + cameraAspect、y では -1 と 1 の間です。

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

于 2014-09-04T08:45:24.250 に答える