I have a 3D scatter plot with spheres to represent the points and I'm trying to display information from the points when clicked. Based on answers to a couple different questions here on SO, I think I'm on the right track. Here is my onCanvasMouseDown:
event.preventDefault();
mouse.x = ( (event.clientX - renderer.domElement.offsetLeft) / renderer.domElement.width ) * 2 - 1;
mouse.y = - ( (event.clientY - renderer.domElement.offsetTop) / renderer.domElement.height ) * 2 + 1;
mouse.z = 0.5;
projector.unprojectVector( mouse, camera);
raycaster.setFromCamera( mouse, camera);
var intersects = raycaster.intersectObjects( scene.children );
if( intersects.length > 0 ) {
intersects.map(function(d) {
if(d.object.name === "sphere") {
//etc...
}
})
}
But intersects keeps coming back empty. I have a feeling the problem has something to do with the way I'm setting up the mouse coordinates, but I'm not sure how to resolve the issue.
EDIT: I seem to be able to find a sphere if I rotate the plot so that I am looking at it from above (but I need to be able to detect it regardless of the rotation). Does this indicate anything specific?
If your objects (points) belong to, for example, THREE.Group(), which is a child of the scene, then
var intersects = raycaster.intersectObjects( scene.children );
will give you negative result of intersection.
To reach group's children it's better to use
var intersects = raycaster.intersectObjects( scene.children, true );
Or you can try a different option: put the objects you want to check for intersection into an array and then pass the array to intersectObjects method.
var objects = [];
for(i=0; i<10; i++){
...
var meshobj = new THREE.Mesh(...);
scene.add(meshobj); // or group.add(meshobj);
objects.push(meshobj);
}
...
var intersects = raycaster.intersectObjects( objects ); // here you don't need recursive parameter, as you have the array with objects you indeed want to check for intersection
if you have offset, use the the boundingRect.
var rect = renderer.domElement.getBoundingClientRect();
mouse.x = ( ( event.clientX - rect.left ) / ( rect.width - rect.left ) ) * 2 - 1;
mouse.y = - ( ( event.clientY - rect.top ) / ( rect.bottom - rect.top) ) * 2 + 1;
see the demo of my answer.
Related
I'm making a 2D scatterplot with a tooltip, and currently the raycaster to detect when a point is being hovered over is broken. The tooltip only activates when touching an object, which is correct behavior, but it shows completely random data from points that aren't even close on the x/y plane, and changes information even when there are no other points close to the one being hovered over. Can anyone help me debug this behavior? Here is some relevant code (the rest can be found in the link above):
...loading in points (stored in data_points array), creating scene, etc.
raycaster = new THREE.Raycaster();
raycaster.params.Mesh.threshold = 20;
view.on("mousemove", () => {
let [mouseX, mouseY] = d3.mouse(view.node());
let mouse_position = [mouseX, mouseY];
checkIntersects(mouse_position);
});
function mouseToThree(mouseX, mouseY) {
return new THREE.Vector3(
mouseX / viz_width * 2 - 1,
-(mouseY / height) * 2 + 1,
1
);
}
function checkIntersects(mouse_position) {
let mouse_vector = mouseToThree(...mouse_position);
raycaster.setFromCamera(mouse_vector, camera);
let intersects = raycaster.intersectObjects(scene.children, true);
if (intersects[0]) {
let sorted_intersects = sortIntersectsByDistanceToRay(intersects);
let intersect = sorted_intersects[0];
let index = intersect.faceIndex;
let datum = data_points[index];
showTooltip(mouse_position, datum);
} else {
hideTooltip();
}
}
function sortIntersectsByDistanceToRay(intersects) {
return _.sortBy(intersects, "distanceToRay");
}
...tooltip functions, details
Any help would be greatly appreciated. Thank you!
Why are you using d3.mouse(view.node()); to get the mouse position? It looks like that's giving you wild results. When moving the pointer in a tiny space, I get an X range from 2200 to -97, when it should be a few pixels apart.
I recommend that on mousemove you get the exact XY screen position by using the default JavaScript method of event.clientX and event.clientY
See this example, taken directly from a Three.js Raycasting example
function onMouseMove( event ) {
mouse.x = ( event.clientX / renderer.domElement.clientWidth ) * 2 - 1;
mouse.y = - ( event.clientY / renderer.domElement.clientHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, camera );
}
Also, I recommend removing document margins via CSS so your measurements aren't off by a few pixels.
Figured out the answer. There was not a proper "index" variable for my datapoints (THREE.Group consisting of [THREE.Mesh, THREE.LineLoop]), which is why the raycasting worked but not point selection (DON'T use faceIndex). So I created one under the userData field of the mesh.
// Create circle geometries
for (var i=0; i<data_points.length; i++) {
// Circle
let geo = new THREE.CircleBufferGeometry(data_points[i].radius, 32);
let mat = new THREE.MeshBasicMaterial( {color: color_array[data_points[i].label] } );
let mesh = new THREE.Mesh(geo, mat);
mesh.userData.id = i;
...lineLoop and Group code
}
...more code
function onMouseMove(event) {
mouseRay.x = ( event.clientX / renderer.domElement.clientWidth ) * 2 - 1;
mouseRay.y = - ( event.clientY / renderer.domElement.clientHeight ) * 2 + 1;
mouseRay.z = 1;
let mouse = [event.clientX, event.clientY];
raycaster.setFromCamera(mouseRay, camera);
let intersects = raycaster.intersectObjects(scene.children, true);
if (intersects[0]) {
let sorted_intersects = sortIntersectsByDistanceToRay(intersects);
console.log(sorted_intersects);
let intersect = sorted_intersects[0];
// Here is the change I made!!!
let index = intersect.object.userData.id;
let datum = data_points[index];
highlightPoint(datum);
showTooltip(mouse, datum);
} else {
removeHighlights();
hideTooltip();
}
}
Is there any way to restrict the panning movement of a camera in scene?
Tried altering the pan method in orbitControls but I'm not really satisfied with the result, I wish there was more convenient/proper way to do it..
if ( scope.object instanceof THREE.PerspectiveCamera ) {
// perspective
var position = scope.object.position;
var offset = position.clone().sub( scope.target );
var targetDistance = offset.length();
// half of the fov is center to top of screen
targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 );
// we actually don't use screenWidth, since perspective camera is fixed to screen height
var dist_l = ( 2 * deltaX * targetDistance / screenHeight );
var dist_u = ( 2 * deltaY * targetDistance / screenHeight );
/////// X,Y limit calculation //////
var limit = 100;
if( (position.x - dist_l) <= -limit ){
dist_l = -0.1;
}else if( (position.x - dist_l) >= limit){
dist_l = 0.1;
}
if( (position.z - dist_u) <= -limit ){
dist_u = -0.1;
}else if( (position.z - dist_u) >= (limit*2.5) ){
dist_u = 0.1;
}
/////// X,Y limit calculation //////
scope.panLeft( dist_l );
scope.panUp( dist_u );
} else if ( scope.object instanceof THREE.OrthographicCamera ) {
// orthographic
scope.panLeft( deltaX * ( scope.object.right - scope.object.left ) / screenWidth );
scope.panUp( deltaY * ( scope.object.top - scope.object.bottom ) / screenHeight );
}
I have encountered the same problem. The solution is not to touch the pan() function but to check the limits in the update() function. Locate the line 162:
// move target to panned location
scope.target.add( panOffset );
Do your limit calculations right after this line:
if (scope.target.x > 1000)
scope.target.setX(1000);
if (scope.target.x < 0)
scope.target.setX (0);
...
This will clamp the target x-position. It works quite smoothly.
I have the exact same problem and thanks to David's solution, which gives me a lot of inspiration. I've some add up to David's answer:
If we only set target X, when keep panning to that limit, I have some unwanted rotation effect. This is because OrbitControls is working with 2 things: the target and the camera. To solve that, we need to set both target and the camera.
scope.target.setX(0);
camera.position.setX(0);
In this way, we guarantee the camera is always on the top of the object, hence no unwanted rotation happens.
If we want to keep the current rotation angle, we need to do some math. For example in my case, I only enabled the polar rotation:
let polarAngle = scope.getPolarAngle();
scope.target.set(0, camera.position.y + camera.position.z * Math.tan(polarAngle), 0);
camera.position.setX(0);
The idea is to set both target and camera position, but don't try to change the rotation angle. If there is rotation, do some math to calculate the target position first.
I'm working on someone else's project to finish it, and it's pretty cool. It is supposed to be a diagram that will dynamically generate nodes(spheres) and be able to click on them and then a new diagram with related nodes will appear.
I am working on the clic kable part now, it seems harder than i thought. I have read into raycasting and vectors. I have even found examples that get pretty close to what i want: http://mrdoob.github.io/three.js/examples/canvas_interactive_cubes.html
BUT, for some reason when i hover next to the nodes (the hover spots seem very arbitrary, as highlighted in the image) they change color. For some most of them i cant even find the "hotspots".
I think i lack some understanding of the unprojecting/converting/transformation from 3d to 2d part. And maybe that's why my mouse doesn't intersect properly with the nodes on screen i guess.
My onmousemove event:
function onDocumentMouseMove2(event){
event.preventDefault();
var canvasSvg= d3.select("#canvas-svg")[0][0];
mouse.x = ( event.clientX / canvasSvg.clientWidth) * 2 - 1;
mouse.y = - ( event.clientY / canvasSvg.clientHeight ) * 2 + 1;
var vector = new THREE.Vector3(mouse.x, mouse.y, 1).unproject(camera);
//raycaster.setFromCamera( mouse, camera );
raycaster.set(camera.position, vector.sub(camera.position).normalize());
var intersects = raycaster.intersectObjects( scene.children );
//console logs
console.log("current 'canvas' div");
console.log(canvasSvg)
console.log("mouse");
console.log(mouse);
console.log("vector");
console.log(vector);
if ( intersects.length > 0 ) {
intersects[0].object.material.color.setHex( Math.random() * 0xffffff ); //gives another color to the node i hover over
intersects[0].object.callback(); //this calls the funcion "callback" i attached to the nodes.
}
for ( var i in intersects ) {
intersects[ i ].object.material.color.setHex( Math.random() * 0xffffff | 0x80000000 );
}
}
My camera
// Set camera attributes and create camera
var VIEW_ANGLE = 7, //field of view
ASPECT = WIDTH / HEIGHT,
//ASPECT = $container[0].clientWidth / $container[0].clientHeight,
NEAR = 0.1,
FAR = 10000;
var camera = new THREE.PerspectiveCamera(VIEW_ANGLE, ASPECT, NEAR, FAR);
http://i.imgur.com/wWlEYQT.png?1
Good day! I managed to fix it with this example:
http://jsfiddle.net/fek9ddg5/62/
mouse.x = ( ( event.clientX - renderer.domElement.offsetLeft ) / renderer.domElement.width ) * 2 - 1;
mouse.y = - ( ( event.clientY - renderer.domElement.offsetTop ) / renderer.domElement.height ) * 2 + 1;
The ray was indeed confused on where it had to be projected. I had to take note of the offSet from the top of thescreen to my canvas.
I have created a sphere in three.js which has around 140 circles on its surface.
Have a look here if you like!
By clicking or touching a circle the user can change its colour.
Its a bit laggy on mobile devices though - is there any way of optimizing the following code?
function onMouseDown( e ) {
mouseVector.x = 2 * (e.clientX / (window.innerWidth*0.8)) - 1;
mouseVector.y = 1 - 2 * ( e.clientY / window.innerHeight );
raycaster.setFromCamera( mouseVector, camera );
var intersects = raycaster.intersectObjects( circles );
if ( intersects.length > 0 ) {
var intersect = intersects[ 0 ];
pickedColor=parseInt(document.getElementById("color").value.replace('#', '0x'));
intersect.object.material.color = new THREE.Color( pickedColor );
render();
}
}
Honestly, i tried to find solution for question below, please help me. And sorry for my English :)
I have created a map, added a camera + added camera control of all dimensions.
And when I get the position of the cursor relative to the mesh, the position is always different from different angles.
View all code in jsfiddle.net: http://jsfiddle.net/BT3g3/
I use Three.js r49.
This code is just responsible for calculating the position.
function onDocumentMouseMove(event) {
var vector = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, - ( event.clientY / window.innerHeight ) * 2 + 1, 1);
projector.unprojectVector( vector, camera );
var ray = new THREE.Ray( camera.position, vector.subSelf( camera.position ).normalize() );
var intersect = ray.intersectObject( island );
if ( intersect.length > 0) { // !!
document.getElementById('z').value = intersect[0].point.z;
document.getElementById('x').value = intersect[0].point.x;
}
}
I was browsing internet and came across an article where the cursor position is caught on the map, but i fail move this method to my project :(
Please, help!
Have you tried with following ?
mouseX = ( ( event.clientX - canvas.offsetLeft ) / canvas.clientWidth ) * 2 - 1;
mouseY = - ( ( event.clientY - canvas.offsetTop ) / canvas.clientHeight ) * 2 + 1;