Three.js boids - improving mouse detection with a changing camera position - javascript

I've been working with three.js examples of boids/flocks for some time, but both the canvas one and the webgl/shaders one have a flaw: the mouseOver event (which "disturbs" birds and triggers a repulsion) only works when camera.position = {x: 0, y: 0,: whatever}.
I've tried to improve that in the canvas example (easier to my eyes) by editing this part:
function onDocumentMouseMove( event ) {
var vector = new THREE.Vector3( event.clientX - SCREEN_WIDTH_HALF, - event.clientY + SCREEN_HEIGHT_HALF, 0 );
for ( var i = 0, il = boids.length; i < il; i++ ) {
boid = boids[ i ];
vector.z = boid.position.z;
boid.repulse( vector );
}
}
And trying something like:
function onDocumentMouseMove( event ) {
var vector = new THREE.Vector3();
vector.x = event.clientX;
vector.y = - event.clientY;
vector.unproject(camera);
for ( var i = 0, il = boids.length; i < il; i++ ) {
boid = boids[ i ];
vector.z = boid.position.z;
boid.repulse( vector );
}
}
But this can't work, the unprojected vector could only be used with a raycaster to find objects intersecting its path. In our case, the repulsion effect must work at 150 distance, according to boid.repulse:
this.repulse = function ( target ) {
var distance = this.position.distanceTo( target );
if ( distance < 150 ) {
var steer = new THREE.Vector3();
steer.subVectors( this.position, target );
steer.multiplyScalar( 0.5 / distance );
_acceleration.add( steer );
}
}
So I'm stuck. Should I find a way to widen the raycaster so it's like a 150-wide cylinder for mouse picking? Or is there a way to unproject the vector then re-project it on the plane nearest to the bird, so to calculate the distance? (but what about performance with 200+ birds? )
If the solution can only come from shaders, feel free to tell me to create another topic.
Included: jsfiddle of the canvas example with a slightly moved camera.

Related

THREE.Raycaster inaccurate results

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

Click to place camera near an object in Three js

My I am trying to do a click to zoom feature with Three.js, I have a canvas and an object loaded in the canvas.On click I am trying to place the camera near the point of intersection(Actually like zooming that point).
Here is what I have done, but doesn't work as I wanted, on click camera positions changes but kind of works partially sometimes camera is placed near the point of intersection, some times not.
onmousedown = function (event) {
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
event.preventDefault();
mouse.x = (event.clientX / self.renderer.domElement.clientWidth) * 2 - 1;
mouse.y = -(event.clientY / self.renderer.domElement.clientHeight) * 2 + 1;
raycaster.setFromCamera(mouse, self.camera);
var objects = [];
for (var i = 0; i < self.scene.children.length; i++) {
if (self.scene.children[i] instanceof THREE.Group) {
objects.push(self.scene.children[i]);
}
}
console.log(objects);
var intersects = raycaster.intersectObjects( objects,true );
console.log(intersects.length);
if (intersects.length > 0) {
self.camera.up = new THREE.Vector3(0, 0, 1);
self.camera.lookAt(new THREE.Vector3(0, 0, 0));
self.camera.position.z = intersects[0].point.z * .9;
self.camera.position.x = intersects[0].point.x * .9;
self.camera.position.y = intersects[0].point.y * .9;
}
};
Here self is a global viewer object which holds camera, canvas, different objects etc.
0.9 is just a number used to place camera just near the point of intersection.
camera used is PerspectiveCamera and controls is TrackballControls
new THREE.PerspectiveCamera(90, this.width / this.height, 1, 1000);
The objects loaded are from .obj or .dae files ,I expect this to work like click on any point on the object and place the camera near that point. But camera is moving but sometimes not near the point I clicked.
Does intersects[0] gives the nearest intersection point? or nearest in the direction of camera ?
What is my mistake here ?
I am new to three js , just started learning it.If something or some logic is wrong help me with that.
The position is a bit complicated to calculate; you have to find the segment between camera and intersection and than place the camera at specific distance from intersection along the segment looking to the intersection point.
try this:
var length=[the desiderated distance camera-intersection]
var dir = camera.position.clone().sub(intersects[0].point).normalize().multiplyScalar(length);
camera.position = intersects[0].point.clone().add(dir);
camera.lookAt(intersects[0].point);
I have created a fiddle: http://jsfiddle.net/h5my29aL/
It's not so difficult. Think of your object as a planet, and your camera as a satellite. You need to position the camera somewhere in an orbit near your object. Three contains a distanceTo function that makes it simple. The example uses a sphere, but it will work with an arbitrary mesh. It measures the distance from the center point to the desired vector3. In your case the vector3 is likely the face position returned by a picker ray. But anyhow, the lookAt is set to the mesh, and then a distance from the vertex is calculated so that the camera is always the same altitude regardless of a vertex's or face's distance from the object center.
var point = THREE.GeometryUtils.randomPointsInGeometry( geometry, 1 );
var altitude = 100;
var rad = mesh.position.distanceTo( point[0] );
var coeff = 1+ altitude/rad;
camera.position.x = point[0].x * coeff;
camera.position.y = point[0].y * coeff;
camera.position.z = point[0].z * coeff;
camera.lookAt(mesh.position);
I've came somewhat close to what I want with an example from Three js.
Three JS webgl_decals
this is what I have done.
function zoomCam(event) {
var point_mouse = new THREE.Vector2(),
var point_x = null;
var point_y = null;
if (event.changedTouches) {
point_x = event.changedTouches[ 0 ].pageX;
point_y = event.changedTouches[ 0 ].pageY;
} else {
point_x = event.clientX;
point_y = event.clientY;
}
point_mouse.x = (point_x / window.innerWidth) * 2 - 1;
point_mouse.y = -(point_y / window.innerHeight) * 2 + 1;
if (sceneObjects.length > 0) {
var raycaster = new THREE.Raycaster();
raycaster.setFromCamera(point_mouse, camera);
var intersects = raycaster.intersectObjects(sceneObjects, true);
if (intersects.length > 0) {
var p = intersects[ 0 ].point;
var n = intersects[ 0 ].face.normal.clone();
n.multiplyScalar(10);
n.add(intersects[ 0 ].point);
camera.position.copy(n);
camera.lookAt(p);
}
}
There might be some minor issues as I formatted/changed the code for answering here. Check the code before implementing.

Three.js 2D X, Y coords to 3D X, Y, Z coords always in front of camera

I've looked at this question:
Mouse / Canvas X, Y to Three.js World X, Y, Z
and have implemented it in my code, the problem is that I can't seem to get it to work as others have stated.
I need to place an object in front of the camera via X and Y screen coords, not necessarily from the mouse.
This object will be at a specified distance in front of the camera, either from a pre-defined maximum distance or a calculated object distance.
Note: My Camera can be at any position in my scene and facing any direction
Here is my code:
this.reticlePos.x = 500;
this.reticlePos.y = 300;
this.reticlePos.z = 0.5;
this.projector.unprojectVector(this.reticlePos, this.camera);
//check for collisions, if collisions set distance to collision distance
var distance = this.RETICLE_RADIUS;
var direction = new THREE.Vector3( 0, 0, -1 );
direction.applyQuaternion( this.camera.quaternion );
this.rayCaster.set(this.camera.position, direction);
var collisionResults = this.rayCaster.intersectObjects(this.sceneController.obstacles);
if( collisionResults.length !== 0 ) {
// console.log('Ray collides with mesh. Distance :' + collisionResults[0].distance);
distance = collisionResults[0].distance - 20;
}
// set the reticle position
var dir = this.reticlePos.clone().sub( this.camera.position ).normalize();
var pos = this.camera.position.clone().add( dir.multiplyScalar( distance ) );
this.reticleMesh.position.copy( pos );
As you can see is is very similar to the linked question, yet I cannot see the object in front of my camera.
Any insight into this would be greatly appreciated.
I figured out the answer myself for anyone that checks this question later on.
To get my screen coordinates I needed to convert some input data from a gyroscope, and didn't realize that I still needed to do the following to my calculated screen coordinates:
this.reticlePos.x = ( this.reticlePos.x / window.innerWidth ) * 2 - 1;
this.reticlePos.y = - ( this.reticlePos.y / window.innerHeight ) * 2 + 1;
After doing this, everything works as expected.

Compute top,left,width,height in Three.js projetion

I'm struggling with a problem: I need to position a DIV over a WebGL animation. I rotate a mesh, based on a PlaneGeometry to occupy a rectangular size, then I'd like top position there the DIV, so I need to know what is the X,Y coordinate and the rendered dimensions of the plane.
I've tried the THREE.Projection class, but didn't help, even if I projected the [0] verticle using .projectVector. It computed:
x: -0.1994540991160383
y: 0.17936202821347358
z: 0.9970982652556688
...which was little help to me.
To project a 3D point position to screen coordinates, relative to the renderer's canvas, do this:
var projector = new THREE.Projector();
var pos = projector.projectVector( position, camera );
var xcoord = Math.round( ( pos.x + 1 ) * canvas.width / 2 );
var ycoord = Math.round( ( -pos.y + 1 ) * canvas.height / 2 );
where canvas is, in this case, renderer.domElement.
A point in the upper left corner of your visible world will project to ( 0, 0 ).
three.js r.53
I found the solution. The top left point is indeed the 0 index of the plane's vertices, but you have to take into account the already performed transformations as well.
function calculateLayer()
{
// multiplyVector3 applies the performed transformations to the object's coordinates.
var topLeft = tubeCard.matrixWorld.multiplyVector3( tubeCard.geometry.vertices[0].clone() );
// index 24 is the bottom right, because the plane has 4x4 faces
var bottomRight = tubeCard.matrixWorld.multiplyVector3( tubeCard.geometry.vertices[24].clone() );
return {
topLeft: convert3Dto2D( topLeft ),
bottomRight: convert3Dto2D( bottomRight )
}
}
function convert3Dto2D( positionVector )
{
var projector = new THREE.Projector();
var pos = projector.projectVector( positionVector, camera );
var xcoord = Math.round( ( pos.x + 1 ) * window.innerWidth / 2 );
var ycoord = Math.round( ( -pos.y + 1 ) * window.innerHeight / 2 );
return { x: xcoord, y: ycoord };
}
So once you have the correct coordinates, applied with the transformations, you just have to use the 3d to 2d conversion thanks to WestLangley.

Can I have the ray collision position (X,Y) in targeted face relative coordinate?

I'm trying to find the ray collision coordinate relative to the face targeted...
code:
var fMouseX = (iX / oCanvas.width) * 2 - 1;
var fMouseY = -(iY / oCanvas.height) * 2 + 1;
//I Use OrthographicCamera
var vecOrigin = new THREE.Vector3( fMouseX, fMouseY, - 1 );
var vecTarget = new THREE.Vector3( fMouseX, fMouseY, 1 );
oProjector.unprojectVector( vecOrigin, this.__oCamera );
oProjector.unprojectVector( vecTarget, this.__oCamera );
vecTarget.subSelf( vecOrigin ).normalize();
var oRay = new THREE.Ray(vecOrigin, vecTarget);
intersects = oRay.intersectObjects([ oCylinderMesh ]);
With intersects[ 0 ].point, I can have the mouse position in 'screen coordinate', but how can I have it in Cylinder coordinate ?
PS: mesh are not rotate, but camera can change position...
Really nice framework ;)
Here is my solution, just get the Cylinder absolute coordinate (position relative to screen), then, itersects[0].point sub the Cylinder absolute coordinate.
The following code may help:
var relativeTo = function(element, ancestor) {
var offset = element.position.clone();
if (element.parent == ancestor) {
return offset;
}
return offset.addSelf(relativeTo(element.parent, ancestor));
}

Categories