How To Tween Camera Lookat
Recently, I faced a situation where I needed to create a navigable 3D business firm within a website. In this house, a user would be able to look around a room past clicking and dragging their signal of view, like to street view on Google Maps. Additionally, there would be clickable hotspots that would permit the user to "walk" over and look at a specific particular in the room. I came to realize this is no easy job, specially when it comes to camera movement. This site was built using the Javascript library 3.js on top of a React v6 unmarried page application. Here are a few things I learned the difficult way while creating this 3D house.
Click and drag camera
The outset issue that needed to be resolved was the click and elevate photographic camera motility. 3.js offers a couple of base camera views to get us started. The ane I went for was the Perspective Camera. This is a bully start, but the camera needed a couple of tweaks to make it move how I wanted.
First, out of the box the camera moves whenever the mouse moves on the page instead of when you click and elevate. This issue was fairly easy to resolve, but in resolving it, some of the chief functionality of the Perspective Camera was changed. In guild to movement the camera just when the mouse is held downward, we needed to tap into the document's mousedown
, mousemove
, and mouseup
event listeners, as shown below.
document.addEventListener('mousedown', onDocumentMouseDown, false); certificate.addEventListener('mousemove', onDocumentMouseMove, false); document.addEventListener('mouseup', onDocumentMouseUp, imitation);
This allowed me to telephone call specific methods when the browser detects the mouse being pressed, being moved, and being lifted respectfully. Inside the onDocumentMouseDown
function, I am noting the starting 10 and Y positions of the mouse for later, likewise as setting a variable called isUserInteracting
to true. This variable is set to false within the onDocumentMouseUp
method, thus tracking whether or not the user has the mouse held down. Subsequently, in the onDocumentMouseMove
method, zilch happens unless isUserInteracting
is ready to true. Now we can get into the logic of actually moving the camera.
By default, the camera motility in the Perspective Camera doesn't act the way I wanted it to. Information technology moves upwards when your mouse goes up, and down when you mouse goes down. Same with left and right. Yous can see an example below of this in a proof-of-concept 3D room that I fix.
For many use cases that people have using Three.js, this is perfectly acceptable. But, since I am trying to mimic an intuitive way someone might click and drag a camera in a room (similar to Google Maps), this would non suffice. So, the next affair that needed to be washed was to rewrite the camera logic.
I updated the camera logic by using Iii.js' camera.lookAt
function to look at different points in 3D space equally the user clicks and drags. To do this, X, Y, and Z coordinates for the points in 3D space demand to be calculated for each small movement of the mouse. First, in the onDocumentMouseMove
method, I accept the starting 10 and Y positions of the mouse click previously mentioned and practise some calculations with information technology. I too used the X and Y positions of where the mouse has moved to during the drag to determine what I am calling the latitude (lat
) and longitude (lon
). The calculations are shown below.
lon = (downPointer.x - event.clientX) * dragFactor + downPointer.lon; lat = (effect.clientY - downPointer.y) * dragFactor + downPointer.lat;
Now, I will break down how this calculation works. To calculate the longitude, nosotros start past calculating the difference between where the mouse was when the click happened (downPointer.10
) and where the mouse is currently in the dragging process (event.clientX
). This is then multiplied past what I call a dragFactor
. This is just a gene to modify how fast the camera moves when dragging. This was tweaked as needed, but I ended upward with 0.2. From there, downPointer.lon
is added to the sum. This variable is the value of what the previous lon
variable was after the terminal drag (initializing every bit 0).
The latitude is calculated in a similar way. Before moving forward, it is ensured that the lat
variable is between -85 and 85 degrees. This is to brand sure that the user tin can't look too far up or downwards and flip the camera in a strange manner. This is ensured past the post-obit code:
lat = Math.max(-85, Math.min(85, lat));
From there, I needed to ascertain two more variables, phi
and theta
, by converting lat
and lon
to radians. Additionally, lat needs to be start subtracted by 90 degrees.
const phi = THREE.Math.degToRad(ninety - lat); const theta = THREE.Math.degToRad(lon);
Finally, we are able the calculate the X, Y, and Z values of the 3D indicate we will be pointing the camera towards. This is done using trigonometry. The formulas are as follows:
photographic camera.target.x = radius * Math.sin(phi) * Math.cos(theta); photographic camera.target.y = radius * Math.cos(phi); camera.target.z = radius * Math.sin(phi) * Math.sin(theta);
There is ane variable shown above that I haven't talked nearly still: radius
. This was the trickiest variable to figure out how to calculate and will be talked well-nigh in item in the adjacent section.
The last thing that needs to be done is a elementary camera.lookAt(camera.target)
. This will be called for every small motion the mouse makes when being dragged and appear seamless, equally shown in the following gif.
Fixing camera weirdness
Now I'm going to get into how that radius variable is calculated and the issues I found along the mode to discover this.
Mathematically, the radius variable represents the radial distance the marker being looked at is from the camera. The formulas existence used to summate the Ten, Y, and Z coordinates are tried and true formulas in mathematics shown beneath, forth with a graphic that helps explain what each of the other variables represent.
x = r sin(ϕ) cos(θ)
y = r sin(ϕ) sin(θ)
z = r cos(ϕ)
In other blogs, articles, and Stack Overflow questions virtually Three.js camera movement, it is stated that the radius variable tin be a fixed number, such as 500 (example here). This works for the initial camera position, simply every bit soon equally the photographic camera moves to a hotspot mark, the math falls apart. Later the motion, the start point that is calculated when trying to drag the camera is off by a certain amount, causing a jump in the camera, shown in the gif below.
To remedy this upshot, the radius needs to be calculated initially and recalculated upon every camera movement. To calculate the correct length of the radius, the 3D version of the Pythagorean theorem tin be used: √(tenii + y2 + zii). In lawmaking, this looks like the post-obit using the Math.hypot
shortcut:
radius = Math.hypot(x, y, z);
The radius needs to be calculated initially, later each camera movement in the onDocumentMouseMove
method, and at the stop of the method moving the camera to a specific hotspot marker.
Now we're washed, right?! Wrong! This didn't fully solve the issue in all cases! Depending on the room or the mark/camera location, in that location would be even more weirdness with the camera. The click and drag would sometimes seem locked into a certain surface area, as shown in the gif below.
As you tin can see, fifty-fifty though the elevate is continuing to the left, the camera starts looking to the right. How can this be solved?
I discovered that this event was existence caused when the 3D bespeak being calculated to look at was too shut to the photographic camera position. This means that the radius variable is very small and acting weird when moving around the nearby camera position. The solution I figured out was to calculate a 3rd point along the same vector path of the camera position and the point calculated from the Pythagorean theorem, but much further away from the camera. Hither is the function I fabricated to calculate this new point.
const getNewPointOnVector = (p1, p2) => { permit distAway = 200; let vector = {x: p2.x - p1.x, y: p2.y - p1.y, z:p2.z - p1.z}; let vl = Math.sqrt(Math.pow(vector.ten, 2) + Math.pow(vector.y, 2) + Math.pow(vector.z, ii)); let vectorLength = {10: vector.10/vl, y: vector.y/vl, z: vector.z/vl}; let 5 = {x: distAway * vectorLength.x, y: distAway * vectorLength.y, z: distAway * vectorLength.z}; render {10: p2.ten + five.ten, y: p2.y + v.y, z: p2.z + v.z}; }
Using this newly calculated point for the camera to look at, my photographic camera dragging issues were solved.
Rotation vs Quaternion
The last issue that I came across when creating this 3D room experience is changing the camera orientation when clicking on a hotspot mark. I originally implemented this by modifying the camera's rotation and position using Tween.js, which seemed like a logical solution. Tween.js is a not bad and like shooting fish in a barrel to apply tweening engine for animations. Tweening the position worked but fine, but tweening the rotation of the photographic camera proved to be problematic in certain cases. The camera would spiral to its new orientation, which looked boundless. This turns out to be a problem known equally gimbal lock. An example is shown beneath.
To fix this, I researched the correct way to modify orientation in 3D infinite to avert gimbal lock. In this inquiry, I discovered something chosen quaternions. Quaternions are used pretty regularly in 3D computer graphics to calculate iii-dimensional rotations. Mathematically, a quaternion is a circuitous number with four dimensions. Without going into more item nigh what a quaternion is, let's but focus on how information technology solved my issue. Quaternions provide a much quicker and more effective manner to transition the orientation of the Three.js photographic camera compared to its rotation property. Originally, using rotation, the part to move to a marker was as follows.
office cameraToMarker(marker) { const currentCamPosition = {ten: marking.cameraPositionX, y: camera.position.y, z: marker.cameraPositionZ}; const storedMarkerPosition = new THREE.Vector3(marker.positionX, marking.positionY, marking.positionZ); const newCameraTarget = getNewPointOnVector(currentCamPosition, storedMarkerPosition); const markerPosition = new THREE.Vector3(...Object.values(newCameraTarget)); const startRotation = new THREE.Euler().re-create(camera.rotation); camera.lookAt(storedMarkerPosition); const endRotation = new THREE.Euler().copy(photographic camera.rotation); camera.rotation.copy(startRotation); new TWEEN.Tween(camera.rotation) .to( { x: endRotation.x, y: endRotation.y, z: endRotation.z, }, 500) .easing(TWEEN.Easing.Quadratic.InOut) .onComplete(() => { new TWEEN.Tween(camera.position) .to({ 10: marker.cameraPositionX, y: photographic camera.position.y, z: marking.cameraPositionZ, }) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { camera.lookAt(storedMarkerPosition); }) .onComplete(() => { camera.lookAt(storedMarkerPosition); radius = Math.hypot(...Object.values(markerPosition)); phi = Math.acos(markerPosition.y / radius); theta = Math.atan2(markerPosition.z, markerPosition.10); lon = THREE.Math.radToDeg(theta); lat = 90 - THREE.Math.radToDeg(phi); }) .showtime(); }) .start(); }
As you can see, I was tweening the rotation to the endRotation
variable, which contained the camera.rotation
after using photographic camera.lookAt
to get the rotation value after looking at the marker position. The call to camera.lookAt
doesn't actually expect at that position because, two lines later, I am copying the photographic camera rotation back to its original value. Later on, I was tweening the camera.position
, which was working just fine.
There are another noteworthy things almost this function. You can come across that I am generating a newCameraTarget
using the getNewPointOnVector
function described in the previous section. Additionally, I am setting the radius
, phi
, theta
, lon
, and lat
once the animation is consummate and then that the click and drag functionality works as expected after the camera movement.
The update to utilize quaternions was unproblematic enough. All that was needed to be done was to change the variables startRotation
and endRotation
to startQuaternion
and endQuaternion
and become their values using camera.quaternion.clone()
instead of the THREE.Euler
. Problem solved, correct? Wrong over again!
There was ane more result that I notwithstanding faced later on all of this! Occasionally, the photographic camera motility would whip to one side before correctly setting the orientation. This was acquired by tweening between quaternions, or slowly changing one quaternion value into another. The way quaternions are supposed to exist altered is through "spherical linear interpolation", or slerp. Conveniently, Iii.js has a slerp method we can tap into. I and then changed the outer tween described above to the following.
permit fourth dimension = {t: 0}; new TWEEN.Tween(time) .to({t: 1}, 500) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { THREE.Quaternion.slerp(startQuaternion, endQuaternion, camera.quaternion, time.t); })
Tweening an arbitrary time variable from 0 to ane over 500 milliseconds and calling Three.js' slerp method in the onUpdate
method is what resolved this issue. Have a await at how the photographic camera moves after these updates.
I have covered each of the most painstaking issues I encountered when working with Three.js. As I was working, I kept wishing at that place was a web log specifically outlining these bug, and now there is. I hope this blog mail allows you to make awesome Three.js camera movements the easy mode.
READY TO GROW YOUR CAREER?
At Perficient, nosotros continually look for ways to champion and challenge our talented workforce with interesting projects for high-contour clients, encourage personal and professional person growth through training and mentoring, and celebrate our people-oriented culture and the innovative means they serve Perficient and the customs.
Learn more near what it's like to work at Perficient at our Careers folio . See open up jobs or join our talent customs for career tips, task openings, company updates, and more!
Get inside Life at Perficient and connect with us on LinkedIn, YouTube, Twitter, and Instagram.
Source: https://blogs.perficient.com/2020/05/21/3d-camera-movement-in-three-js-i-learned-the-hard-way-so-you-dont-have-to/
Posted by: williamsontheresobted.blogspot.com
0 Response to "How To Tween Camera Lookat"
Post a Comment