web-dev-qa-db-fra.com

Souris/Toile X, Y à Three.js Monde X, Y, Z

J'ai cherché un exemple qui correspond à mon cas d'utilisation, mais ne parvient pas à en trouver un. J'essaie de convertir les coordonnées de la souris à l'écran en coordonnées du monde 3D en tenant compte de la caméra.

Les solutions que j'ai trouvées font toutes une intersection de rayons pour permettre la sélection d'objets.

Ce que j'essaie de faire est de positionner le centre d'un objet Three.js aux coordonnées que la souris est actuellement "sur".

Ma caméra est à x: 0, y: 0, z: 500 (bien qu'elle bouge pendant la simulation) et tous mes objets sont à z = 0 avec des valeurs variables x et y, donc j'ai besoin de connaître le monde en fonction de X, Y en supposant que az = 0 pour l'objet qui suivra la position de la souris.

Cette question ressemble à un problème similaire mais n'a pas de solution: Obtenir les coordonnées de la souris par rapport à l'espace 3D dans THREE.js

Étant donné la position de la souris à l’écran avec une plage de "haut à gauche = 0, 0 | en bas à droite = window.innerWidth, window.innerHeight", peut-on trouver une solution pour déplacer un objet Three.js aux coordonnées de la souris le long de z = 0?

51
Rob Evans

Pour ce faire, vous n'avez besoin d'aucun objet dans votre scène.

Vous connaissez déjà la position de la caméra.

En utilisant vector.unproject( camera ), vous pouvez faire pointer un rayon dans la direction souhaitée.

Il vous suffit d’étendre ce rayon depuis la position de la caméra jusqu’à ce que la coordonnée z de l’extrémité du rayon soit égale à zéro.

Vous pouvez faire ça comme ça:

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 ) );

La variable pos est la position du point dans l'espace 3D "sous la souris" et dans le plan z=0.


EDIT: Si vous avez besoin du point "sous la souris" et du plan z = targetZ, remplacez le calcul de la distance par:

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

trois.js r.98

104
WestLangley

En r.58 ce code fonctionne pour moi:

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);
5
lazymf

pour obtenir les coordonnées de la souris d'un objet 3d, utilisez 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;

pour obtenir les coordonnées 3D three.js associées à des coordonnées de souris spécifiques, utilisez le contraire, unrojectVector:

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 );

Il y a un excellent exemple ici . Cependant, pour utiliser le vecteur de projet, il doit exister un objet sur lequel l'utilisateur a cliqué. les intersections seront un tableau de tous les objets à l'emplacement de la souris, quelle que soit leur profondeur.

2
BishopZ

Cela a fonctionné pour moi avec un 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 trois.js r.89

2
KTCO

ThreeJS se détache lentement de Projector. (Un) ProjectVector et la solution avec printer.pickingRay () ne fonctionnent plus, je viens juste de mettre à jour mon propre code .. la version la plus récente devrait donc être la suivante:

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;
}
1
Pete Kozak

Ci-dessous, une classe ES6 que j'ai écrite et basée sur la réponse de WestLangley, qui me convient parfaitement dans THREE.js r77.

Notez que cela suppose que votre fenêtre de rendu occupe toute la fenêtre de votre navigateur.

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 ) );
    }
}

Vous pouvez utiliser la classe comme ceci:

// 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 contient maintenant la position projetée de la souris sur le plan z = 0.

1
drone1

Bien que les réponses fournies puissent être utiles dans certains scénarios, je peux difficilement imaginer ces scénarios (peut-être des jeux ou des animations) car ils ne sont pas du tout précis (devinez-vous autour du NDC z de la cible?). Vous ne pouvez pas utiliser ces méthodes pour annuler la projection des coordonnées de l'écran avec celles du monde si vous connaissez le plan z cible. Mais pour la plupart des scénarios, vous devriez connaître ce plan. 

Par exemple, si vous dessinez une sphère par centre (point connu dans l'espace objet) et par rayon, vous devez obtenir le rayon en tant que delta des coordonnées de la souris non projetées, mais vous ne pouvez pas! Avec tout le respect que je vous dois, la méthode de WestLangley avec targetZ ne fonctionne pas, elle donne des résultats incorrects (je peux fournir jsfiddle si nécessaire). Autre exemple - vous devez définir les commandes d’orbite cibles en double-cliquant sur la souris, mais sans "réel" raycasting avec des objets de scène (lorsque vous n’avez rien à choisir).

La solution pour moi consiste simplement à créer le plan virtuel au point cible le long de l’axe des z et à utiliser ensuite le raycasting avec ce plan. Le point cible peut être la commande d'orbite actuelle ou le sommet de l'objet que vous devez dessiner étape par étape dans l'espace objet existant, etc. Cela fonctionne parfaitement et est simple (exemple dans TypeScript):

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; 
}
0
SalientBrain

J'avais une toile qui était plus petite que ma fenêtre complète et j'avais besoin de déterminer les coordonnées mondiales d'un clic:

// 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);

Voici un exemple. Cliquez sur la même région du beignet avant et après le glissement et vous constaterez que les coordonnées restent constantes (consultez la console du navigateur):

// 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>
0
duhaime

Voici ma conception de la création d’une classe es6. Travailler avec Three.js r83. La méthode d’utilisation de rayCaster vient de mrdoob ici: Projecteur Three.js et objets 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)
      }
    }

Vous l'utiliseriez comme ceci si vous souhaitiez rechercher tous les objets de la scène. J'ai fait le drapeau récursif faux ci-dessus parce que pour mes utilisations je n'en avais pas besoin. 

var helper = new RaycasterHelper(camera, scene)
var intersects = helper.getIntersectsFromTap(tapX, tapY, 
this.scene.children)
...
0
Matt Thompson