Let's say I have a shape at position.x = 0 and I want to smoothly animate it in the render loop to position.x = 2.435. How would I go about that?
You can use the THREE AnimationMixer. The function below sets up the animation. Example jsFiddle.
const createMoveAnimation = ({ mesh, startPosition, endPosition }) => {
mesh.userData.mixer = new AnimationMixer(mesh);
let track = new VectorKeyframeTrack(
'.position',
[0, 1],
[
startPosition.x,
startPosition.y,
startPosition.z,
endPosition.x,
endPosition.y,
endPosition.z,
]
);
const animationClip = new AnimationClip(null, 5, [track]);
const animationAction = mesh.userData.mixer.clipAction(animationClip);
animationAction.setLoop(LoopOnce);
animationAction.play();
mesh.userData.clock = new Clock();
this.animationsObjects.push(mesh);
};
Set your target position as a variable (outside the render loop):
var targetPositionX = 2.435;
Then in your render loop, create an if statement that checks if the object's X position is less than the targetPositionX. If it is , it will add an increment (which you can change based on how fast you want it to move) to the object's X position. When the object's X position becomes greater or equal to the targetPositionX, it will stop moving.
Something like this:
if (object.position.x <= targetPositionX) {
object.position.x += 0.001; // You decide on the increment, higher value will mean the objects moves faster
}
Here is the full code for the render loop:
function loop(){
// render the scene
renderer.render(scene, camera);
// Check the object's X position
if (object.position.x <= targetPositionX) {
object.position.x += 0.001; // You decide on the increment, higher value will mean the objects moves faster
}
// call the loop function again
requestAnimationFrame(loop);
}
Side note
For more detailed/complex animations you may want to look into Tween.js for Three.js which makes animation easier and also allows you to add easing functions to your animation.
You can find it here:
https://github.com/sole/tween.js
I would recommend reading into it if you are getting into Three.js.
Related
I am new to THREEJS and at the moment I am trying to move a cube using arrow keys. Please see this fiddle: https://jsfiddle.net/mauricederegt/y6cw7foj/26/
All works, I can move the cube using the arrow keys and I even managed to rotate the cube around the correct axis when moving it around. The problem is with the animations. I can’t seem to get them to work. At the moment when you press the left arrow key, the cube moves to the left and also rolls around the axis. Well…at the moment it snaps into position, instead of smoothly transitioning.
What I want is that it smoothly moves to the left while it rotates, but how to do that? At the end of the code I do call for the
requestAnimationFrame
but that doesn’t do much. I have a fiddle here of my attempt doing this with CSS. Here the animations work (but never got the rotating direction correct): https://jsfiddle.net/mauricederegt/5ozqg9uL/3/ This does show what animations I want to have.
So what am I missing in the THREEjs? Thanks a lot
What you're looking for is something called "tweening," where you draw intermediary steps, rather than jumping immediately to the end result. There are several JavaScript libraries that will do this for you, but I'll cover some of the basics of implementing it yourself.
Take your example. When you tap the left arrow, you move the mesh 1 unit along the -x axis, and rotate -PI/2 about the +y axis. Rather than snapping to these positions/rotations, consider how long you want the animation to take, and start dividing out steps.
Let's say you want to to take 500ms (half a second). Your browser tries to run at about 60fps, so you have 30 frames (about 500ms) to work with at that rate. So for every frame, you can move the box 1/30 units, and rotate it by -PI/60. After 30 frames, the box should be in about the right place, give or take some rounding.
I use "about" when talking about the framerate of the browser because you aren't always guaranteed to get 60FPS. If your framerates dip, and you're locked to the framerate to draw your animation, then it too will slow down and take longer than you wanted. So what can be done about that?
Rather than relying on requestAnimationFrame as your timer, you can set a real timer to step through your animation. Toss calculating the frames you need to complete the animation, and instead calculate the steps needed.
We already know that 60fps is roughly 1 frame every 16.6ms, so that's the absolute maximum target that can expect the browser to draw. But when we do our updates by steps, nothing stops us from going faster. To make things easier to calculate, let's say we want to do 50 update steps rather than the 30 from before. This means that for the 500ms play time, we will need to perform an update every 10ms (slightly faster than the framerate). Also, because we are performing 50 steps, we will be updating the position by 1/50 units, and rotating by -PI/100.
let animationId = setInterval( ()=>{
// update position by 1/50 units
// update rotation by -PI/100
}, 10 ); // every 10 ms
As the interval runs, it will update the object. Meanwhile, the animation loop churns out new frames whenever it can.
Here's a full, running example, with only left-arrow support:
let W = window.innerWidth;
let H = window.innerHeight;
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(28, 1, 1, 1000);
camera.position.set(0, 0, 50);
camera.lookAt(scene.position);
scene.add(camera);
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 0, -1);
camera.add(light);
const cube = new THREE.Mesh(
new THREE.BoxBufferGeometry(1, 1, 1),
new THREE.MeshPhongMaterial({
color: "red"
})
);
cube.position.set(10, 0, 0);
scene.add(cube);
function render() {
renderer.render(scene, camera);
}
function resize() {
W = window.innerWidth;
H = window.innerHeight;
renderer.setSize(W, H);
camera.aspect = W / H;
camera.updateProjectionMatrix();
render();
}
window.addEventListener("resize", resize);
resize();
function animate() {
requestAnimationFrame(animate);
render();
}
requestAnimationFrame(animate);
const yAxis = new THREE.Vector3(0, 1, 0);
function updateCube() {
//cube.position.x -= 1;
//cube.rotateOnWorldAxis(yAxis, THREE.Math.degToRad(-90));
cube.position.x -= 1 / 50;
cube.rotateOnWorldAxis(yAxis, -(Math.PI / 100));
}
let step = 0;
let animationId = null;
function startStepping() {
animationId = setInterval(() => {
updateCube();
if (++step === 50) {
clearInterval(animationId);
animationId = null;
}
}, 10)
}
function handleKeyboard(e) {
//if (e.keyCode == 65 || e.keyCode == 37) {
// updateCube();
//}
if (animationId === null && (e.keyCode == 65 || e.keyCode == 37)) {
step = 0;
startStepping();
}
}
document.addEventListener("keydown", handleKeyboard, false);
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
background: skyblue;
}
<script src="https://threejs.org/build/three.min.js"></script>
Now there is a downside to this method. Sometimes you may see updates skipped (if the browser's framerate drops), or the same position drawn twice (if your update rate is significantly lower than the browser's framerate). You could try to get the best of both worlds but computing the framerate live from your render loop, and adjusting your frame steps accordingly, but at that point you must ask whether the extra time spent computing those statistics are actually hurting trying to achieve a rock-steady framerate-locked draw rate.
Switching based on key input
Because your key input is now disjointed from drawing, you now need some kind of flag to determine the action being taken. Your key press handler will set that flag, and then updateCube will act based on that flag. Something like:
let action = null
function startStepping(){
// set up the interval...
// but then also ensure the action stops after the animation plays:
setTimeout( () => action = null, 500 );
}
function handleKeyboard(e){
if (animationId === null) {
step = 0;
switch(e.keyCode){
case 37:
case 65:
action = "left";
break;
// other keys...
}
startStepping();
}
}
function updateCube(){
switch(action){
case "left":
// move as if it's rolling left
break;
case "right":
// move as if it's rolling right
break;
// etc. for the other directions
}
}
I want to rotate an object3D with hammerjs gestures.
Basically the rotation work, but with two issues I can't figure out.
The direction of the rotation changes randomly. It turns left. I stop and than move my fingers in the same diction again and suddenly instead of continuing to rotate left its going right. Happens sometimes but not always.
Once the rotation is started it only rotates to the same direction, despite me moving my fingers to different directions.
Here is how I handle the rotation:
public rotateObject3D (e: HammerInput): void {
if (rotationEnabled) {
const translation = new THREE.Vector3();
const rotation = new THREE.Quaternion();
const scale = new THREE.Vector3();
const rotateMatrix = new THREE.Matrix4();
const deltaRotationQuaternion = new THREE.Quaternion();
this._myObject.matrix.decompose(translation, rotation, scale);
this._deltaRot = (e.rotation * 0.01);
deltaRotationQuaternion.setFromEuler(new THREE.Euler(
0,
this._deltaRot * (Math.PI / 180),
0,
'XYZ'
));
deltaRotationQuaternion.multiplyQuaternions(deltaRotationQuaternion, rotation);
this._myObject.matrix = rotateMatrix.compose(translation, deltaRotationQuaternion, scale);
}
}
And this is the call of it:
this._hammerManager.on('rotate', (e) => {
this._arTools.rotateObject3D(e);
});
Is there anything I am missing?
It looks like you're using the absolute rotation value, instead of the change in rotation. For instance, consider the following scenario:
Rotation1: 12°
Rotation2: 10°
Rotation3: 5°
By multiplying your quaternions, your object is being rotated to 12°, then 22°, then finally 27°, even though you were turning back towards 0. This is because you're adding the new rotation to the last rotation on each event.
What you should do is save the previous rotation value, and subtract it from the new one, to get the rotation delta:
var previousRot = 0;
var newRot = 0;
rotateObject3D(e) {
newRot = e.rotation - previousRot;
// You could use newRot for your quaternion calculations
// but modifying .rotation.y is simpler
myObject.rotation.y += newRot
// Save value to be used on next event
previousRot = newRot;
}
With this method, the scenario above will give you the change in rotation. Your object will first rotate by +12°, then -2°, then -5°, for a more natural behavior.
Just make sure to reset previousRot = 0 on HammerJS' rotateend event so you don't use the value from previous gestures when a new one begins.
I have a run into a problem with FPS camera controls in a three.js scene. I'm using a Raycaster to determine the camera group position based on it's intersection with the scene along the Y axis (a poor man's gravity if you will =) and then apply user input to move around. The camera group position keeps getting reset to the intersection location on every frame, essentially gluing you to the spot.
I'm assuming this is either an updateMatrix() problem or a that a Vector3 is getting passed by reference somewhere, but for the life of me I can't seem to put my finger on it. I need some help... I hope this code is clear enough to help understand the problem :
renderer.setAnimationLoop((event) => {
if (clock.running) {
update();
renderer.render(scene,character.view);
}
});
clock.start();
//
const update = () => {
// velocity
const velocity = new THREE.Vector3();
velocity.x = input.controller.direction.x;
velocity.z = input.controller.direction.y;
velocity.clampLength(0,1);
if (velocity.z < 0) {
velocity.z *= 1.4;
}
// gravity
if (scene.gravity.length() > 0) {
const origin = new THREE.Vector3().copy(character.view.position);
const direction = new THREE.Vector3().copy(scene.gravity).normalize();
const intersection = new THREE.Raycaster(origin,direction).intersectObjects(scene.collision).shift();
if (intersection) {
character.group.position.copy(intersection.point);
character.group.updateMatrix();
}
}
// rotation
const rotation = new THREE.Euler();
rotation.x = input.controller.rotation.y;
rotation.y = input.controller.rotation.x;
character.group.rotation.set(0,rotation.y,0);
character.view.rotation.set(rotation.x,0,0);
// velocity.applyEuler(rotation);
const quaternion = new THREE.Quaternion();
character.group.getWorldQuaternion(quaternion);
velocity.applyQuaternion(quaternion);
// collision
const origin = new THREE.Vector3().setFromMatrixPosition(character.view.matrixWorld);
const direction = new THREE.Vector3().copy(velocity).normalize();
const raycaster = new THREE.Raycaster(origin,direction);
for (const intersection of raycaster.intersectObjects(scene.collision)) {
if (intersection.distance < 0.5) {
// face normals ignore object quaternions
const normal = new THREE.Vector3().copy(intersection.face.normal);
const matrix = new THREE.Matrix4().extractRotation(intersection.object.matrixWorld);
normal.applyMatrix4(matrix);
// normal
normal.multiplyScalar(velocity.clone().dot(normal));
velocity.sub(normal);
}
}
// step
const delta = 0.001 / clock.getDelta();
velocity.multiplyScalar(delta);
// apply
character.group.position.add(velocity);
}
The camera setup is a lot like the PointerLockControls helper, the camera being a child of a Group Object for yaw and pitch. The controller input is defined elsewhere as it can come from the mouse or a gamepad, but it returns normalized values.
To be more precise, the part that is causing the problem is here :
// gravity
if (scene.gravity.length() > 0) {
const origin = new THREE.Vector3().copy(character.view.position);
const direction = new THREE.Vector3().copy(scene.gravity).normalize();
const intersection = new THREE.Raycaster(origin,direction).intersectObjects(scene.collision).shift();
if (intersection) {
character.group.position.copy(intersection.point);
character.group.updateMatrix();
}
}
if I comment out character.group.position.copy(intersection.point);, for example, the camera moves like it's supposed to (except of course it's flying), but otherwise it moves a frame's worth of distance and then gets reset back to the intersection point on the next frame.
I have tried all manner of updateMatrix(), updateMatrixWorld(), updateProjectionMatrix(), and Object.matrixWorldNeedsUpdate = true, but alas no joy.
I apologise for using a copy/paste of my code rather than a testable case scenario. Thank you for your time.
Holy cow, I feel dumb... const origin = new THREE.Vector3().copy(character.view.position); returns local space coordinates, of course it gets reset to the origin!
replacing it with const origin = new THREE.Vector3().setFromMatrixPosition(character.view.matrixWorld); gives me the proper result
There's a lesson in there somewhere about staring blankly et your code for too long. I hope at least that this question helps someone out there one day.
I'm using three.js on a separate canvas on top of a openlayers map. the way I synchronize the view of the map and the three.js camera (which looks straight down onto the map) works fine, and looks like this:
function updateThreeCam(
group, // three.js group — has camera in it
view, // ol3 view
map // ol3 map
) {
// get visible part of map
const extent = getViewExtent(view, map);
const h = ol.extent.getHeight(extent);
const mapCenter = ol.extent.getCenter(extent);
// calculates how for away from the ground the camera has to
// be to match the currently visible part of the map
const camDist = h / (2 * Math.tan(constants.CAM_V_FOV_RAD / 2));
camera.updateProjectionMatrix(); // needed?
group.position.set(...mapCenter, camDist);
group.updateMatrixWorld();
}
however, when I am animating the flight of an object from in front of the camera down to the ground, the movement is not smooth: the vertical movement is jumping a lot. https://jsbin.com/qohutabofe/2/edit?js,output
this is the animation code:
// construct a spline to animate along
const waypoints = [
objStartPosition,
objEndPosition,
];
const pathSpline = new THREE.CatmullRomCurve3(waypoints);
// const pathSpline = new THREE.LineCurve3(...waypoints);
startTime = Date.now();
// [...]
function animate() {
const diff = Date.now() - startTime;
const t = diff / aniDuration;
// sample curve at `t`
const pos = pathSpline.getPointAt(t);
// update position
obj.position.copy(pos);
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
what is weird is that I don't really see the jumps in the sampled positions:
as you can see the map coordinates tend to be very big numbers. that's why I thought it might be a float precision issue. — dividing everything by 1000 indeed seems to help. there is just two new problems with that:
the foreshortening effect is way less drastic / not realistic anymore
panning does not work as expected anymore: the rectangle on the ground does not stay in its position
https://jsbin.com/bujepediro/1/edit?js,output
I am trying to spawn a set of objects on a setInterval and give each of these objects their own animation on a path (currently using requestAnimationFrame to do so). I managed to add one object and animate this on a path. With this code:
var psGeometry = new THREE.PlaneGeometry(3,2,10,1);
var psPlane = new THREE.Mesh(psGeometry, new THREE.MeshBasicMaterial({color:0x0000ff}));
scene.add(psPlane);
function animatePaper(obj = psPlane, offset= 0.007)
{
if(counter <=( 1-obj.geometry.vertices.length/2 *offset))
{
for (var i=0; i < obj.geometry.vertices.length/2; i++)
{
obj.geometry.vertices[i].y = curvePath.getPoint(counter + i * offset).y;
obj.geometry.vertices[i].z = -0.5;
obj.geometry.vertices[i + obj.geometry.vertices.length/2].y = curvePath.getPoint(counter + i * offset).y;
obj.geometry.vertices[i + obj.geometry.vertices.length/2].z = -2.5;
obj.geometry.vertices[i].x = curvePath.getPoint(counter + i * offset).x;
obj.geometry.vertices[i + obj.geometry.vertices.length/2].x = curvePath.getPoint(counter + i * offset).x;
}
obj.geometry.verticesNeedUpdate = true;
counter += 0.005;
}
else{
console.log("Removing...");
scene.remove(obj);
}
}
function animate() {
requestAnimationFrame(animate);
animatePaper(psPlane, 0.007);
render();
}
Example can be found here: jsfiddle.net.
Since this animates the object along the curvePath (see jsfiddle example), I figured that spawning these objects on an interval and applying the above code should work. Wrong!.
I tried: creating a function spawning objects and applying the above code:
setInterval(drawSheets, 1000);
function drawSheets()
{
var psGeometry = new THREE.PlaneGeometry(3,2,10,1);
var psPlane = new THREE.Mesh(psGeometry, new THREE.MeshBasicMaterial({color:0x0000ff}));
scene.add(psPlane);
setInterval(function(){animatePaper(psPlane, 0.007);}, 30);
}
I also tried on the basis of this answer:
setInterval(objArray.forEach(function(obj){setInterval(function(){animatePaper(obj);},300);}), 3000);
Expected:
Spawning multiple objects on an interval and animate each of these objects seperately over a curve.
Hopefully anyone could help me out! Cheers.
Version: Three.js r82
** EDIT: ** Small refinement. After another small test (jsfiddle). I found out that when I use setInterval on a function, it shares the same variable (thus speeding up the animation). Since this is part of the problem I would like to ask if someone knows how to make these variables local to an object.
Consider creating an array containing each of your Path and Plane objects (or perhaps one array for Paths and one array for Planes) along with their distinctive offsets or other values, then loop though these in an update function in your animation loop, running each through your animatePaper function.
In pseudo code:
var planesAndMeshesArray = [
{ path1 : (your plane here), plane1 : (your curved mesh here), offset : (an offset value), extrudeSettings : (your settings object here) },
{ path2 : (your plane here), plane2 : (your curved mesh here), offset : (an offset value), extrudeSettings : (your settings object here) },
{ path3 : (your plane here), plane3 : (your curved mesh here), offset : (an offset value), extrudeSettings : (your settings object here) },
...]
- create a loop to write the above array with random values in an appropriate range to suit the effects you're looking for
- loop through the above array to add each of the meshes and planes to the scene
function update() {
- update each object by looping through the above array through your `animatePaper` function. It works as a handy set of pointers to each of the objects in your scene - if you change them in the array, they will change in your scene.
- also update your controls
}
function animate() {
requestAnimationFrame(animate);
update();
render();
}
Going one step further, you can write object-oriented Javascript to create each of your curve-and-paper objects. I'd recommend starting with the array first and adding further complexity as needed.