Мышь / Холст X, Y to Three.js Мир X, Y, Z

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

Все решения, которые я нашел, делают пересечение лучей, чтобы добиться выбора объекта.

То, что я пытаюсь сделать, - это расположить центр объекта Three.js в координатах, над которыми мышь в данный момент находится "над".

Моя камера имеет значения x:0, y:0, z:500 (хотя она будет двигаться во время симуляции), а все мои объекты - с z = 0 с различными значениями x и y, поэтому мне нужно знать мир на основе X, Y при допущении a z = 0 для объекта, который будет следовать позиции мыши.

Этот вопрос выглядит как похожая проблема, но не имеет решения: получение координат мыши относительно трехмерного пространства в THREE.js

Учитывая положение мыши на экране с диапазоном "top-left = 0, 0 | bottom-right = window.innerWidth, window.innerHeight", любой может предложить решение для перемещения объекта Three.js в координаты мыши вдоль z = 0?

11 ответов

Решение

Вам не нужно иметь какие-либо объекты в вашей сцене, чтобы сделать это.

Вы уже знаете положение камеры.

С помощью vector.unproject( camera ) Вы можете получить луч, указывающий направление, которое вы хотите.

Вам просто нужно продлить этот луч от положения камеры до координаты z кончика луча, равной нулю.

Вы можете сделать это так:

var vec = new THREE.Vector3(); // create once and reuse
var pos = new THREE.Vector3(); // create once and reuse

vec.set(
    ( event.clientX / window.innerWidth ) * 2 - 1,
    - ( event.clientY / window.innerHeight ) * 2 + 1,
    0.5 );

vec.unproject( camera );

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

var distance = - camera.position.z / vec.z;

pos.copy( camera.position ).add( vec.multiplyScalar( distance ) );

Переменная pos положение точки в трехмерном пространстве, "под мышкой" и в плоскости z=0,


РЕДАКТИРОВАТЬ: если вам нужна точка "под мышкой" и в плоскости z = targetZзамените вычисление расстояния следующим:

var distance = ( targetZ - camera.position.z ) / vec.z;

three.js r.98

Это сработало для меня при использовании orthographic camera

let vector = new THREE.Vector3();
vector.set(
    (event.clientX / window.innerWidth) * 2 - 1,
    - (event.clientY / window.innerHeight) * 2 + 1,
    0
);
vector.unproject(camera);

WebGL three.js r.89

В r.58 этот код работает для меня:

var planeZ = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
var mv = new THREE.Vector3(
    (event.clientX / window.innerWidth) * 2 - 1,
    -(event.clientY / window.innerHeight) * 2 + 1,
    0.5 );
var raycaster = projector.pickingRay(mv, camera);
var pos = raycaster.ray.intersectPlane(planeZ);
console.log("x: " + pos.x + ", y: " + pos.y);

Ниже приведен класс ES6, который я написал на основе ответа WestLangley, который отлично работает для меня в THREE.js r77.

Обратите внимание, что предполагается, что ваш видовой экран визуализации занимает весь видовой экран браузера.

class CProjectMousePosToXYPlaneHelper
{
    constructor()
    {
        this.m_vPos = new THREE.Vector3();
        this.m_vDir = new THREE.Vector3();
    }

    Compute( nMouseX, nMouseY, Camera, vOutPos )
    {
        let vPos = this.m_vPos;
        let vDir = this.m_vDir;

        vPos.set(
            -1.0 + 2.0 * nMouseX / window.innerWidth,
            -1.0 + 2.0 * nMouseY / window.innerHeight,
            0.5
        ).unproject( Camera );

        // Calculate a unit vector from the camera to the projected position
        vDir.copy( vPos ).sub( Camera.position ).normalize();

        // Project onto z=0
        let flDistance = -Camera.position.z / vDir.z;
        vOutPos.copy( Camera.position ).add( vDir.multiplyScalar( flDistance ) );
    }
}

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

// Instantiate the helper and output pos once.
let Helper = new CProjectMousePosToXYPlaneHelper();
let vProjectedMousePos = new THREE.Vector3();

...

// In your event handler/tick function, do the projection.
Helper.Compute( e.clientX, e.clientY, Camera, vProjectedMousePos );

vProjectedMousePos теперь содержит проецируемую позицию мыши на плоскости z=0.

У меня был холст, который был меньше моего полного окна, и мне нужно было определить мировые координаты щелчка:

// get the position of a canvas event in world coords
function getWorldCoords(e) {
  // get x,y coords into canvas where click occurred
  var rect = canvas.getBoundingClientRect(),
      x = e.clientX - rect.left,
      y = e.clientY - rect.top;
  // convert x,y to clip space; coords from top left, clockwise:
  // (-1,1), (1,1), (-1,-1), (1, -1)
  var mouse = new THREE.Vector3();
  mouse.x = ( (x / canvas.clientWidth ) * 2) - 1;
  mouse.y = (-(y / canvas.clientHeight) * 2) + 1;
  mouse.z = 0.5; // set to z position of mesh objects
  // reverse projection from 3D to screen
  mouse.unproject(camera);
  // convert from point to a direction
  mouse.sub(camera.position).normalize();
  // scale the projected ray
  var distance = -camera.position.z / mouse.z,
      scaled = mouse.multiplyScalar(distance),
      coords = camera.position.clone().add(scaled);
  return coords;
}

var canvas = renderer.domElement;
canvas.addEventListener('click', getWorldCoords);

Вот пример. Щелкните одну и ту же область пончика до и после скольжения, и вы обнаружите, что координаты остаются неизменными (проверьте консоль браузера):

// three.js boilerplate
var container = document.querySelector('body'),
    w = container.clientWidth,
    h = container.clientHeight,
    scene = new THREE.Scene(),
    camera = new THREE.PerspectiveCamera(75, w/h, 0.001, 100),
    controls = new THREE.MapControls(camera, container),
    renderConfig = {antialias: true, alpha: true},
    renderer = new THREE.WebGLRenderer(renderConfig);
controls.panSpeed = 0.4;
camera.position.set(0, 0, -10);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(w, h);
container.appendChild(renderer.domElement);

window.addEventListener('resize', function() {
  w = container.clientWidth;
  h = container.clientHeight;
  camera.aspect = w/h;
  camera.updateProjectionMatrix();
  renderer.setSize(w, h);
})

function render() {
  requestAnimationFrame(render);
  renderer.render(scene, camera);
  controls.update();
}

// draw some geometries
var geometry = new THREE.TorusGeometry( 10, 3, 16, 100, );
var material = new THREE.MeshNormalMaterial( { color: 0xffff00, } );
var torus = new THREE.Mesh( geometry, material, );
scene.add( torus );

// convert click coords to world space
// get the position of a canvas event in world coords
function getWorldCoords(e) {
  // get x,y coords into canvas where click occurred
  var rect = canvas.getBoundingClientRect(),
      x = e.clientX - rect.left,
      y = e.clientY - rect.top;
  // convert x,y to clip space; coords from top left, clockwise:
  // (-1,1), (1,1), (-1,-1), (1, -1)
  var mouse = new THREE.Vector3();
  mouse.x = ( (x / canvas.clientWidth ) * 2) - 1;
  mouse.y = (-(y / canvas.clientHeight) * 2) + 1;
  mouse.z = 0.0; // set to z position of mesh objects
  // reverse projection from 3D to screen
  mouse.unproject(camera);
  // convert from point to a direction
  mouse.sub(camera.position).normalize();
  // scale the projected ray
  var distance = -camera.position.z / mouse.z,
      scaled = mouse.multiplyScalar(distance),
      coords = camera.position.clone().add(scaled);
  console.log(mouse, coords.x, coords.y, coords.z);
}

var canvas = renderer.domElement;
canvas.addEventListener('click', getWorldCoords);

render();
html,
body {
  width: 100%;
  height: 100%;
  background: #000;
}
body {
  margin: 0;
  overflow: hidden;
}
canvas {
  width: 100%;
  height: 100%;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js'></script>
<script src=' https://threejs.org/examples/js/controls/MapControls.js'></script>

Чтобы получить координаты мыши трехмерного объекта, используйте projectVector:

var width = 640, height = 480;
var widthHalf = width / 2, heightHalf = height / 2;

var projector = new THREE.Projector();
var vector = projector.projectVector( object.matrixWorld.getPosition().clone(), camera );

vector.x = ( vector.x * widthHalf ) + widthHalf;
vector.y = - ( vector.y * heightHalf ) + heightHalf;

чтобы получить трехмерные координаты three.js, которые относятся к конкретным координатам мыши, используйте противоположный unprojectVector:

var elem = renderer.domElement, 
    boundingRect = elem.getBoundingClientRect(),
    x = (event.clientX - boundingRect.left) * (elem.width / boundingRect.width),
    y = (event.clientY - boundingRect.top) * (elem.height / boundingRect.height);

var vector = new THREE.Vector3( 
    ( x / WIDTH ) * 2 - 1, 
    - ( y / HEIGHT ) * 2 + 1, 
    0.5 
);

projector.unprojectVector( vector, camera );
var ray = new THREE.Ray( camera.position, vector.subSelf( camera.position ).normalize() );
var intersects = ray.intersectObjects( scene.children );

Здесь есть отличный пример. Однако для использования вектора проекта должен существовать объект, по которому пользователь щелкнул. Пересечения будут массив всех объектов на месте мыши, независимо от их глубины.

Для тех, кто использует @react-three/fiber(он же r3f и react-three-fiber), я нашел это обсуждение и связанные с ним примеры кода Мэтта Россмана полезными. В частности, многие примеры, использующие описанные выше методы, относятся к простым орфографическим представлениям, а не к случаям, когда OrbitControls находятся в игре.

Обсуждение: https://github.com/pmndrs/react-three-fiber/discussions/857

Простой пример с использованием техники Мэтта: https://codesandbox.io/s/r3f-mouse-to-world-elh73?file=/src/index.js

Более общий пример: https://codesandbox.io/s/react-three-draggable-cxu37?file=/src/App.js .

ThreeJS медленно косится от Projector.(Un)ProjectVector, и решение с jector.pickingRay() больше не работает, только что закончил обновление моего собственного кода... поэтому самая последняя рабочая версия должна выглядеть следующим образом:

var rayVector = new THREE.Vector3(0, 0, 0.5);
var camera = new THREE.PerspectiveCamera(fov,this.offsetWidth/this.offsetHeight,0.1,farFrustum);
var raycaster = new THREE.Raycaster();
var scene = new THREE.Scene();

//...

function intersectObjects(x, y, planeOnly) {
  rayVector.set(((x/this.offsetWidth)*2-1), (1-(y/this.offsetHeight)*2), 1).unproject(camera);
  raycaster.set(camera.position, rayVector.sub(camera.position ).normalize());
  var intersects = raycaster.intersectObjects(scene.children);
  return intersects;
}

Вот актуальный ответ(THREE.REVISION==157), который работает с повернутыми камерами и средствами визуализации размером меньше окна и находит точку на нулевой плоскости. (Замените любой другой самолет, который вам нравится.)

      const raycaster = new THREE.Raycaster()
const pt = new THREE.Vector3()
renderer.domElement.addEventListener('mousemove', evt => {
  const rect = evt.target.getBoundingClientRect()

  // Update the vector to use normalized screen coordinates [-1,1]
  pt.set(
    ((evt.clientX - rect.left) / rect.width) * 2 - 1,
    ((rect.top - evt.clientY) / rect.height) * 2 + 1,
    1
  )

  raycaster.setFromCamera(pt, camera)

  // Intersecting with the ground plane, where +Y is up
  const ground = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
  const pointHitsPlane = raycaster.ray.intersectPlane(ground, pt);
  if (pointHitsPlane) {
    // pt is now a position in the 3D world
    // move a global test object to that location to verify
    testObject.position.copy(pt)
  }
})

Хотя предоставленные ответы могут быть полезны в некоторых сценариях, я вряд ли могу представить эти сценарии (возможно, игры или анимации), потому что они не совсем точны (догадываясь о NDC z цели). Вы не можете использовать эти методы для проецирования экранных координат на мировые, если вы знаете целевую z-плоскость. Но для большинства сценариев вы должны знать этот самолет.

Например, если вы рисуете сферу по центру (известная точка в пространстве модели) и радиусу - вам нужно получить радиус как дельту непроецированных координат мыши - но вы не можете! При всем уважении метод @WestLangley с targetZ не работает, он дает неверные результаты (я могу предоставить jsfiddle, если необходимо). Другой пример - вам нужно установить цель управления орбитой двойным щелчком мыши, но без "реального" лучевого вещания с объектами сцены (когда вам нечего выбирать).

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

screenToWorld(v2D: THREE.Vector2, camera: THREE.PerspectiveCamera = null, target: THREE.Vector3 = null): THREE.Vector3 {
    const self = this;

    const vNdc = self.toNdc(v2D);
    return self.ndcToWorld(vNdc, camera, target);
}

//get normalized device cartesian coordinates (NDC) with center (0, 0) and ranging from (-1, -1) to (1, 1)
toNdc(v: THREE.Vector2): THREE.Vector2 {
    const self = this;

    const canvasEl = self.renderers.WebGL.domElement;

    const bounds = canvasEl.getBoundingClientRect();        

    let x = v.x - bounds.left;      

    let y = v.y - bounds.top;       

    x = (x / bounds.width) * 2 - 1;     

    y = - (y / bounds.height) * 2 + 1;      

    return new THREE.Vector2(x, y);     
}

ndcToWorld(vNdc: THREE.Vector2, camera: THREE.PerspectiveCamera = null, target: THREE.Vector3 = null): THREE.Vector3 {
    const self = this;      

    if (!camera) {
        camera = self.camera;
    }

    if (!target) {
        target = self.getTarget();
    }

    const position = camera.position.clone();

    const origin = self.scene.position.clone();

    const v3D = target.clone();

    self.raycaster.setFromCamera(vNdc, camera);

    const normal = new THREE.Vector3(0, 0, 1);

    const distance = normal.dot(origin.sub(v3D));       

    const plane = new THREE.Plane(normal, distance);

    self.raycaster.ray.intersectPlane(plane, v3D);

    return v3D; 
}

Вот мой взгляд на создание класса ES6 из него. Работа с Three.js R83. Метод использования rayCaster происходит от mrdoob здесь: Three.js Projector и Ray объекты

    export default class RaycasterHelper
    {
      constructor (camera, scene) {
        this.camera = camera
        this.scene = scene
        this.rayCaster = new THREE.Raycaster()
        this.tapPos3D = new THREE.Vector3()
        this.getIntersectsFromTap = this.getIntersectsFromTap.bind(this)
      }
      // objects arg below needs to be an array of Three objects in the scene 
      getIntersectsFromTap (tapX, tapY, objects) {
        this.tapPos3D.set((tapX / window.innerWidth) * 2 - 1, -(tapY / 
        window.innerHeight) * 2 + 1, 0.5) // z = 0.5 important!
        this.tapPos3D.unproject(this.camera)
        this.rayCaster.set(this.camera.position, 
        this.tapPos3D.sub(this.camera.position).normalize())
        return this.rayCaster.intersectObjects(objects, false)
      }
    }

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

var helper = new RaycasterHelper(camera, scene)
var intersects = helper.getIntersectsFromTap(tapX, tapY, 
this.scene.children)
...
Другие вопросы по тегам