Threejs-关键帧动画

什么是关键帧动画

关键帧动画,你可以理解为在一个时间轴上,选择几个关键的时间点,然后分别定义这几个时间点对应物体状态(比如位置、姿态、颜色等),然后基于这几个关键的时间点,采用特定的插值方法计算物体的状态数据,从而达到比较流畅的动画效果。

创建关键帧动画对象

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 } 

 上一篇
常用正则表达式 常用正则表达式
整数或者小数 ^[0-9]+\.{0,1}[0-9]{0,2}$ 只能输入数字 ^[0-9]*$ 只能输入n位的数字 ^\d{n}$ 只能输入至少n位的数字 ^\d{n,}$ 只能输入m~n位的数字 ^\d{m,n}$ 只能输
2023-07-22
下一篇 
Threejs-管道漫游 Threejs-管道漫游
管道漫游 管道外观: 通过一个轨迹线生成一个管道几何体,然后相机沿着该轨迹线移动,注意相机的方向要沿着轨迹线的切线方向,这样会形成一个管道漫游的效果。 管道几何体TubeGeometry、纹理贴图 相机对象Camera的.positio
2023-07-17
  目录