最近在学习Three.js中如何移动摄像机,如何移动物体,如何在移动的同时,旋转,然后继续移动,补了Tween.js的基础知识,结合案例,Three.js的学习过程立马生动起来了,一起来学一学吧~
学习链接
作者的github地址 - github.com/tamani-codi…
动画效果
- 镜头动画,移动摄像机,呈现不同画面!
- 立方体动画,由大变小,再由小变大
- 球体动画,移动不同位置,呈棱形移动
- 小人动画,向前移动,左转,再前移,再左转,一个循环。
- 机械臂动画,学习整体旋转的同时,局部旋转
基础知识
主要使用的是Tween.js,用在Three.js的场景里!
这里介绍一下Three.js中的一些基础概念,还有Tween.js的基本使用。
Three.js基础知识
学习内容来源
- Three.js官方的基础知识教程
- threejs.org/manual/#en/…
主要介绍内容
- 渲染流程 - 为什么可以有画面
- 摄像机镜头 - 画面如何呈现,镜头动画就是移动摄像机!
- 场景设置 - 添加物体,灯光,阴影,模型
- 坐标系 - 所有物体在哪里,包括摄像机,场景内容,灯光,模型等
- 物体的层级设置 - 物体的层级是如何影响的
渲染流程
- 渲染器 - 最终的画面呈现
- 摄像机 - 代表人眼,观看画面的视角
- Scene - 所有场景内容都放在这里 - 都是Scene的子元素。
- Mesh - 添加进场景,具体是啥得看用的啥几何元素和材质。。
摄像机镜头
场景设置
所有的物体都是放在Scene里面的,做为Scene的子元素,这里介绍添加物体,设置灯光和阴影效果
以案例中的代码为例。
- 环境光 - AmbientLight添加进Scene,环境光可以设置场景的基础色调,
- 平行光 - DirectionalLight添加进Scene,平行光可以设置亮度,投掷阴影效果(需要接收的物体)
- 地面 - 一个立方体,设置
receiveShadow即可接收阴影了!
坐标系
在实际场景中的坐标系
- 添加如下代码 - 坐标系展示
- 设置不同的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);
在Blender中
- Z轴在上,Y轴控制前后,X轴控制左右
- 和Threejs是不一样的!
物体的层级设置
这个层级设置,主要是机械臂用到了。
- 可以理解为 - 局部 和整体,整体移动,局部移动
- 也可以理解为 CSS 中的,相对定位 和全局定位。。相对父元素
Tween.js基础知识
学习内容来源
- Tween.js官方文档 - tween.js user guide
- tweenjs.github.io/tween.js/do…
主要介绍内容
- 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 ofchainis just tweenA, not a new tween.
代码来源 - 下方的球体动画。。
- 我们打印一下这个start
- 因为每个动画设置的初始值都是start,但是,它并不是从start的位置开始的。。
可以看到它的start的值是变化的!
- 为什么start的值会变!
- 因为它是对象 - 引用类型!!!
- 在
onUpdate函数中,修改了! - 在每个动画设置中放入start,就是放入一直在变化的起点!!!
动画内容
进入主线了!
镜头动画
案例中的镜头动画,是一组循环动画,实现俯拍,平移,两种效果!
俯拍
平移
具体代码如下。
- 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的值
- 这样就很直观了~~~
立方体动画
立方体动画,主要设置的是放大缩小的效果,主要是设置box的scale参数,也是通过onUpdate方法
// 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,左右和上下!
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)
}
小人动画
加载模型,添加阴影,
- 设置移动动画,主要是修改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) // 添加进场景!
})
});
}
机械臂动画
这里是我感觉最酷的地方了,机械臂的旋转
- 通过层级的设置,整体旋转 和局部旋转
为什么要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轴!
最重要的点!
- 如果要有连贯的动画,起始位置应该设置给每一个动画内容,这样每个动画的起点,就是上一个的终点,这样就连的起来 - 案例就是那个球体动画,还有小人动画!
- 单独设置也可以,就是麻烦,每个起点都单独设置的话,也连的起来。。。
西塞山前白鹭飞,桃花流水鳜鱼肥。 青箬笠,绿蓑衣,斜风细雨不须归。