Related
Using three.js I have the following.
A scene containing several Object3D instances
Several predefined camera Vector3 positions
A dynamic width/height of the canvas if the screen resizes
A user can select an object (from above)
A user can select a camera position (from above)
Given an object being viewed and the camera position they have chosen how do I compute the final camera position to "best fit" the object on screen?
If the camera positions are used "as is" on some screens the objects bleed over the edge of my viewport whilst others they appear smaller. I believe it is possible to fit the object to the camera frustum but haven't been able to find anything suitable.
I am assuming you are using a perspective camera.
You can set the camera's position, field-of-view, or both.
The following calculation is exact for an object that is a cube, so think in terms of the object's bounding box, aligned to face the camera.
If the camera is centered and viewing the cube head-on, define
dist = distance from the camera to the _closest face_ of the cube
and
height = height of the cube.
If you set the camera field-of-view as follows
fov = 2 * Math.atan( height / ( 2 * dist ) ) * ( 180 / Math.PI ); // in degrees
then the cube height will match the visible height.
At this point, you can back the camera up a bit, or increase the field-of-view a bit.
If the field-of-view is fixed, then use the above equation to solve for the distance.
EDIT: If you want the cube width to match the visible width, let aspect be the aspect ratio of the canvas ( canvas width divided by canvas height ), and set the camera field-of-view like so
fov = 2 * Math.atan( ( width / aspect ) / ( 2 * dist ) ) * ( 180 / Math.PI ); // in degrees
three.js r.69
Based on WestLangleys answer here is how you calculate the distance with a fixed camera field-of-view:
dist = height / 2 / Math.tan(Math.PI * fov / 360);
To calculate how far away to place your camera to fit an object to the screen, you can use this formula (in Javascript):
// Convert camera fov degrees to radians
var fov = camera.fov * ( Math.PI / 180 );
// Calculate the camera distance
var distance = Math.abs( objectSize / Math.sin( fov / 2 ) );
Where objectSize is the height or width of the object. For cube/sphere objects you can use either the height or width. For a non-cube/non-sphere object, where length or width is greater, use var objectSize = Math.max( width, height ) to get the larger value.
Note that if your object position isn't at 0, 0, 0, you need to adjust your camera position to include the offset.
Here's a CodePen showing this in action. The relevant lines:
var fov = cameraFov * ( Math.PI / 180 );
var objectSize = 0.6 + ( 0.5 * Math.sin( Date.now() * 0.001 ) );
var cameraPosition = new THREE.Vector3(
0,
sphereMesh.position.y + Math.abs( objectSize / Math.sin( fov / 2 ) ),
0
);
You can see that if you grab the window handle and resize it, the sphere still takes up 100% of the screen height. Additionally, the object is scaling up and down in a sine wave fashion (0.6 + ( 0.5 * Math.sin( Date.now() * 0.001 ) )), to show the camera position takes into account scale of the object.
Assuming that object fits into screen if it's bounding sphere fits, we reduce the task to fitting sphere into camera view.
In given example we keep PerspectiveCamera.fov constant while changing camera rotation to achieve best point of view for the object. Zoom effect is achieved by moving camera along .lookAt direction vector.
On the picture you can see problem definition:
given bounding sphere and camera.fov, find L, so that bounding sphere touches camera's frustum planes.
Here's how you calculate desired distance from sphere to camera:
Complete solution: https://jsfiddle.net/mmalex/h7wzvbkt/
var renderer;
var camera;
var scene;
var orbit;
var object1;
function zoomExtents() {
let vFoV = camera.getEffectiveFOV();
let hFoV = camera.fov * camera.aspect;
let FoV = Math.min(vFoV, hFoV);
let FoV2 = FoV / 2;
let dir = new THREE.Vector3();
camera.getWorldDirection(dir);
let bb = object1.geometry.boundingBox;
let bs = object1.geometry.boundingSphere;
let bsWorld = bs.center.clone();
object1.localToWorld(bsWorld);
let th = FoV2 * Math.PI / 180.0;
let sina = Math.sin(th);
let R = bs.radius;
let FL = R / sina;
let cameraDir = new THREE.Vector3();
camera.getWorldDirection(cameraDir);
let cameraOffs = cameraDir.clone();
cameraOffs.multiplyScalar(-FL);
let newCameraPos = bsWorld.clone().add(cameraOffs);
camera.position.copy(newCameraPos);
camera.lookAt(bsWorld);
orbit.target.copy(bsWorld);
orbit.update();
}
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(54, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.x = 15;
camera.position.y = 15;
camera.position.z = 15;
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(new THREE.Color(0xfefefe));
document.body.appendChild(renderer.domElement);
orbit = new THREE.OrbitControls(camera, renderer.domElement);
// create light
{
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(0, 100, 50);
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.near = 500;
spotLight.shadow.camera.far = 4000;
spotLight.shadow.camera.fov = 30;
scene.add(spotLight);
}
var root = new THREE.Object3D();
scene.add(root);
function CustomSinCurve(scale) {
THREE.Curve.call(this);
this.scale = (scale === undefined) ? 1 : scale;
}
CustomSinCurve.prototype = Object.create(THREE.Curve.prototype);
CustomSinCurve.prototype.constructor = CustomSinCurve;
CustomSinCurve.prototype.getPoint = function(t) {
var tx = t * 3 - 1.5;
var ty = Math.sin(2 * Math.PI * t);
var tz = 0;
return new THREE.Vector3(tx, ty, tz).multiplyScalar(this.scale);
};
var path = new CustomSinCurve(10);
var geometry = new THREE.TubeGeometry(path, 20, 2, 8, false);
var material = new THREE.MeshPhongMaterial({
color: 0x20f910,
transparent: true,
opacity: 0.75
});
object1 = new THREE.Mesh(geometry, material);
object1.geometry.computeBoundingBox();
object1.position.x = 22.3;
object1.position.y = 0.2;
object1.position.z = -1.1;
object1.rotation.x = Math.PI / 3;
object1.rotation.z = Math.PI / 4;
root.add(object1);
object1.geometry.computeBoundingSphere();
var geometry = new THREE.SphereGeometry(object1.geometry.boundingSphere.radius, 32, 32);
var material = new THREE.MeshBasicMaterial({
color: 0xffff00
});
material.transparent = true;
material.opacity = 0.35;
var sphere = new THREE.Mesh(geometry, material);
object1.add(sphere);
var size = 10;
var divisions = 10;
var gridHelper = new THREE.GridHelper(size, divisions);
scene.add(gridHelper);
var animate = function() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();
try this for OrbitControls
let padding = 48;
let w = Math.max(objectLength, objectWidth) + padding;
let h = objectHeight + padding;
let fovX = camera.fov * (aspectX / aspectY);
let fovY = camera.fov;
let distanceX = (w / 2) / Math.tan(Math.PI * fovX / 360) + (w / 2);
let distanceY = (h / 2) / Math.tan(Math.PI * fovY / 360) + (w / 2);
let distance = Math.max(distanceX, distanceY);
From user151496's suggestion about using the aspect ratio, this seems to work, although I've only tested with a few different parameter sets.
var maxDim = Math.max(w, h);
var aspectRatio = w / h;
var distance = maxDim/ 2 / aspectRatio / Math.tan(Math.PI * fov / 360);
I had the same question but I expected the object(s) (represented by a Box3 as a whole) could rotate on my phone if the whole was wider than my screen so I could view it by zooming in as near as possible.
const objectSizes = bboxMap.getSize();
console.log('centerPoint', centerPoint, bboxMap, objectSizes, tileMap);
//setupIsometricOrthographicCamera(bboxMap);
//https://gamedev.stackexchange.com/questions/43588/how-to-rotate-camera-centered-around-the-cameras-position
//https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
//https://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object
// Top
// +--------+
// Left | Camera | Right
// +--------+
// Bottom
// canvas.height/2 / disance = tan(fov); canvas.width/2 / disance = tan(fovLR);
// => canvas.width / canvas.height = tan(fovLR)/tan(fov);
// => tan(fovLR) = tan(fov) * aspectRatio;
//If rotating the camera around z-axis in local space by 90 degrees.
// Left
// +---+
// Bottom | | Top
// | |
// +---+
// Right
// => tan(fovLR) = tan(fov) / aspectRatio;
const padding = 0, fov = 50;
let aspectRatio = canvas.width / canvas.height;
let tanFOV = Math.tan(Math.PI * fov / 360);
let viewWidth = padding + objectSizes.x, viewHeight = padding + objectSizes.y;
//The distances are proportional to the view's with or height
let distanceH = viewWidth / 2 / (tanFOV * aspectRatio);
let distanceV = viewHeight / 2 / tanFOV;
const camera = this.camera = new THREE.PerspectiveCamera(fov, aspectRatio, 0.1, 10000); //VIEW_ANGLE, ASPECT, NEAR, FAR
if (aspectRatio > 1 != viewWidth > viewHeight) {
console.log('screen is more narrow than the objects to be viewed');
// viewWidth / canvas.width => viewHeight / canvas.width
// viewHeight / canvas.height => viewWidth / canvas.height;
distanceH *= viewHeight / viewWidth;
distanceV *= viewWidth / viewHeight;
camera.rotateZ(Math.PI / 2);
}
camera.position.z = Math.max(distanceH, distanceV) + bboxMap.max.z;
//camera.lookAt(tileMap.position);
I had tested two different aspect of Box3 on tow different orientations (landscape and portrait) using my phone, it worked well.
References
Box3.getSize ( target : Vector3 ) : Vector3
target — the result will be copied into this Vector3.
Returns the width, height and depth of this box.
Object3D.rotateZ ( rad : Float ) : this (PerspectiveCamera)
rad - the angle to rotate in radians.
Rotates the object around z axis in local space.
Other answers
I have a rotating sphere on which I have a div attached the example can be viewed here: https://jsfiddle.net/ao5wdm04/
I calculate the x and y values and place the div using a translate3d transform and that works quite well.
My question is how to can get the values for the rotateX, rotateY and rotateZ or rotate3d transforms so the div "tangents" the sphere surface. I know the cube mesh faces the sphere center so I assume the rotation vector of the outward facing normal vector in relation to the camera would contain the values I need. But I'm not quite sure how to obtain these.
Update
By using Euler angles I'm almost achieving the desired effect, shown here: https://jsfiddle.net/ao5wdm04/1/ but the rotation is not large enough.
Disclaimer: I know nothing about three.js. I've just done a bit of OpenGL.
Your euler angles are coming from a model-view-projected origin (lines 74-80). I can't see the logic behind this.
If your div is on the sphere surface, then it should be oriented by the normal of the sphere at the location of the div. Fortunately, you already have these angles. They are named rotation.
If you replace the euler angles in lines 82-84 with the rotation angles used to position the div, then in my browser the div appears edge on when it is at the edge of the circle, and face on when it is at the centre. It kind of looks like it is moving in a circle, edge on to the screen. Is this the effect you want?
My modification to the linked code:
82 var rotX = (rotation.x * (180/ Math.PI));
83 var rotY = (rotation.y * (180/ Math.PI));
84 var rotZ = 0;
EDIT
Ah, ok. The rotation variable is just that of the camera. It governs the tangent at the equator. You also need to modify the orientation to account for latitude.
Make rotY equal to negative your latitude. Then make sure that this rotation happens before the equatorial rotation. Rotations are not commutative.
In summary, changes from the code at https://jsfiddle.net/ao5wdm04/1/ are as follows:
27 var lat = 45 * Math.PI / 180;
...
82 var rotX = (rotation.x * (180/ Math.PI));
83 var rotY = - 45;
...
88 document.getElementById('face').style.webkitTransform = 'translate3d(' + x + 'px,' + y + 'px,0px) rotateY('+rotX+'deg) rotateX('+rotY+'deg)';
I don't know how the latitude should propagate between the init and render functions. As I said, I'm not familiar with the language.
For details about transformation and rotation in openGL or any other graphics please go through here.
Basic -
There is basically 2 kind of transformations in 3D world-
Translation
Rotation
A small example on this things are given here.
If u go through all of them, I think u have a clear concept on the 3D transformation system.
If u can understand those, u can easily simulate that :) because u need to do this 2 things for each move at the same time.
Try this code-
var camera, scene, renderer, raycaster, geometry, material, mesh, box;
var rotation = {
x: 0,
y: 0
};
var distance = 500;
init();
animate();
function init() {
raycaster = new THREE.Raycaster(); ;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 10000);
camera.position.z = distance;
camera.position.y = 100;
scene.add(camera);
geometry = new THREE.SphereGeometry(100, 50, 50, 50);
material = new THREE.MeshNormalMaterial();
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
var transform = new THREE.Matrix4().getInverse(scene.matrix);
var lat = 0 * Math.PI / 180;
var lon = 90 * Math.PI / 180;
var r = 100;
var p = new THREE.Vector3(-r * Math.cos(lat) * Math.cos(lon),
r * Math.sin(lat),
r * Math.cos(lat) * Math.sin(lon)
);
p.applyMatrix4(transform);
var geometry = new THREE.CubeGeometry(10, 10, 10);
box = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({
color: 0xff0000,
}));
box.position.set(p.x, p.y, p.z);
box.lookAt(mesh.position);
//scene.add(box);
box.updateMatrix();
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
rotation.x += 0.01;
camera.position.x = distance * Math.sin(rotation.x) * Math.cos(rotation.y);
camera.position.y = distance * Math.sin(rotation.y);
camera.position.z = distance * Math.cos(rotation.x) * Math.cos(rotation.y);
camera.lookAt(mesh.position);
var w = window.innerWidth;
var h = window.innerHeight;
var mat = new THREE.Matrix4();
var v = new THREE.Vector3();
mat.copy(scene.matrix);
mat.multiply(box.matrix);
v.set(0, 0, 0);
v.applyMatrix4(mat);
v.project(camera);
var euler = new THREE.Euler().setFromVector3(v);
var rotX = (rotation.x * (180/ Math.PI));
var rotY = (rotation.y * (180/ Math.PI));
var rotZ = 0;
var x = (w * (v.x + 1) / 2) - 12.5; //compensate the box size
var y = (h - h * (v.y + 1) / 2) - 12.5;
document.getElementById('face').style.webkitTransform = 'translate3d(' + x + 'px,' + y + 'px,0px) rotateX('+rotY+'deg) rotateY('+rotX+'deg) rotateZ('+rotZ+'deg)';
renderer.render(scene, camera);
}
#face {
position: absolute;
width: 25px;
height: 25px;
border-radius: 50%;
background-color: red;
}
<script src="https://rawgit.com/mrdoob/three.js/master/build/three.min.js"></script>
<div id="face"></div>
I would like to convert a THREE.CircleGeometry to a THREE.Shape, but I haven't found a way yet. I want to extrude this circle, but the ExtrudeGeometry() only works with shapes.
I know I can use curves to draw a circle but I want to keep the topology the CircleGeometry() gives.
If that's not possible, is there a work around to draw a circle shape which is made of a number of triangular segments that are oriented around a central point?
This will generate a shape as circle geometry does.
Here is fiddle: http://jsfiddle.net/tcLr7eg3/
var circleTri = new THREE.Shape();
radius = 30;
segments = 32;
var theta_next, x_next, y_next, j;
for (var i = 0; i < segments; i++) {
theta = ((i + 1) / segments) * Math.PI * 2.0;
x = radius * Math.cos(theta);
y = radius * Math.sin(theta);
j = i + 2;
if( (j - 1) === segments ) j = 1;
theta_next = (j / segments) * Math.PI * 2.0;
x_next = radius * Math.cos(theta_next);
y_next = radius * Math.sin(theta_next);
circleTri.moveTo(0, 0);
circleTri.lineTo(x, y);
circleTri.lineTo(x_next, y_next);
circleTri.lineTo(0, 0);
}
For anyone new to three.js looking to do this, you should try the SphereGeometry shape. You can create a perfectly round sphere this way without the hassle of creating your own shape or trying to extrude a CircleGeometry shape. The code example on the docs should give you a good starting point.
const geometry = new THREE.SphereGeometry( 5, 32, 32 );
const material = new THREE.MeshBasicMaterial( {color: 0xffff00} );
const sphere = new THREE.Mesh( geometry, material );
scene.add( sphere );
(new to stackoverflow, new to webgl/three.js, ...)
I'm using three.js r54 to plot a force-directed graph. the edges between the nodes are THREE.Lines, which is fine, but lines are not selectable with a raycaster. so my aim is to take cylinders instead(/along with) of lines(also because I can do some further stuff: using textures,...)
this is what I'm doing to place the cylinders:
// init reference vector
var upVec = new THREE.Vector3(0,1,0);
//---withhin a loop---
// get direction
var direction = startPoint.subSelf(endPoint).clone();
// half length for cylinder height
var halfLength = direction.length() * 0.5;
// get offset
var offset = endPoint.clone().addSelf(direction.clone().multiplyScalar(0.5));
// normalize direc
direction.normalize();
//newUpVec = upVec - (upVec *(dot) direction) * direction - projection of direction
var newUpVec = upVec.clone().subSelf(direction.clone().multiplyScalar(upVec.dot(direction.clone()))).normalize();
var right = newUpVec.clone().crossSelf(direction.clone());
//build rotation matrix
var rot = new THREE.Matrix4(right.x, right.y, right.z, 0,
newUpVec.x, newUpVec.y, newUpVec.z, 0,
direction.x, direction.y, direction.z,0,
0,0,0,1);
//build translation matrix
var transla = new THREE.Matrix4(1, 0, 0, offset.x,
0, 1, 0, offset.y,
0, 0, 1, offset.z,
0, 0, 0, 1);
//build transformation matrix
var transfo = new THREE.Matrix4().multiply(transla, rot);
// create geometry
var cylgeo = new THREE.CylinderGeometry(2, 2, halfLength * 2, 12, 1, false);
cylgeo.applyMatrix(transfo);
var cylMesh = new THREE.Mesh(cylgeo, new THREE.MeshLambertMaterial({color:0x000000,
wireframe: true, shading: THREE.FlatShading}));
(descripted in: http://www.fastgraph.com/makegames/3drotation/ )
So the cylinders are placed at the right offset and align in some kind of way, but not to the two points (start, end) of the edges.
any suggestion would be appreciated!
using that :
object3d-rotation-to-align-to-a-vector
given 2 Vector3 and a scene:
function drawCylinder(vstart, vend,scene){
var HALF_PI = +Math.PI * .5;
var distance = vstart.distanceTo(vend);
var position = vend.clone().addSelf(vstart).divideScalar(2);
var material = new THREE.MeshLambertMaterial({color:0x0000ff});
var cylinder = new THREE.CylinderGeometry(10,10,distance,10,10,false);
var orientation = new THREE.Matrix4();//a new orientation matrix to offset pivot
var offsetRotation = new THREE.Matrix4();//a matrix to fix pivot rotation
var offsetPosition = new THREE.Matrix4();//a matrix to fix pivot position
orientation.lookAt(vstart,vend,new THREE.Vector3(0,1,0));//look at destination
offsetRotation.rotateX(HALF_PI);//rotate 90 degs on X
orientation.multiplySelf(offsetRotation);//combine orientation with rotation transformations
cylinder.applyMatrix(orientation)
var mesh = new THREE.Mesh(cylinder,material);
mesh.position=position;
scene.add(mesh);
}
r58+ code :
function drawCylinder(vstart, vend,scene){
var HALF_PI = Math.PI * .5;
var distance = vstart.distanceTo(vend);
var position = vend.clone().add(vstart).divideScalar(2);
var material = new THREE.MeshLambertMaterial({color:0x0000ff});
var cylinder = new THREE.CylinderGeometry(10,10,distance,10,10,false);
var orientation = new THREE.Matrix4();//a new orientation matrix to offset pivot
var offsetRotation = new THREE.Matrix4();//a matrix to fix pivot rotation
var offsetPosition = new THREE.Matrix4();//a matrix to fix pivot position
orientation.lookAt(vstart,vend,new THREE.Vector3(0,1,0));//look at destination
offsetRotation.makeRotationX(HALF_PI);//rotate 90 degs on X
orientation.multiply(offsetRotation);//combine orientation with rotation transformations
cylinder.applyMatrix(orientation)
var mesh = new THREE.Mesh(cylinder,material);
mesh.position=position;
scene.add(mesh);
}
#jdregister's answer didn't quite work for me in R77, since the cylinder ended up with its center at vstart (rotation and lookAt were otherwise fine).
This modification to the second last line of the R58+ answer did the trick:
mesh.position.set(position.x, position.y, position.z);
There's a very succinct answer here: https://stackoverflow.com/a/44346439/1556416
I paraphrased it here:
function drawCylinder(vstart, vend, radius){
var cylLength = new THREE.Vector3().subVectors(vend, vstart).length();
var cylGeom = new THREE.CylinderGeometry(radius, radius, cylLength, 16);
cylGeom.translate(0, cylLength / 2, 0);
cylGeom.rotateX(Math.PI / 2);
var material = new THREE.MeshLambertMaterial({color: "blue"})
var cyl = new THREE.Mesh(cylGeom, material);
cyl.position.copy(vstart);
cyl.lookAt(vend); // and do the trick with orienation
return cyl
}
In R87 the "vend.clone().add(vstart).divideScalar(2);" is not working
You can position the item like this
mesh.position.copy(start);
mesh.position.lerp(end, 0.5);
All the others from R58 are fine :)
I've searched around for an example that matches my use case but cannot find one. I'm trying to convert screen mouse co-ordinates into 3D world co-ordinates taking into account the camera.
Solutions I've found all do ray intersection to achieve object picking.
What I am trying to do is position the center of a Three.js object at the co-ordinates that the mouse is currently "over".
My camera is at x:0, y:0, z:500 (although it will move during the simulation) and all my objects are at z = 0 with varying x and y values so I need to know the world X, Y based on assuming a z = 0 for the object that will follow the mouse position.
This question looks like a similar issue but doesn't have a solution: Getting coordinates of the mouse in relation to 3D space in THREE.js
Given the mouse position on screen with a range of "top-left = 0, 0 | bottom-right = window.innerWidth, window.innerHeight", can anyone provide a solution to move a Three.js object to the mouse co-ordinates along z = 0?
You do not need to have any objects in your scene to do this.
You already know the camera position.
Using vector.unproject( camera ) you can get a ray pointing in the direction you want.
You just need to extend that ray, from the camera position, until the z-coordinate of the tip of the ray is zero.
You can do that like so:
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 ) );
The variable pos is the position of the point in 3D space, "under the mouse", and in the plane z=0.
EDIT: If you need the point "under the mouse" and in the plane z = targetZ, replace the distance computation with:
var distance = ( targetZ - camera.position.z ) / vec.z;
three.js r.98
This worked for me when using an 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
In r.58 this code works for me:
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);
Below is an ES6 class I wrote based on WestLangley's reply, which works perfectly for me in THREE.js r77.
Note that it assumes your render viewport takes up your entire browser viewport.
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 ) );
}
}
You can use the class like this:
// 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 now contains the projected mouse position on the z=0 plane.
to get the mouse coordinates of a 3d object use 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;
to get the three.js 3D coordinates that relate to specific mouse coordinates, use the opposite, 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 );
There is a great example here. However, to use project vector, there must be an object where the user clicked. intersects will be an array of all objects at the location of the mouse, regardless of their depth.
I had a canvas that was smaller than my full window, and needed to determine the world coordinates of a click:
// 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);
Here's an example. Click the same region of the donut before and after sliding and you'll find the coords remain constant (check the browser console):
// 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>
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;
}
For those using #react-three/fiber (aka r3f and react-three-fiber), I found this discussion and it's associated code samples by Matt Rossman helpful. In particular, many examples using the methods above are for simple orthographic views, not for when OrbitControls are in play.
Discussion: https://github.com/pmndrs/react-three-fiber/discussions/857
Simple example using Matt's technique: https://codesandbox.io/s/r3f-mouse-to-world-elh73?file=/src/index.js
More generalizable example: https://codesandbox.io/s/react-three-draggable-cxu37?file=/src/App.js
Here is my take at creating an es6 class out of it. Working with Three.js r83. The method of using rayCaster comes from mrdoob here: Three.js Projector and Ray objects
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)
}
}
You would use it like this if you wanted to check against all your objects in the scene for hits. I made the recursive flag false above because for my uses I did not need it to be.
var helper = new RaycasterHelper(camera, scene)
var intersects = helper.getIntersectsFromTap(tapX, tapY,
this.scene.children)
...
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.
For example, if you draw sphere by center (known point in model space) and radius - you need to get radius as delta of unprojected mouse coordinates - but you can't! With all due respect #WestLangley's method with targetZ doesn't work, it gives incorrect results (I can provide jsfiddle if needed). Another example - you need to set orbit controls target by mouse double click, but without "real" raycasting with scene objects (when you have nothing to pick).
The solution for me is to just create the virtual plane in target point along z-axis and use raycasting with this plane afterward. Target point can be current orbit controls target or vertex of object you need to draw step by step in existing model space etc. This works perfectly and it is simple (example in 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;
}