什么是关键帧动画
关键帧动画,你可以理解为在一个时间轴上,选择几个关键的时间点,然后分别定义这几个时间点对应物体状态(比如位置、姿态、颜色等),然后基于这几个关键的时间点,采用特定的插值方法计算物体的状态数据,从而达到比较流畅的动画效果。
创建关键帧动画对象
const geometry = new THREE.SphereGeometry( 15, 32, 16 );
const texLoader = new THREE.TextureLoader();
// 球面纹理
const texture = texLoader.load('./img.jpg');
const material = new THREE.MeshLambertMaterial({
coor:0xffff00,
map: texture,
});
const mesh = new THREE.Mesh( geometry, material );
mesh.name = 'Sphere'
KeyframeTrack
关键帧轨道(KeyframeTrack)是关键帧(keyframes)的定时序列, 它由时间和相关值的列表组成, 用来让一个对象的某个特定属性动起来。
关键帧轨道(KeyframeTrack)中总是存在两个数组:times
数组按顺序存储该轨道的所有关键帧的时间值,而values
数组包含动画属性的相应更改值。
构造器
KeyframeTrack( name : String, times : Array, values : Array, interpolation : Constant )
name
- 轨道的名称可以指动画对象中的变形目标,格式:模型对象的名字.属性构成(Box.position)
。times
- 时间轴上取的几个关键帧时间点。values
- 时间点对应的物体状态。interpolation
- 默认使用的插值类(我也不知道有啥用)。
const maxT = 30
//时间轴上的几个关键时间点0,10,20,30
const times = [0, 10, 20, maxT]
//times中不同时间点,物体分别对应values中的三个xyz坐标
const values = [0, 0, 0, 100, 0, 0, 0, 0, 100, 0, 0, 0];
//物体位置变化
const kf1 = new THREE.KeyframeTrack('Sphere.position',times,values)
//物体颜色变化
const kf2 = new THREE.KeyframeTrack('Sphere.material.color',[0, 10, 20, 30],[
0, 1, 0,
1, 0, 0,
0, 0, 1,
1, 1, 1
])
AnimationClip
动画剪辑(AnimationClip)是一个可重用的关键帧轨道集,它代表动画。
构造器
AnimationClip( name : String, duration : Number, tracks : Array )
name
- 此剪辑的名称duration
- 持续时间 (单位秒). 如果传入负数, 持续时间将会从传入的数组中计算得到。tracks
- 一个由关键帧轨道(KeyframeTracks
)组成的数组。AnimationClip里面,每个动画属性的数据都存储在一个单独的KeyframeTrack中。
基于关键帧数据KeyframeTrack
,创建关键帧动画AnimationClip
,这样就可以利用关键帧里面的数据生成一个关键帧动画,用于接下来的动画播放。
下面代码基于关键帧数据kf1、kf2
,创建一个clip
关键帧动画对象AnimationClip
,命名为test
,动画持续时间30
秒。
const clip = new THREE.AnimationClip("test", maxT, [kf1, kf2]);
常用属性
.duration
: Number 剪辑的持续时间 (单位秒)。可以用来获取或设置播放结束时间。
//动画结束时间
console.log('clip.duration',clip.duration); // 30
//设置播放结束时间:到5秒时刻对应的动画状态停止
clip.duration = 5;
AnimationMixer
动画混合器是用于场景中特定对象的动画的播放器。当场景中的多个对象独立动画时,每个对象都可以使用同一个动画混合器。
构造器
AnimationMixer( rootObject : Object3D )
rootObject
- 混合器播放的动画所属的对象,也就是包含关键帧动画的模型对象。
//包含关键帧动画的模型对象作为AnimationMixer的参数创建一个播放器mixer
const mixer = new THREE.AnimationMixer(mesh);
常用方法
.clipAction (clip : AnimationClip, optionalRoot : Object3D) : AnimationAction
该方法返回一个AnimationAction
对象,AnimationAction
对象用来控制如何播放,比如.play()
方法。
clip
:可以是动画剪辑(AnimationClip)对象或者动画剪辑的名称。optionalRoot
:根对象参数可选,默认值为混合器的默认根对象。
//AnimationMixer的`.clipAction()`返回一个AnimationAction对象
const clipAction = mixer.clipAction(clip);
//.play()控制动画播放,默认循环播放
clipAction.play();
.update (deltaTimeInSeconds : Number) : this
更新播放器AnimationMixer
时间。
如果想播放动画开始变化,需要周期性执行mixer.update()
更新播放器AnimationMixer
时间数据,比如你可以在requestAnimationFrame
创建的可以周期性执行的函数中,更新播放器时间数据。
通过Clock
对象辅助获取每次loop()
执行的时间间隔,执行mixer.update()
更新播放器AnimationMixer
时间数据。
const clock = new THREE.Clock();
function loop() {
requestAnimationFrame(loop);
const t = clock.getDelta();
// 执行mixer.update()更新播放器AnimationMixer时间数据
mixer.update(t);
}
loop();
AnimationAction
AnimationActions 对象的功能就是用来控制如何播放关键帧动画,比如是否播放、几倍速播放、是否循环播放、是否暂停播放…
构造器
AnimationAction( mixer : AnimationMixer, clip : AnimationClip, localRoot : Object3D )
mixer
- 被此动作控制的 动画混合器clip
- 动画剪辑 保存了此动作当中的动画数据localRoot
- 动作执行的根对象
注意:
不要直接调用这个构造函数,而应该先用AnimationMixer.clipAction实例化一个AnimationAction,因为这个方法提供了缓存以提高性能。
执行播放器AnimationMixer
的.clipAction()
方法会返回一个AnimationAction
对象。
const clipAction = mixer.clipAction(clip);
常用属性
.loop
: Number
控制动画是否循环播放,默认值是 THREE.LoopRepeat
必须是以下值之一:
THREE.LoopOnce
- 只执行一次THREE.LoopRepeat
- 重复次数为repetitions的值, 且每次循环结束时候将回到起始动作开始下一次循环。THREE.LoopPingPong
- 重复次数为repetitions的值, 且像乒乓球一样在起始点与结束点之间来回循环。
const clipAction = mixer.clipAction(clip);
//.play()控制动画播放,默认循环播放
clipAction.play();
//不循环播放,只执行一次,结束后回到起始位置
clipAction.loop = THREE.LoopOnce;
.clampWhenFinished
: Boolean
当你通过clipAction.loop = THREE.LoopOnce
设置播放模式为非循环模式的时候,关键帧动画执行完成一个后,模型会回到关键帧动画开始状态,如果想模型停留在关键帧动画结束的状态,可以设置.clampWhenFinished
属性,.clampWhenFinished
属性默认是false
,设置为true即可。
clipAction.loop = THREE.LoopOnce;
// 物体状态停留在动画结束的时候
clipAction.clampWhenFinished = true;
.paused
: Boolean
是否暂停动画播放,.paused
设置为true
动画会暂停在当前位置,.paused
设置为false
动画会接着暂停的位置继续执行。
btn.addEventListener('click',function(){
// AnimationAction.paused默认值false,设置为true,可以临时暂停动画
if (clipAction.paused) {//暂停状态
clipAction.paused = false;//切换为播放状态
} else {//播放状态
clipAction.paused = true;//切换为暂停状态
}
})
.time
: Number
全局的混合器时间(单位秒),设置动画从什么时间开始播放。
从时间轴上选择时间段播放动画,开始时刻AnimationAction.time
,结束时刻AnimationClip.duration
//AnimationAction设置开始播放时间:从2秒时刻对应动画开始播放
clipAction.time = 2;
//AnimationClip设置播放结束时间:到10秒时刻对应的动画状态停止
clip.duration = 10;
注意:
.loop
和.clampWhenFinished
对播放效果的影响,如果需要上面代码完全起作用,要设置非循环模式(clipAction.loop = THREE.LoopOnce
),同时动画播放完,物体停留在结束状态,而不是回到开始状态(clipAction.clampWhenFinished=true
)。
把动画设置为暂停状态,你可以通过AnimationAction.time
把动画定格在时间轴上任何位置。
//在暂停情况下,设置.time属性,把动画定位在任意时刻
clipAction.paused = true;
//物体状态为动画10秒对应状态
clipAction.time = 10;
//物体状态为动画20秒对应状态
clipAction.time = 20;
在暂停情况下,我们可以查看动画下一步状态:
clipAction.paused = true;
btn.addEventListener('click', function () {
// 设置步长 0.1
clipAction.time += 0.1;
})
.timeScale
: Number 默认 是1
通过时间控制动画播放的速度。
//2倍速
clipAction.timeScale = 2;
.weight
: Number
动作的影响程度 (取值范围[0, 1]). 0 (无影响)到1(完全影响)之间的值可以用来混合多个动作。默认值是1
常用方法
.play ()
: this
播放动画
const clipAction = mixer.clipAction(clip);
clipAction.play();
注意:
调用了.play()
方法并不意味着动画会立刻开始,如果动作在此之前已经完成(到达最后一次循环的结尾),或者如果已经设置了延时 启动(通过 startAt
),则必须先执行重置操作(reset
)。 一些其它的设置项 (paused=true, enabled=false, weight=0, timeScale=0
) 也可以阻止动画的开始。
.stop ()
: this
执行.stop()
方法,动画会停止,并结束,模型会回到动画开始状态,注意不是暂停,是动画彻底终止,回到初始状态。
document.getElementById('stop').addEventListener('click',function(){
clipAction.stop();//动画停止结束,回到开始状态
})
.reset ()
: this
重置动画的状态
该方法会将暂停值 paused
设为false, 启用值enabled
设为true,时间值 time
设为0, 中断任何预定的淡入淡出和变形, 以及移除内部循环次数以及延迟启动。
注意:
停止方法stop内调用了重置方法(reset), 但是 .reset不会调用 .stop。 这就表示: 如果你想要这两者, 重置并且停止, 不要调用reset; 而应该调用stop。
.startAt ( startTimeInSeconds : Number ) : this
定义延时启动的事件
事件监听
分别表示了单次循环的结束和全部动作的结束。
mixer.addEventListener( 'loop', function( e ) { …} );
mixer.addEventListener( 'finished', function( e ) { …} );
动画实例
上面介绍了动画到的几个方法使用,下面我们来实操一下。
效果展示
完整代码
index.html
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>关键帧动画</title>
<style>
body{
overflow: hidden;
margin: 0px;
}
.pos{
position: fixed;
bottom:50px;
left:0px;
right:0;
display: flex;
align-items: center;
justify-content: center;
}
.pos button{
cursor: pointer;
}
</style>
</head>
<body>
<div class="pos">
<button id="stopBtn">停止</button>
<button id="playBtn" style="margin-left: 10px;">播放</button>
<button id="pausedBtn" style="margin-left: 10px;">暂停</button>
<button id="speedBtn" style="margin-left: 10px;">2倍速</button>
<button id="loopBtn1" style="margin-left: 10px;">关闭循环</button>
<button id="nextBtn" style="margin-left: 10px;">下一步状态</button>
<button id="resetBtn" style="margin-left: 10px;">重置</button>
</div>
<script type="importmap">
{
"imports": {
"three": "../three.js-r148/build/three.module.js",
"three/addons/": "../three.js-r148/examples/jsm/",
"@tweenjs/tween.js": "../tween/tween.esm.js"
}
}
</script>
<script src="./base.js" type="module"> </script>
</body>
</html>
base.js
文件
import * as THREE from 'three';
import {
OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import {mesh,plane} from './model.js';
//场景
const scene = new THREE.Scene()
scene.add(mesh)
scene.add(plane)
scene.background = new THREE.Color(0xcce0ff);
scene.fog = new THREE.Fog( 0xcce0ff, 500, 10000)
//辅助观察的坐标系
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);
//光源设置
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(100, 60, 50);
scene.add(directionalLight);
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambient);
//相机
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 10000);
camera.position.set(431, 105, 269);
// camera.lookAt(0, 0, 0)
// WebGL渲染器设置
const renderer = new THREE.WebGLRenderer({
antialias: true,
});
// renderer.setClearColor(0xcce0ff)
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
// 渲染循环
function render() {
// console.log(camera.position)
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
// 相机控件
const controls = new OrbitControls(camera, renderer.domElement);
// 画布跟随窗口变化
window.onresize = function () {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
model.js
文件
import * as THREE from 'three';
// 创建一个地面
function createPlaneGeometryBasicMaterial() {
const groundGeometry = new THREE.PlaneGeometry( 20000, 20000 ) //草地平面几何体
const groundTexture = new THREE.TextureLoader().load('./dimian.jpg') //加载草地材质
groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping //设置重复贴图
groundTexture.repeat.set( 50, 50 )
groundTexture.anisotropy = 16
const groundMaterial = new THREE.MeshLambertMaterial({ //生成贴图的材质
map: groundTexture
})
const ground = new THREE.Mesh( groundGeometry, groundMaterial ) //生成草地
ground.rotation.x = - 0.5 * Math.PI;
ground.position.x = 0;
ground.position.z = 0;
ground.position.y = -15;
return ground
}
// 将平面添加到场景中
const plane = createPlaneGeometryBasicMaterial();
const geometry = new THREE.SphereGeometry( 15, 32, 16 );
const texLoader = new THREE.TextureLoader();
const texture = texLoader.load('./img.jpg');
const material = new THREE.MeshLambertMaterial({
coor:0xffff00,
map: texture,
});
const mesh = new THREE.Mesh( geometry, material );
mesh.name = 'Sphere'
const maxT = 30
const times = [0, 10, 20, maxT]
const values = [0, 0, 0, 100, 0, 0, 0, 0, 100, 0, 0, 0];
const posKf = new THREE.KeyframeTrack('Sphere.position',times, values)
const colorKf = new THREE.KeyframeTrack('Sphere.material.color',[0, 10, 20, 30],[
0, 1, 0,
1, 0, 0,
0, 0, 1,
1, 1, 1
])
const clip = new THREE.AnimationClip("test",maxT,[posKf,colorKf]);
const mixer = new THREE.AnimationMixer(mesh);
const clipAction = mixer.clipAction(clip);
clipAction.play();
// 从10s的时候开始播放
clipAction.time = 10;
// 循环执行的函数
let flag = true
const clock = new THREE.Clock();
function loop() {
if(flag){
// 球体滚动
mesh.rotateZ(0.1)
}
requestAnimationFrame(loop);
//clock.getDelta()方法获得loop()两次执行时间间隔
const frameT = clock.getDelta();
// 更新播放器相关的时间
mixer.update(frameT);
}
loop();
// 添加事件
function handleClick(id,fn){
const btn = document.getElementById(id)
btn.addEventListener('click', fn)
}
// 停止动画
handleClick('stopBtn',function(){
flag = false
clipAction.stop();
})
// 播放动画
handleClick('playBtn',function(){
flag = true
clipAction.play();
if(clipAction.paused){
clipAction.paused = false;
}
})
// 暂停动画
handleClick('pausedBtn',function(){
flag = false
clipAction.paused = true;
})
// 加速动画
handleClick('speedBtn',function(){
clipAction.timeScale = 3;
})
// 关闭循环
handleClick('loopBtn1',function(){
flag = false
clipAction.loop = THREE.LoopOnce
})
// 下一步状态
handleClick('nextBtn',function(){
clipAction.time += 0.1;
})
// 重置
handleClick('resetBtn',function(){
clipAction.reset();
})
export{ mesh, plane }