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