Tweenjs动画实例~

2,876 阅读11分钟

最近在学习Three.js中如何移动摄像机,如何移动物体,如何在移动的同时,旋转,然后继续移动,补了Tween.js的基础知识,结合案例,Three.js的学习过程立马生动起来了,一起来学一学吧~

学习链接

作者的github地址 - github.com/tamani-codi…

002.gif

动画效果

  1. 镜头动画,移动摄像机,呈现不同画面!
  2. 立方体动画,由大变小,再由小变大
  3. 球体动画,移动不同位置,呈棱形移动
  4. 小人动画,向前移动,左转,再前移,再左转,一个循环。
  5. 机械臂动画,学习整体旋转的同时,局部旋转

基础知识

主要使用的是Tween.js,用在Three.js的场景里!

这里介绍一下Three.js中的一些基础概念,还有Tween.js的基本使用。

Three.js基础知识

学习内容来源

主要介绍内容

  • 渲染流程 - 为什么可以有画面
  • 摄像机镜头 - 画面如何呈现,镜头动画就是移动摄像机!
  • 场景设置 - 添加物体,灯光,阴影,模型
  • 坐标系 - 所有物体在哪里,包括摄像机,场景内容,灯光,模型等
  • 物体的层级设置 - 物体的层级是如何影响的

渲染流程

  • 渲染器 - 最终的画面呈现
  • 摄像机 - 代表人眼,观看画面的视角
  • Scene - 所有场景内容都放在这里 - 都是Scene的子元素。
  • Mesh - 添加进场景,具体是啥得看用的啥几何元素和材质。。

image.png

摄像机镜头

image.png

场景设置

所有的物体都是放在Scene里面的,做为Scene的子元素,这里介绍添加物体,设置灯光和阴影效果

以案例中的代码为例。

  • 环境光 - AmbientLight添加进Scene,环境光可以设置场景的基础色调,
  • 平行光 - DirectionalLight添加进Scene,平行光可以设置亮度,投掷阴影效果(需要接收的物体)
  • 地面 - 一个立方体,设置receiveShadow 即可接收阴影了!

image.png

image.png

image.png

坐标系

在实际场景中的坐标系

  • 添加如下代码 - 坐标系展示
  • 设置不同的XYZ,就可以设置不同的位置了!

The X axis is red. The Y axis is green. The Z axis is blue.

// 添加代码 - 可显示坐标系
const axesHelper = new THREE.AxesHelper(50);
scene.add(axesHelper);

image.png

在Blender中

  • Z轴在上,Y轴控制前后,X轴控制左右
  • 和Threejs是不一样的!

image.png

物体的层级设置

这个层级设置,主要是机械臂用到了。

  • 可以理解为 - 局部 和整体,整体移动,局部移动
  • 也可以理解为 CSS 中的,相对定位 和全局定位。。相对父元素

image.png

Tween.js基础知识

学习内容来源

主要介绍内容

  • Tween.js运行流程
  • 如何设置参数
  • Tween.js链式调用!

Tween.js运行流程

  • new 一个Tween对象
  • 设置to方法,设置时间
  • start()启动tween对象
  • 在周期函数中,设置TWEEN.update方法
var position = {x: 100, y: 0}

// Create a tween for position first
var tween = new TWEEN.Tween(position) // 起点

// Then tell the tween we want to animate the x property over 1000 milliseconds
tween.to({x: 200}, 1000) // 终点,

// And set it to start
tween.start() // 需要显式开启动画

function animate() {
	requestAnimationFrame(animate)
	// [...]
	TWEEN.update() // 更新画面内容
	// [...]
}

animate()

如何设置参数

  • new的时候,传入的起始位置(起点
  • 调用的to方法,传入的是最终位置(终点
  • to方法,可以设置时间,单位 - 毫秒ms

在上方的例子中

  • new 的时候传入position对象,它包含x 和 y两个值
  • to 方法修改 x 的值,再设置时间,就可以完成动画内容的设置了。
  • 对于复杂的情况,需要设置set方法,才能修改参数,可以在onUpdate方法中调用修改,可以在下文的案例看到!

Tween.js链式调用!

链式调用 - tweenA.chain(tweenB)

在A结束的时候调用B,实际上是修改了A里的内容,B每次从A结束的时候开始。

WARNING: Calling tweenA.chain(tweenB) actually modifies tweenA so that tweenB is always started when tweenA finishes. The return value of chain is just tweenA, not a new tween.

代码来源 - 下方的球体动画。。

  • 我们打印一下这个start
  • 因为每个动画设置的初始值都是start,但是,它并不是从start的位置开始的。。 image.png

可以看到它的start的值是变化的!

  • 为什么start的值会变!
  • 因为它是对象 - 引用类型!!!
  • onUpdate函数中,修改了!
  • 在每个动画设置中放入start,就是放入一直在变化的起点!!!

03.gif

动画内容

进入主线了!

镜头动画

案例中的镜头动画,是一组循环动画,实现俯拍,平移,两种效果!

俯拍

03.gif

平移

03.gif

具体代码如下。

  • new的时候,传入对象
  • to方法中,传入修改内容
  • onUpdate方法中,实际修改

注!我们传入的对象,要包含需要设置的参数,名字取容易识别的就行,比如x, y, z, rotX等,但是在onUpdate方法中,我们需要设置的对象,应该来自Three.js中的实例,比如camera,要设置它的位置,应该是camera.position.x,这个具体怎么设置,可以打印一下Scene,然后找找参数。。。还有,在onUpdate方法中的形参 - Object对象,就是来自起点和终点,它们中间的过度值,Tween.js帮我们完成了,这样就实现了动画补间效果!


// CAMERA
const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 1, 1500);
camera.position.set(-35, 70, 100);
camera.lookAt(new THREE.Vector3(0, 0, 0));

// 起点,终点
const tweenCamera1 = new TWEEN.Tween({ x: -35, y: 85, z: 100, lookAtX: 0, lookAtY: 0, lookAtZ: 0 })
  .to({ x: 45, y: 66, z: -80, lookAtX: 0, lookAtY: 0, lookAtZ: 0 }, 7000)
const tweenCamera2 = new TWEEN.Tween({ x: -45, y: 10, z: -80, lookAtX: -35, lookAtY: 10, lookAtZ: 100 })
  .to({ x: 35, y: 10, z: -80, lookAtX: 35, lookAtY: 10, lookAtZ: 100 }, 6000)

// 链接 - 循环!
tweenCamera1.chain(tweenCamera2)
tweenCamera2.chain(tweenCamera1)

// 在该方法中,设置set方法,和lookAt方法。。修改摄像机的位置和朝向。
const updateCamera = function (object: { x: number; y: number; z: number; lookAtX: number; lookAtY: number; lookAtZ: number }, elapsed: number) {
  camera.position.set(object.x, object.y, object.z);
  camera.lookAt(new THREE.Vector3(object.lookAtX, object.lookAtY, object.lookAtZ))
}

// 需要set方法,所以在onUpdate函数中设置!
// onUpdate是和 动画运行 一起触发的!
tweenCamera1.onUpdate(updateCamera)
tweenCamera2.onUpdate(updateCamera)

tweenCamera1.start()

这样的参数设置,不容易识别

  • tweenCamera1
  • { x: -35, y: 85, z: 100, lookAtX: 0, lookAtY: 0, lookAtZ: 0 }
  • { x: 45, y: 66, z: -80, lookAtX: 0, lookAtY: 0, lookAtZ: 0 }
  • tweenCamera2
  • { x: -45, y: 10, z: -80, lookAtX: -35, lookAtY: 10, lookAtZ: 100 }
  • { x: 35, y: 10, z: -80, lookAtX: 35, lookAtY: 10, lookAtZ: 100 }

我们可以在Blender中,模拟一下。。。

  • 设置为Top视角,这样坐标系就和Threejs中一样了。方便查看位置
  • 立方体为移动位置,设置对应的x,y,z的值
  • 圆球为朝向,设置对应的lookAtX,lookAtY,lookAtZ的值
  • 这样就很直观了~~~

image.png

立方体动画

立方体动画,主要设置的是放大缩小的效果,主要是设置box的scale参数,也是通过onUpdate方法

03.gif

// box
function createBox() {
  let box = new THREE.Mesh(new THREE.BoxBufferGeometry(),
    new THREE.MeshPhongMaterial({ color: 0xDC143C }));
  box.position.set(0, 6, 0)
  box.scale.set(6, 6, 6);
  box.castShadow = true;
  box.receiveShadow = true;
  scene.add(box)

// 对象可以定义好,这里考虑大小即可,名字可以为ScaleX
  var start = { x: 3, y: 3, z: 3 };
  var target1 = { x: 6, y: 6, z: 6 };
  var target2 = { x: 3, y: 3, z: 3 };
  
// 这里都是以start为起点,但是值都不一样,在onUpdate函数中修改了!
  var tween1 = new TWEEN.Tween(start).to(target1, 2000).easing(TWEEN.Easing.Elastic.InOut)
  var tween2 = new TWEEN.Tween(start).to(target2, 2000).easing(TWEEN.Easing.Elastic.InOut).chain(tween1)
  tween1.chain(tween2)
  tween1.start()

// 这里设置缩放!
  const update = function () {
    box.scale.x = start.x;
    box.scale.y = start.y;
    box.scale.z = start.z;
  }
// 这样也行! - 上面没看懂。。看懂了,上面的start的值,会变的!!!
// const update = function (object: { x: number, y: number, z: number, }) {
//   box.scale.x = object.x;
//   box.scale.y = object.y;
//   box.scale.z = object.z;
// }

  tween1.onUpdate(update) // 
  tween2.onUpdate(update);
}

球体动画

球体动画效果为 - 棱形移动

  • 主要移动的是X 和Y,左右和上下!

03.gif

function createSphere() {
  let sphere = new THREE.Mesh(new THREE.SphereBufferGeometry(4, 32, 32),
    new THREE.MeshPhongMaterial({ color: 0x43a1f4 }))
  sphere.position.set(0, 5, -15)
  sphere.castShadow = true
  sphere.receiveShadow = true
  scene.add(sphere)

  var start = { x: 0, y: 5, z: -15 }; // 起点 - 主要移动的是X 和Y
  var target1 = { x: 10, y: 15, z: -15 };
  var target2 = { x: 0, y: 25, z: -15 };
  var target3 = { x: -10, y: 15, z: -15 };
  var target4 = { x: 0, y: 5, z: -15 }; // 回到起点。

  const updateFunc = function (object: {
    x: number;
    y: number;
    z: number;
  }, elapsed: number) {
    sphere.position.x = object.x;
    sphere.position.y = object.y;
    sphere.position.z = object.z;
  }

// 设置移动动画 - 都以start为起点,了解!
  var tween1 = new TWEEN.Tween(start).to(target1, 2000).easing(TWEEN.Easing.Elastic.InOut)
  var tween2 = new TWEEN.Tween(start).to(target2, 2000).easing(TWEEN.Easing.Exponential.InOut)
  var tween3 = new TWEEN.Tween(start).to(target3, 2000).easing(TWEEN.Easing.Bounce.InOut)
  var tween4 = new TWEEN.Tween(start).to(target4, 2000).easing(TWEEN.Easing.Quadratic.InOut)

  tween1.chain(tween2).start() // 链接!
  tween2.chain(tween3)
  tween3.chain(tween4)
  tween4.chain(tween1)

// 实时更新位置。
  tween1.onUpdate(updateFunc)
  tween2.onUpdate(updateFunc)
  tween3.onUpdate(updateFunc)
  tween4.onUpdate(updateFunc)
}

小人动画

03.gif

加载模型,添加阴影,

  • 设置移动动画,主要是修改X轴 和 Z轴
  • 设置旋转动画,主要是修改角度,使用的是弧度值, Math.PI / 2代表顺时针90°
  • 链接 - 移动结束链接旋转,旋转结束链接移动 - 重复四次。

function createSwordMan() {
  new MTLLoader().load('./chr_sword.mtl', function (materials) {
    materials.preload();
    new OBJLoader().setMaterials(materials).loadAsync('./chr_sword.obj').then((group) => {
      const swordMan = group.children[0];

      swordMan.position.x = -15
      swordMan.position.z = -15

      swordMan.scale.x = 7;
      swordMan.scale.y = 7;
      swordMan.scale.z = 7;

      swordMan.castShadow = true // 添加阴影
      swordMan.receiveShadow = true

      const start = { x: -15, z: -15 } // 移动位置,主要是X 轴 和Z轴
      const moveto1 = { x: -15, z: 15 }
      const moveto2 = { x: -35, z: 15 }
      const moveto3 = { x: -35, z: -15 }
      const moveto4 = { x: -15, z: -15 } // 回到原点

      const rotStart = { rotY: 0 } // 旋转,使用的是弧度, Math.PI / 2代表顺时针90°
      const rotto1 = { rotY: - Math.PI / 2 }
      const rotto2 = { rotY: - Math.PI }
      const rotto3 = { rotY: - Math.PI * (3 / 2) }
      const rotto4 = { rotY: - Math.PI * 2 }

       // 都以rotStart为起点,
      var tweenRot1 = new TWEEN.Tween(rotStart).to(rotto1, 400) // 设置旋转动画内容, 0.4s
      var tweenRot2 = new TWEEN.Tween(rotStart).to(rotto2, 400)
      var tweenRot3 = new TWEEN.Tween(rotStart).to(rotto3, 400)
      var tweenRot4 = new TWEEN.Tween(rotStart).to(rotto4, 400)
        
      // 都以start为起点,
      var tweenMove1 = new TWEEN.Tween(start).to(moveto1, 2000) // 设置移动动画内容, 2s
      var tweenMove2 = new TWEEN.Tween(start).to(moveto2, 2000)
      var tweenMove3 = new TWEEN.Tween(start).to(moveto3, 2000)
      var tweenMove4 = new TWEEN.Tween(start).to(moveto4, 2000)

      tweenMove1.chain(tweenRot1) // 移动链接旋转
      tweenRot1.chain(tweenMove2) // 旋转链接移动 - 重复
      tweenMove2.chain(tweenRot2)
      tweenRot2.chain(tweenMove3)
      tweenMove3.chain(tweenRot3)
      tweenRot3.chain(tweenMove4)
      tweenMove4.chain(tweenRot4)
      tweenRot4.chain(tweenMove1)

      const updatePos = function (object: { // 更新位置
        x: number;
        z: number;
      }, elapsed: number) {
        swordMan.position.x = object.x;
        swordMan.position.z = object.z;
      }
      tweenMove1.onUpdate(updatePos)
      tweenMove2.onUpdate(updatePos)
      tweenMove3.onUpdate(updatePos)
      tweenMove4.onUpdate(updatePos)

      const updateRot = function (object: { // 更新角度,主要是 Y 轴
        rotY: number;
      }, elapsed: number) {
        swordMan.rotation.y = object.rotY;
      }
      tweenRot1.onUpdate(updateRot)
      tweenRot2.onUpdate(updateRot)
      tweenRot3.onUpdate(updateRot)
      tweenRot4.onUpdate(updateRot)

      tweenMove1.start()

      scene.add(swordMan) // 添加进场景!
    })
  });
}

机械臂动画

03.gif

这里是我感觉最酷的地方了,机械臂的旋转

  • 通过层级的设置,整体旋转 和局部旋转

image.png

为什么要joint - (它是一个空物体)

  • 这样展示的和操作的就分离了
  • 旋转操作命名也统一了!
  • part里面可以放任意模型!!!

时间设置

  • 整体的设置 4s
  • 局部的设置 5s
  • 20秒的时候,大家就可以同步了(没同步的时候,就像各转各的,同时开始,就像一起干活了),好厉害啊!!!
function createRobotArm() {
  const material = new THREE.MeshPhongMaterial({ color: 0xfc8403 })

  const foundation1 = new THREE.Mesh(new THREE.CylinderBufferGeometry(5, 5, 1, 64), material);
  foundation1.position.set(-35, 0.5, 35)
  foundation1.castShadow = true; // 设置阴影,每个部件都设置。
  foundation1.receiveShadow = true;
  const foundation2 = new THREE.Mesh(new THREE.BoxBufferGeometry(1, 4, 4), material)
  foundation2.position.y = 2
  foundation2.castShadow = true;
  foundation2.receiveShadow = true;
  foundation1.add(foundation2) // 添加层级。

  const joint1 = new THREE.Object3D; // 一个空物体!用于旋转操作!
  joint1.position.y = 1
  joint1.position.x = 1
  joint1.rotation.x = - Math.PI / 4 // 设置X轴的旋转,是绕着X轴的!
  foundation2.add(joint1) // 添加层级!
  const part1 = new THREE.Mesh(new THREE.BoxBufferGeometry(1, 15, 4), material)
  part1.position.y = 7.5
  part1.castShadow = true;
  part1.receiveShadow = true;
  joint1.add(part1) // 添加层级!

  const joint2 = new THREE.Object3D;
  joint2.position.y = 7.5
  joint2.position.x = -1
  joint2.rotation.x = Math.PI / 2
  part1.add(joint2)
  const part2 = new THREE.Mesh(new THREE.BoxBufferGeometry(1, 15, 4), material)
  part2.position.y = 7.5
  part2.castShadow = true;
  part2.receiveShadow = true;
  joint2.add(part2)

  const joint3 = new THREE.Object3D;
  joint3.position.y = 7.5
  joint3.position.x = 1
  joint3.rotation.x = Math.PI / 6
  part2.add(joint3)
  const part3 = new THREE.Mesh(new THREE.BoxBufferGeometry(1, 6, 4), material)
  part3.position.y = 3
  part3.castShadow = true;
  part3.receiveShadow = true;
  joint3.add(part3)

  scene.add(foundation1)

  // TWEEN FOUNDATION
  // foundation1Tween1旋转,整体都会旋转,绕着Y轴左右旋转! -4s
  const foundation1Tween1 = new TWEEN.Tween({ rotY: 0 }).to({ rotY: 2 * Math.PI }, 4000)
    .easing(TWEEN.Easing.Quadratic.InOut)
  const foundation1Tween2 = new TWEEN.Tween({ rotY: 2 * Math.PI }).to({ rotY: 0 }, 4000)
    .easing(TWEEN.Easing.Quadratic.InOut)
  foundation1Tween1.chain(foundation1Tween2)
  foundation1Tween2.chain(foundation1Tween1)
  const updateFoundation1 = function (object: {
    rotY: number;
  }, elapsed: number) {
    foundation1.rotation.y = object.rotY;
  }
  foundation1Tween1.onUpdate(updateFoundation1) // 设置旋转的角度
  foundation1Tween2.onUpdate(updateFoundation1)
  foundation1Tween1.start()

  // TWEEN JOINTS
  // 局部旋转,设置joint,都是绕着X轴旋转!- 5s
  // 这里个了三个参数,给三个joint用!
  // 注意这里的 +-号,代表顺时针和逆时针。
  const jointsTween1 = new TWEEN.Tween({
    rotX1: - Math.PI / 2.5,
    rotX2: Math.PI * (1.5 / 2),
    rotX3: - Math.PI / 2
  })
    .to({ rotX1: Math.PI / 2.5, rotX2: -Math.PI * (1.5 / 2), rotX3: Math.PI / 2 }, 5000)
    .easing(TWEEN.Easing.Quadratic.InOut)
  const jointsTween2 = new TWEEN.Tween({
     rotX1: Math.PI / 2.5, 
     rotX2: -Math.PI * (1.5 / 2), 
     rotX3: Math.PI / 2 
  })
    .to({ rotX1: - Math.PI / 2.5, rotX2: Math.PI * (1.5 / 2), rotX3: - Math.PI / 2 }, 5000)
    .easing(TWEEN.Easing.Quadratic.InOut)
  jointsTween1.chain(jointsTween2)
  jointsTween2.chain(jointsTween1)
  const updateJoint1 = function (object: {
    rotX1: number;
    rotX2: number;
    rotX3: number
  }, elapsed: number) {
    joint1.rotation.x = object.rotX1;
    joint2.rotation.x = object.rotX2;
    joint3.rotation.x = object.rotX3;
  }
  jointsTween1.onUpdate(updateJoint1) // 设置旋转的角度
  jointsTween2.onUpdate(updateJoint1)
  jointsTween1.start()
}

总结

很有收获的案例!学到的Tween.js的实际使用方法,一共有五个案例,每个都可以成单独的小动画了!

  • 镜头动画,学到了在unpdate方法中,设置摄像机的位置和朝向
  • 立方体动画,学到了立方体的变大变小,只修改缩放的话,是在原地进行的
  • 球体动画,在空中走了个棱形,一圈,需要起点位置和四个位置,最后一个位置,回到起点!
  • 小人动画,移动链接旋转,旋转链接移动,重复四次,绕一圈。。
  • 机械臂动画,整体旋转,局部旋转,整体绕Y轴,局部绕X轴!

最重要的点!

  • 如果要有连贯的动画,起始位置应该设置给每一个动画内容,这样每个动画的起点,就是上一个的终点,这样就连的起来 - 案例就是那个球体动画,还有小人动画!
  • 单独设置也可以,就是麻烦,每个起点都单独设置的话,也连的起来。。。

西塞山前白鹭飞,桃花流水鳜鱼肥。 青箬笠,绿蓑衣,斜风细雨不须归。