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

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.

Related

THREE.js orthographic camera zoom to mouse point

I'm working on an orthographic camera for our THREE.js app. Essentially, this camera will present the scene to the user in 2D (users have the option of switching between the 2D and 3D camera). This camera will allow for panning and zooming to mouse point. I have the panning working, and I have zooming working, but not zooming to mouse point. Here's my code:
import React from 'react';
import T from 'three';
let panDamper = 0.15;
let OrthoCamera = React.createClass({
getInitialState: function () {
return {
distance: 150,
position: { x: 8 * 12, y: 2 * 12, z: 20 * 12 },
};
},
getThreeCameraObject: function () {
return this.camera;
},
applyPan: function (x, y) { // Apply pan by changing the position of the camera
let newPosition = {
x: this.state.position.x + x * -1 * panDamper,
y: this.state.position.y + y * panDamper,
z: this.state.position.z
};
this.setState({position: newPosition});
},
applyDirectedZoom: function(x, y, z) {
let zoomChange = 10;
if(z < 0) zoomChange *= -1;
let newDistance = this.state.distance + zoomChange;
let mouse3D = {
x: ( x / window.innerWidth ) * 2 - 1,
y: -( y / window.innerHeight ) * 2 + 1
};
let newPositionVector = new T.Vector3(mouse3D.x, mouse3D.y, 0.5);
newPositionVector.unproject(this.camera);
newPositionVector.sub(this.camera.position);
let newPosition = {
x: newPositionVector.x,
y: newPositionVector.y,
z: this.state.position.z
};
this.setState({
distance: newDistance,
position: newPosition
});
},
render: function () {
let position = new T.Vector3(this.state.position.x, this.state.position.y, this.state.position.z);
let left = (this.state.distance / -2) * this.props.aspect + this.state.position.x;
let right = (this.state.distance / 2) * this.props.aspect + this.state.position.x;
let top = (this.state.distance / 2) + this.state.position.y;
let bottom = (this.state.distance / -2) + this.state.position.y;
// Using react-three-renderer
// https://github.com/toxicFork/react-three-renderer
return <orthographicCamera
{...(_.pick(this.props, ['near', 'far', 'name']))}
position={position}
left={left}
right={right}
top={top}
bottom={bottom}
ref={(camera) => this.camera = camera}/>
}
});
module.exports = OrthoCamera;
Some zooming towards the mouse point happens but it seems erratic. I want to keep a 2D view, so as I zoom, I also move the camera (rather than having a non-perpendicular target, which kills the 2D effect).
I took cues from this question. As far as I can tell, I am successfully converting to THREE.js coordinates in mouse3D (see the answer to this question).
So, given this setup, how can I smoothly zoom to the mouse point (mouse3D) using the orthographic camera and maintaining a two dimensional view? Thanks in advance.
Assuming you have a camera that is described by a position and a look-at (or pivot) point in world coordinates, zooming at (or away from) a specific point is quite simple at its core.
Your representation seems to be even simpler: just a position/distance pair. I didn't see a rotation component, so I'll assume your camera is meant to be a top-down orthographic one.
In that case, your look-at point (which you won't need) is simply (position.x, position.y - distance, position.z).
In the general case, all you need to do is move both the camera position and the look-at point towards the zoom-at point while preserving the camera normal (i.e. direction). Note that this will work regardless of projection type or camera rotation. EDIT (2020/05/01): When using an orthographic projection, this is not all you need to do (see update at the bottom).
If you think about it, this is exactly what happens when you're zooming at a point in 3D. You keep looking at the same direction, but you move ever closer (without ever reaching) your target.
If you want to zoom by a factor of 1.1 for example, you can imagine scaling the vector connecting your camera position to your zoom-at point by 1/1.1.
You can do that by simply interpolating:
var newPosition = new THREE.Vector3();
newPosition.x = (orgPosition.x - zoomAt.x) / zoomFactor + zoomAt.x;
newPosition.y = (orgPosition.y - zoomAt.y) / zoomFactor + zoomAt.y;
newPosition.z = (orgPosition.z - zoomAt.z) / zoomFactor + zoomAt.z;
As I said above, in your case you won't really need to update a look-at point and then calculate the new distance. Your new distance will simply be:
var newDistance = newPosition.y
That should do it.
It only gets a little bit more sophisticated (mainly in the general case) if you want to set minimum and maximum distance limits both between the position/look-at and position/zoom-at point pairs.
UPDATE (2020/05/01):
I just realized that the above, although correct (except for missing one minor but very important step) is not a complete answer to OP's question. Changing the camera's position in orthographic mode won't of course change the scale of graphics being rendered. For that, the camera's projection matrix will have to be updated (i.e. the left, right, top and bottom parameters of the orthographic projection will have to be changed).
For this reason, many graphics libraries include a scaling factor in their orthographic camera class, which does exactly that. I don't have experience with ThreeJS, but I think that property is called 'zoom'.
So, summing everything up:
var newPosition = new THREE.Vector3();
newPosition.x = (orgPosition.x - zoomAt.x) / zoomFactor + zoomAt.x;
newPosition.y = (orgPosition.y - zoomAt.y) / zoomFactor + zoomAt.y;
newPosition.z = (orgPosition.z - zoomAt.z) / zoomFactor + zoomAt.z;
myCamera.zoom = myCamera.zoom * zoomFactor
myCamera.updateProjectionMatrix()
If you want to use your orthographic camera class code above instead, you will probably have to change the section that computes left, right, top and bottom and add a scaling factor in the calculation. Here's an example:
var aspect = this.viewportWidth / this.viewportHeight
var dX = (this.right - this.left)
var dY = (this.top - this.bottom) / aspect
var left = -dX / (2 * this.scale)
var right = dX / (2 * this.scale)
var bottom = -dY / (2 * this.scale)
var top = dY / (2 * this.scale)
mat4.ortho(this.mProjection, left, right, bottom, top, this.near, this.far)

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

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.

Converting World particle coordinates to Screen coordinates in Three.js using Projection

I am trying to get the screen coordinates for the vertices in a particle system within three.js. I have been trying to do this using the following code from Three.js: converting 3d position to 2d screen position:
function toScreenXY( position, camera, div ) {
var pos = position.clone();
projScreenMat = new THREE.Matrix4();
projScreenMat.multiply( camera.projectionMatrix, camera.matrixWorldInverse );
projScreenMat.multiplyVector3( pos );
var offset = findOffset(div);
return {
x: ( pos.x + 1 ) * div.width / 2 + offset.left,
y: ( - pos.y + 1) * div.height / 2 + offset.top
};
}
function findOffset(element) {
var pos = new Object();
pos.left = pos.top = 0;
if (element.offsetParent)
{
do
{
pos.left += element.offsetLeft;
pos.top += element.offsetTop;
} while (element = element.offsetParent);
}
return pos;
}
I call this with the following information:
var test = toScreenXY(particleGeometry.vertices[i].clone(), camera, renderer.domElement);
However the coordinates that are returned do not match the screen coordinates.
There are other examples where this is discussed but they don't seem applicable (Converting World coordinates to Screen coordinates in Three.js using Projection) since the "object" referred to cannot be a Vector3D that defines the vertex.
What is an easy way to get the screen position of a particle in the particle system?

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.

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