Efficient way to translate a camera dolly - three.js

I currently have a VR camera attached to a dolly in order to allow for translation and rotation.
I'm trying to translate the dolly, based on gamepad inputs, relative to the orientation of the camera (which is linked to a VR headset.)
I'm also trying to avoid letting the dolly pitch up or down relative to the camera.
My current code looks something like this:
this.camerDirectionVector = new THREE.Vector3()
this.camera.getWorldDirection(this.cameraDirectionVector)
this.moveVec.y = 0
this.dolly.translateOnAxis(this.cameraDirectionVector, this.gamepad.axes[0] * this.moveSpeed)
This works great for moving the dolly in the direction the camera is pointing (minus y rotation).
What I can't figure out is how to also translate the dolly "left and right" relative to the camera based off an additional gamepad input.

Based on the comments on the question, I think I understand. If I don't, please leave a comment, and I'll update this answer.
My understanding is that you want to be able to move left and right, with respect to the camera, all without altering the dolly's up direction.
This is actually easier than it sounds, and is even easier because you are already comfortable translating along an axis.
First, understand that the camera has its own spatial frame of reference, where it sits at the origin, with a +Y up direction, and it looks down the -Z axis. With this in mind, you already know the "left" and "right" axes: -X (-1, 0, 0) and +X (1, 0, 0).
But the camera (especially in VR) might not be so nicely aligned in world space, so you need to convert these nice uniform axes into world axes. Three.js makes this very easy using Object3D.localToWorld.
(Note: Object3D.localToWorld is destructive to the input Vector3.)
Here's a function to get the world-aligned left axis:
const getLeftWorld = (function(){
const localLeft = new THREE.Vector3(-1, 0, 0);
return function(vectorRef){ // you can give it a vector to overwrite
let out = vectorRef || new THREE.Vector3();
out.copy(localLeft);
camera.localToWorld(out);
return out;
};
))();
You can create a similar function for the "right" axis.
With your new world-aligned left axis in hand, you can translate the dolly along it, using the "speed" given by your controller input. Translation won't change the pitch of the dolly, though it may change the elevation, depending on how the camera is tipped at the time of computation (but you can just zero-out the y component like you did before, if you want).

This is the solution that ended up working best for me. I've adapted it from Brian Peiris code here: https://github.com/brianpeiris/three-firstperson-vr-controls/blob/master/FirstPersonVRControls.js#L125
// Create a dolly
this.dolly = new THREE.Group()
this.dolly.add(this.camera)
this.scene.add(this.dolly)
// Some variables for movement translations
this.dummy = new THREE.Object3D()
this.dummyDirection = new THREE.Vector3()
this.ZAXIS = new THREE.Vector3(0, 0, 1)
this.moveSpeed = 0.075
// Get controller stick positions
const stickForwardBack = this.leftStick[3]
const stickLeftRight = this.leftStick[2]
// In THREE.js when using the WebXR API, the camera rotation will not update to match the headset orientation.
// You'll need to get pose information from the XRSession or get the xr camera using the following method.
const xrCamera = globals.renderer.xr.getCamera(this.camera)
this.dummy.position.set(0, 0, 0)
this.dummy.quaternion.copy(xrCamera.quaternion)
this.collapseY(this.dummy.quaternion)
// Translate the dummy object forwards/backwards left/right relative to the direction the camera is facing
this.dummy.translateZ(stickForwardBack * this.moveSpeed)
this.dummy.translateX(stickLeftRight * this.moveSpeed)
// Add the dummy position to the dolly
this.dolly.position.add(this.dummy.position)
// Flatten out up and down rotation
collapseY(quaternion) {
this.dummyDirection.set(0, 0, 1)
this.dummyDirection.applyQuaternion(quaternion)
this.dummyDirection.y = 0
this.dummyDirection.normalize()
quaternion.setFromUnitVectors(this.ZAXIS, this.dummyDirection)
}

Related

THREEjs create an intersection plane for a raycast with negative origin

I have a THREEJS scene with an object that 'looks at my mouse'. This works fine and I am using a raycast to get the mouse position like so:
this.intersectionPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 10);
this.raycaster = new THREE.Raycaster();
this.mouse = new THREE.Vector2();
this.pointOfIntersection = new THREE.Vector3();
On the mouse-move event I lookAt the pointOfIntersection vector and the object rotates. This works really well.
onDocumentMouseMove = (event) => {
event.preventDefault();
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
this.raycaster.setFromCamera(this.mouse, this.camera);
this.raycaster.ray.intersectPlane(this.intersectionPlane, this.pointOfIntersection);
let v3 = new THREE.Vector3(this.pointOfIntersection.x*0.05, this.pointOfIntersection.y*0.05, this.pointOfIntersection.z);
if(this.pebbleLogo){
this.pebbleLogo.lookAt(v3);
// console.log(v3);
}
if(this.videoWall){
this.videoWall.lookAt(v3);
}
}
BUT, I want to do the same thing with another object that lives at a z-depth of -20 and the camera flies through to this position. At this point, it also flies through the intersectionPlane and the raycast no longer works.
The intersectionPlane is not added to the scene so it doesn't have a position that I can move so how do I make sure that it stays with the camera?
I can see that the plane has two properties:
normal - (optional) a unit length Vector3 defining the normal of the plane. Default is (1, 0, 0).
constant - (optional) the signed distance from the origin to the plane. Default is 0.
I have been able to move the Plane using a translate but this is not ideal as I need the plane to be in a constant position in relation to the camera (just in front of it). I tried to make the plane a child of the camera but it didn't seem to make any difference to its position.
Any help appreciated.
When you perform renderer.render(scene, cam), the engine updates the transformation matrices of all objects that need to be rendered. However, since your camera and plane are not descendants of the scene, you'll have to manually update these matrices. The plane doesn't know that it's parent camera has moved, so you might need to perform plane.updateMatrix(). You can read about manually updating transformation matrices in the docs.
I think since only the parent moves, you might need to use updateMatrixWorld() or updateWorldMatrix() instead. But one of these 3 options should work.
Edit
Upon re-reading your code, it looks like you're using a purely Mathematical THREE.Plane object. This is not an Object3D, which means it cannot be added as a child of anything, so it doesn't behave as a regular object.
My answer assumed you were using a Mesh with PlaneGeometry, which is an Object3D, and it can be added as a child of the camera.

How to programmatically undo positional translation to pivot point?

I think this is ultimately a pretty simple question, but it's hard to describe, thus, I provide a working example here (in the sample press 'z' to see rotation with unwanted translation and 'x' keys to rotate with a compensating re-position).
Basically, I am trying to rotate an object (a thumbstick) about the z-axis of a complex model loaded via gltf (a model of the oculus rift touch controller). It's easy to rotate about the x-axis because it's 90 deg. orthogonal to the x-axis. About the z-axis, it's harder because the plane the thumbstick is attached to is angled at 30 deg. I realize that if the thumbstick were using local coordinates, this wouldn't be a problem, but 'thumb.rotation.z' does not seem to be using local coordinates and is rotating about the model's (as a whole), or maybe even the scene's global y and z (?). Anyway, after a bunch of futzing around, I was able to get things to work by doing the following:
// occulus plane is angle at 30 deg, which corresponds to
// 5 units forward to 3 units down.
var axis = new THREE.Vector3(0, 5, -3).normalize();
factory.thumbstick.geometry.center();
var dir = (evt.key === 'x' ? 1 : -1);
thumb.rotateOnAxis(axis, factory.ONE_DEG * 5.0 * dir);
Basically, I'm rotating about a "tilted" axis, and then calling 'center' to make thumbstick centered on the pivot point, so it rotates about the pivot point, rather than around the pivot point (like the earth orbiting the sun).
Only problem is that when you call 'geometry.center()' and then call 'rotateOnAxis', it translates the thumbstick to the pivot point:
Note: the position on the thumbstick object is (0,0,0) before and after the calls.
I have empirically determined that if I alter the position of the thumbstick after the translation like so:
// magic numbers compensating position
var zDisp = 0.0475;
var yDisp = zDisp / 6.0
thumb.position.x = 0.001;
thumb.position.y = -yDisp;
thumb.position.z = zDisp;
Then it (almost) returns back to it's original position:
Problem is these numbers were just determined by interactively and repeatedly trying to re-position the thumbstick i.e. empirically. I simply cannot find a programmatic, analytical, api kind of way to restore the original position. Note: saving the original position doesn't work, because it's zero before and after the translation. Some of the things I tried were taking the difference between the bounding spheres of the global object and the thumbstick object, trying to come up with some 'sin x- cos x' relation on one distance etc. but nothing works.
My question is, how can I progammatically reverse the offset due to calling 'geometry.center()' and rotateOnAxis (which translates to the pivot point), without having to resort to hacked, empircal "magic" numbers, that could conceivably change if the gltf model changes.
Of course, if someone can also come up with a better way to achieve this rotation, that would be great too.
What's throwing me is the (peceived?) complexity of the gltf model itself. It's confusing because I have a hard time interpreting it and it's various parts: I'm really not sure where the "center" is, and in certain cases, it appears with the 'THREE.AxesHelper' I'm attaching that what it shows as 'y' is actually 'z' and sometimes 'up' is really 'down' etc, and it gets confusing fast.
Any help would be appreciated.
The breakthrough for me on this was to re-frame the problem as how do I change the pivot point for the thumbstick, rather than how do I move the thumbstick to the (default and pre-existing) pivot point. To paraphrase JFK, "ask not how you can move to the pivot, but ask how the pivot can move to you" :-)
After changing my angle of attack, I pretty quickly found the aforementioned link, which yielded my solution.
I posted an updated glitch here, so now pressing z works as I expected. Here is the relevant code portion:
factory.onModelLoaded = function(evt) {
console.log(`onModelLoaded: entered`);
factory.thumbstick = this.scene.children[1].children[2]
let thumb = factory.thumbstick;
// make the thumb red so it's easier to see
thumb.material = (new THREE.MeshBasicMaterial({color: 0xFF7777}));
// use method from https://stackoverflow.com/questions/28848863/threejs-how-to-rotate-around-objects-own-center-instead-of-world-center/28860849#28860849
// to translate the pivot point of the thumbstick to the the thumbstick center
factory.thumbParent = thumb.parent;
let thumbParent = factory.thumbParent;
thumbParent.remove(thumb);
var box = new THREE.Box3().setFromObject( thumb );
box.getCenter( thumb.position ); // this basically yields my prev. "magic numbers"
// thumb.position.multiplyScalar( - 1 );
var pivot = new THREE.Group();
thumbParent.add( pivot );
pivot.add( thumb );
thumb.geometry.center();
// add axeshelp after centering, otherwise the axes help, as a child of thumb,
// will increase the bounding box of thumb, and positioning will be wrong.
axesHelper = new THREE.AxesHelper();
thumb.add(axesHelper);
}
Which allows my "z" handler to just rotate without having to do translation:
case 'z':
case 'Z':
var axis = new THREE.Vector3(0, 5, -3).normalize();
var dir = (evt.key === 'z' ? 1 : -1);
thumb.rotateOnAxis(axis, factory.ONE_DEG * 5.0 * dir);
break;
Interestingly, it's the call to box.getCenter() that generates numbers very close to my "magic numbers":
box.getCenter()
Vector3 {x: 0.001487499801442027, y: -0.007357006114165027, z: 0.04779449797522323}
My empirical guess was {x: 0.001, y: -0.00791666666, z: 0.0475} which is %error {x: 32.7%, y: 7.6%, z: 0.61%}, so I was pretty close esp. on the z component, but still not the "perfect" numbers of box.getCenter().

Three.js - change camera POV on click

Some project background:
I have a Sprite particle field that is randomly generated. The camera is located at position 0, 0, 0. The particle field is all around the camera. I'm using Raycaster to be able to select the particle that is clicked on and change it's color. Once clicked I would like the camera to focus on this particle. I'm also attempting to use Tween to glide the particle into view.
I've attempted several different methods and none of them work. They are described here:
A traditional lookAt method that used Raycaster to pick up the intersect point from clicking.
var raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, this.camera);
var intersects = raycaster.intersectObjects( this.starfield.children );
this.camera.lookAt(intersects[0].object.position)
A distanceTo method where the distance between the camera and the intersect coordinates is used to move the camera. This only moves the camera along the z plane. It wont actually change its POV.
var cameraPosition = new THREE.Vector3(this.camera.position.x, this.camera.position.y, this.camera.position.z);
var intersectPosition = new THREE.Vector3(intersects[0].object.position.x, intersects[0].object.position.y , intersects[0].object.position.z );
var zoomPos = intersectPosition.distanceTo( cameraPosition );
const newCameraPosition = cameraPosition.addVectors(this.camera.position, vector.setLength(zoomPos));
I calculated the angle of rotation for each X, Y, and Z axis via tan and cos equations. I then attempted to rotate the camera by those degrees. I even tried converting them to radians to see if that would make a difference with the rotation method. It didnt :(
I don't know what else to do. At this stage I'm completely open to a different approach as long as I get this camera working. I'm very stuck,
any help would be greatly appreciated!
Instead of using
intersects[0].object.position
try using
intersects[0].point
.point is the world space position of the hit.
.objectis the object the triangle belongs to. .object.position is just the origin of that object, in this case the particle system. The particle positions themselves are relative to this origin.

THREE.js checking how close the camera is to a mesh

I have a mesh landscape in THREE.js that the camera points down at, I'd like to maintain a certain distance from that mesh (so if there's peaks in the terrain the camera moves further away).
I thought raycasting would be the correct way to start going about this (by getting the intersection distance) but all the examples I find relate to using mouse co-ordinates; when I try to set the origin as the camera position, and the direction co-ords to be the camera position but with a 0 on the Y axis (so camera up in the air facing down) the intersect results come up empty.
For example, on the render event I have:
t.o.ray.vector = new THREE.Vector3(t.o.camera.position.x, 0, t.o.camera.position.z );
t.o.ray.cast = new THREE.Raycaster(t.o.camera.position,t.o.ray.vector );
t.o.ray.intersect = t.o.ray.cast.intersectObject(object, true);
console.log(t.o.ray.intersect);
This results in an empty array, even when I'm moving the camera, the only way I can seem to get this to work is by using the examples that rely on mouse events.
Any ideas what I'm missing?
I realised it was because that setting 0 as the Y property was not enough. I had assumed that the vector co-ordinate simply helped calculate the direction in which the ray was pointing in, but this doesn't seem to be the case. i.e. -:
t.o.ray.vector = new THREE.Vector3(t.o.camera.position.x, -1000, t.o.camera.position.z );
t.o.ray.vector.normalize();
t.o.ray.cast = new THREE.Raycaster(t.o.camera.position,t.o.ray.vector );
t.o.ray.intersect = t.o.ray.cast.intersectObject(t.Terrain.terrain, true);
Produces the expected results.
What about this approach:
console.log( t.o.camera.position.distanceTo(t.o.vector.position) );

Spawn particle at edge of screen

I've searched far and wide, so if there's a similar question please forgive me but I just couldn't find it.
To put what I'm trying to do in context: I want to create an infinitely-generated field of stars that disappear as they go offscreen and reappear at the edge of the screen where the camera is moving. I'm working with a top-down view, so it must be pretty simple to achieve this, but alas I haven't a clue.
I'm using the following code to determine whether a star has gone off-screen and then replace it:
//update camera frustum
camera.projScreenMatrix.multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
camera.frustum.setFromMatrix(camera.projScreenMatrix);
//loop through stars
var stars=scene.stars.geometry.vertices;
for(var i=0;i<stars.length;i++) {
if(!camera.frustum.containsPoint(stars[i])) {
stars[i]=new THREE.Vector3(
// fill in the blank
);
scene.stars.geometry.verticesNeedUpdate=true;
}
}
Since I'm using a perspective camera, I know I'll need to somehow factor in camera.fov and other perspective elements, but as you can tell I'm no expert on the third dimension.
Assuming I have an angle or normalized vector telling me the direction the view is panning, how would I go about creating a vertex along the edge of the screen regardless of its Z position?
If I'm not clear enough, I'll be happy to clarify. Thanks.
I know this is an old question, but I came across it while looking for an answer and found a simple, trigonometry reliant method to get the left edge of the camera frustum, and I'm sharing it in case someone else might find it useful:
// Get half of the cameras field of view angle in radians
var fov = camera.fov / 180 * Math.PI / 2;
// Get the adjacent to calculate the opposite
// This assumes you are looking at the scene
var adjacent = camera.position.distanceTo( scene.position );
// Use trig to get the leftmost point (tangent = o / a)
var left = Math.tan( fov ) * adjacent * camera.aspect;
Basically, this gets the leftmost point, but if you don't multiply by the aspect ratio you should get a point in a circle around your camera frustum, so you could translate a point any direction away from the cameras focus and it would always be outside the frustum.
It works by assuming that the imaginary plane that is the camera is perpendicular to the line connecting the camera and its focus, so there is a straight angle. This should work if you want objects further away as well (so if you want them at a further point from the camera you just need to increase the distance between the focus and the camera).
Well, countless headaches and another question later, I've come up with a fairly makeshift answer. Just in case by some unlikely chance someone else has the same question, the following function plots a point on the scene relative to the camera's current view with whatever Z specified:
//only needs to be defined once
var projector=new THREE.Projector();
//input THREE.Vector3
function(vector) {
var z=vector.z;
vector.z=0;
projector.unprojectVector(vector,camera);
return camera.position.clone().add(
vector
.sub(camera.position)
.normalize()
.multiplyScalar(
-(camera.position.z-z)/vector.z
)
);
The x and y, in this case, both range from -1 to 1 for bottom-left to top-right. You can use position/window.Width and position/window.Height for extra precision (using mouse coordinates or what have you).

Resources