Подтвердить что ты не робот

Объекты проектора Three.js и Ray

Я пытаюсь работать с классами Projector и Ray, чтобы выполнить некоторые демонстрации обнаружения конфликтов. Я начал просто пытаться использовать мышь, чтобы выбрать объекты или перетащить их. Я рассмотрел примеры, которые используют объекты, но ни у кого из них нет комментариев, объясняющих, что именно делают некоторые из методов Projector и Ray. У меня есть пара вопросов, на которые я надеюсь, кому-то будет легко ответить.

Что именно происходит и в чем разница между Projector.projectVector() и Projector.unprojectVector()? Я замечаю, что во всех примерах, использующих как объекты проектора, так и луча, метод unproject вызывается до создания луча. Когда вы будете использовать projectVector?

Я использую следующий код в этом demo, чтобы вращать куб при перетаскивании мышью. Может кто-то объяснить простыми словами, что именно происходит, когда я unproject с 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);
}
4b9b3361

Ответ 1

В принципе, вам нужно проектировать из 3D-пространства мира и пространства 2D-экрана.

Renderers используют 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;

Затем этот код использует матрицу проекции камеры, чтобы преобразовать ее в наше трехмерное пространство мира.

projector.unprojectVector(mouse3D, camera);

С точкой mouse3D, преобразованной в 3D-пространство, мы можем теперь использовать его для получения направления, а затем использовать положение камеры, чтобы выбросить луч.

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

Ответ 2

Я обнаружил, что мне нужно пройти немного глубже под поверхностью, чтобы работать вне сферы действия образца кода (например, иметь холст, который не заполняет экран или не имеет дополнительных эффектов). Я написал сообщение в блоге об этом здесь. Это сокращенная версия, но должна охватывать почти все, что я нашел.

Как это сделать

Следующий код (аналогичный уже предоставленному @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), как показано ниже:

translation from web page coordinates

Обратите внимание, что z имеет значение 0,5. Я не буду вдаваться в подробности о значении z в этой точке, кроме как сказать, что это глубина точки от камеры, которую мы проецируем в 3D-пространство вдоль оси z. Подробнее об этом позже.

Далее:

    projector.unprojectVector( mouse3D, camera );

Если вы посмотрите на код three.js, вы увидите, что это действительно инверсия матрицы проекции из 3D-мира в камеру. Имейте в виду, что для того, чтобы добраться от координат 3D-мира до проекции на экран, 3D-мир нужно проецировать на двумерную поверхность камеры (это то, что вы видите на экране). Мы в основном делаем обратный.

Обратите внимание, что mouse3D теперь будет содержать это непроецируемое значение. Это положение точки в трехмерном пространстве вдоль интересующей нас лучи/траектории. Точная точка зависит от значения z (мы увидим это позже).

В этот момент может оказаться полезным взглянуть на следующее изображение:

Camera, unprojected value and ray

Точка, которую мы только что вычислили (mouse3D), показана зеленой точкой. Обратите внимание, что размер точек является чисто иллюстративным, они не влияют на размер камеры или точки mouse3D. Нас больше интересуют координаты в центре точек.

Теперь мы просто не хотим иметь единственную точку в трехмерном пространстве, но вместо этого мы хотим лучи/траектории (показаны черными точками), чтобы мы могли определить, расположен ли объект вдоль этого луча/траектории. Обратите внимание, что точки, показанные вдоль луча, являются просто произвольными точками, луч является направлением от камеры, а не множеством точек.

К счастью, поскольку у нас есть точка вдоль луча, и мы знаем, что траектория должна проходить от камеры к этой точке, мы можем определить направление луча. Поэтому следующим шагом является вычитание положения камеры из положения mouse3D, это даст вектор направления, а не только одну точку:

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

Теперь у нас есть направление от камеры к этой точке в трехмерном пространстве (mouse3D теперь содержит это направление). Затем он преобразуется в единичный вектор, нормализуя его.

Следующим шагом будет создание луча (Raycaster), начиная с положения камеры и с помощью направления (mouse3D), чтобы нанести луч:

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

Остальная часть кода определяет, пересекаются ли объекты в трехмерном пространстве лучом или нет. К счастью, все это позаботится о нас за кулисами, используя intersectsObjects.

Демо

Хорошо, так что давайте посмотрим на демонстрацию с моего сайта здесь, которая показывает, что эти лучи отображаются в трехмерном пространстве. Когда вы нажимаете в любом месте, камера вращается вокруг объекта, чтобы показать вам, как луч выполняется. Обратите внимание: когда камера вернется в исходное положение, вы увидите только одну точку. Это связано с тем, что все остальные точки расположены вдоль линии проекции и поэтому блокируются от передней точки. Это похоже на то, когда вы смотрите вниз по линии стрелки, указывающей прямо от вас - все, что вы видите, является базой. Конечно, то же самое относится, если смотреть вниз по линии стрелки, которая движется прямо к вам (вы видите только голову), что обычно бывает плохой ситуацией.

Координата 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 value of 0.5

Обратите внимание на зеленую линию точек из камеры (синяя точка) на непроектированное значение (координата в трехмерном пространстве). Это похоже на бочонок пистолета, указывая в направлении, в котором они должны быть брошены. Зеленая линия по существу представляет направление, которое рассчитывается до нормализации.

ОК, попробуйте значение 0.9:

z value of 0.9

Как вы можете видеть, зеленая линия теперь расширена в 3D-пространстве. 0.99 продолжается еще дальше.

Я не знаю, имеет ли какое-либо значение, насколько велика величина z. Похоже, что более высокая ценность будет более точной (например, более длинная пушка), но поскольку мы вычисляем направление, даже небольшое расстояние должно быть довольно точным. Примеры, которые я видел, используют 0,5, так что я буду придерживаться, если не сказать иначе.

Проецирование, когда холст не полный экран

Теперь, когда мы знаем немного больше о том, что происходит, мы можем выяснить, какие значения должны быть, когда холст не заполняет окно и находится на странице. Скажем, например, что:

  • div, содержащий холст three.js, является offsetX слева и offsetY в верхней части экрана.
  • холст имеет ширину, равную viewWidth и height, равную 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), аналогичный тому, когда холст заполнил окно.

Это, надеюсь, это помогает.

Ответ 3

Начиная с выпуска 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 указывает точку на объекте, на который был нажат указатель мыши.

Ответ 4

Projector.unprojectVector() рассматривает vec3 как позицию. Во время процесса вектор переводится, поэтому мы используем .sub(camera.position). Кроме того, мы должны нормализовать его после этой операции.

Я добавлю некоторые графики в этот пост, но пока я могу описать геометрию операции.

Мы можем представить камеру как пирамиду с точки зрения геометрии. Фактически мы определяем его с 6 панелями - слева, справа, сверху, снизу, рядом и далеко (ближе к плоскости, ближайшей к кончику).

Если бы мы стояли в 3d и наблюдали эти операции, мы увидели бы эту пирамиду в произвольном положении с произвольным вращением в пространстве. Допустим, что это начало пирамиды у него на вершине, а отрицательная ось z движется к дну.

Все, что заканчивается тем, что содержится в этих 6 плоскостях, в конечном итоге будет отображаться на нашем экране, если мы применим правильную последовательность матричных преобразований. Который я opengl иду что-то вроде этого:

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

Это выводит нашу мешу из этого пространства объекта в мировое пространство, в пространство камеры, и, в конце концов, оно проектирует его перспективную матрицу проекций, которая по существу ставит все в маленький куб (NDC с диапазонами от -1 до 1).

Объектное пространство может быть аккуратным набором координат xyz, в котором вы генерируете что-то процедурно или говорите, 3d модель, которую художник моделировал с использованием симметрии и, таким образом, аккуратно сидит в соответствии с координатным пространством, в отличие от архитектурной модели, полученной из скажем, что-то вроде REVIT или AutoCAD.

Объектная матрица может произойти между матрицей модели и матрицей вида, но об этом обычно заботятся заранее. Скажем, переворачивание y и z или приведение модели, находящейся далеко от начала координат, в границы, единицы преобразования и т.д.

Если мы подумаем о нашем плоском 2d экране, как если бы он имел глубину, его можно было бы описать так же, как куб NDC, хотя и слегка искаженный. Вот почему мы поставляем соотношение сторон к камере. Если мы представим квадрат размера нашей высоты экрана, остальное - это соотношение сторон, которое необходимо масштабировать по нашим координатам x.

Теперь вернемся в 3D-пространство.

Мы стоим на 3d-сцене, и мы видим пирамиду. Если мы вырезаем все вокруг пирамиды, а затем возьмем пирамиду вместе с частью содержащейся в ней сцены и наложим ее на 0,0,0, и нарисуем дно в направлении оси -z, мы закончим здесь:

viewMatrix * modelMatrix * position.xyzw

Умножение этого на матрицу проецирования будет таким же, как если бы мы взяли наконечник, и начали тянуть его appart в оси x и y, создавая квадрат из этой одной точки и превращая пирамиду в ящик.

В этом процессе окно становится масштабированным до -1 и 1, и мы получаем нашу перспективную проекцию, и мы заканчиваем здесь:

projectionMatrix * viewMatrix * modelMatrix * position.xyzw; 

В этом пространстве мы имеем контроль над двумерным событием мыши. Так как это на нашем экране, мы знаем, что он двумерный и что он где-то внутри куба NDC. Если это двумерное, можно сказать, что мы знаем X и Y, но не Z, а следовательно, и необходимость лучевого кастинга.

Итак, когда мы бросаем луч, мы в основном отправляем линию через куб перпендикулярно одной из ее сторон.

Теперь нам нужно выяснить, попадает ли этот луч в сцену, и для этого нам нужно преобразовать луч из этого куба в некоторое пространство, подходящее для вычисления. Нам нужен луч в мировом пространстве.

Луч - бесконечная линия в пространстве. Он отличается от вектора, потому что он имеет направление и должен проходить через точку в пространстве. И действительно, именно так Raycaster принимает свои аргументы.

Итак, если мы сжимаем верхнюю часть коробки вместе с линией, вернемся к пирамиде, линия будет исходить от кончика и бежать вниз и пересекать нижнюю часть пирамиды где-то между - mouse.x * farRange и - мышь .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)

Если вы связываете это с примером box/pyramid, эта точка находится внизу, а так как линия берется из камеры, она проходит через эту точку.

НО, в пространстве NDC эта точка растянута до бесконечности, и эта линия заканчивается параллелью с левыми, верхними, правыми, нижними плоскостями.

Непроецирование с помощью вышеупомянутого метода превращает это в положение/точку по существу. Далекая плоскость просто отображается в мировое пространство, поэтому наша точка находится где-то в z = -1, между -камерным аспектном и + cameraAspect на X и -1 и 1 на y.

так как это точка, применяя матрицу мира камеры, не только повернет ее, но и переведет ее. Следовательно, необходимо вернуть это в начало координат, вычитая положение камеры.