鼠标/画布 X,Y 到 Three. js World X,Y,Z

我到处搜索了一个与我的用例匹配的示例,但是没有找到。我正在尝试将屏幕鼠标坐标转换成3D 世界坐标,同时考虑到摄像机。

我发现所有的解决方案都做光线交叉来实现物体拾取。

我要做的是将 Three. js 对象的中心定位在鼠标当前“在上”的坐标上。

我的相机在 x: 0,y: 0,z: 500(尽管在模拟过程中它会移动) ,我的所有对象都在 z = 0,x 和 y 值不同,所以我需要知道世界 X,Y 基于假设 z = 0的对象将跟随鼠标的位置。

这个问题看起来像一个类似的问题,但没有一个解决方案: 在 THREE.js 中获取鼠标相对于3D 空间的坐标

考虑到鼠标在屏幕上的位置范围是“ top-left = 0,0 | bottom-right = window.innerWidth,window.innerHeight”,有人能提供一个解决方案,将 Three. js 对象沿 z = 0移动到鼠标坐标吗?

70439 次浏览

使用 projectVector 获取3D 对象的鼠标坐标:

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;

要获得与特定鼠标坐标相关的三维坐标,使用相反的 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 );

有一个很好的例子 给你。但是,要使用项目向量,必须有一个用户单击的对象。相交将是一个数组的所有对象在鼠标的位置,无论其深度。

你不需要在你的场景中有任何对象来做到这一点。

你已经知道摄像头的位置了。

使用 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;

三个 JS R.98

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

ThreeJS is slowly mowing away from Projector.(Un)ProjectVector and the solution with projector.pickingRay() doesn't work anymore, just finished updating my own code.. so the most recent working version should be as follow:

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

下面是我根据 WestLangley 的回复编写的 ES6类,它在 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平面上的投影鼠标位置。

以下是我对如何用它创建一个 es6类的看法。使用 Three js r83。使用 rayCaster 的方法来自 mrdob: 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)
}
}

如果你想检查场景中所有对象的命中率,你可以这样使用它。我将上面的递归标志设置为 false,因为对于我的使用,我不需要它。

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

当我使用 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 3.js r. 89

Although the provided answers can be useful in some scenarios, I hardly can imagine those scenarios (maybe games or animations) because they are not precise at all (guessing around target's NDC z?). You can't use those methods to unproject screen coordinates to the world ones if you know target z-plane. But for the most scenarios, you should know this plane.

例如,如果您按中心绘制球体(模型空间中的已知点)和半径-您需要得到半径作为未投影鼠标坐标的增量-但是您不能!恕我直言,@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;
}

我有一块画布,比我的整个窗口还小,需要确定一次点击的世界坐标:

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

对于那些使用 @react-three/fiber(又名 r3f 和反应三纤维)的人,我发现这个讨论和 Matt Rossman 提供的相关代码示例很有帮助。特别是,许多使用上述方法的例子都是用于简单的正交视图,而不是用于使用 OrbitControls 时。

讨论: https://github.com/pmndrs/react-three-fiber/discussions/857

使用 Matt 技术的简单示例: 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