让模型实现动画效果,需要使用Three动画系统,动画系统
官方文档链接
,这里稍作了修改。
动画系统(Animation system)概述:
在three.js动画系统中,您可以为模型的各种属性设置动画: SkinnedMesh(蒙皮和装配模型)的骨骼,morph targets(变形目标), 不同的材料属性(颜色,不透明度,布尔运算),可见性和变换。动画属性可以淡入、淡出、交叉淡化和扭曲。 在相同或不同物体上同时发生的动画的权重和时间比例的变化可以独立地进行。 相同或不同物体的动画也可以同步发生。
组件
|
作用
|
动画片段(Animation Clips)
|
成功导入3D动画对象的响应字段中包含一个名为“animations”的数组, 其中包含此模型的AnimationClips(请参阅下面可用的加载器列表)。
每个AnimationClip通常保存对象某个活动的数据, 举个例子,假如mesh是一个角色,可能有一个AnimationClip实现步行循环, 第二个AnimationClip实现跳跃,第三个AnimationClip实现闪避等等。
|
关键帧轨道(Keyframe Tracks)
|
在AnimationClip中,每个动画属性的数据都存储在一个单独的KeyframeTrack中。
假设一个角色对象有Skeleton(骨架), 一个关键帧轨道可以存储下臂骨骼位置随时间变化的数据, 另一个轨道追踪同一块骨骼的旋转变化,第三个追踪另外一块骨骼的位置、转角和尺寸,等等。
AnimationClip可以由许多这样的轨道组成。假设模型具有morph Targets(变形目标)—— 例如一个变形目标显示一个笑脸,另一个显示愤怒的脸 —— 每个轨道都持有某个变形目标在AnimationClip运行期间产生的Mesh.morphTargetInfluences(变形目标影响)如何变化的信息。
|
动画混合器(Animation Mixer)
|
存储的数据仅构成动画的基础 —— 实际播放由AnimationMixer控制。可以同时控制和混合若干动画。
|
动画行为(Animation Actions)
|
AnimationMixer本身只有很少的(大体上)属性和方法, 因为它可以通过AnimationActions来控制。 通过配置AnimationAction,您可以决定何时播放、暂停或停止其中一个混合器中的某个AnimationClip, 这个AnimationClip是否需要重复播放以及重复的频率, 是否需要使用淡入淡出或时间缩放,以及一些其他内容(例如交叉渐变和同步)。
|
动画对象组(Animation Object Groups)
|
当需要一组对象接收共享的动画状态时,可以使用AnimationObjectGroup。
|
支持的格式和加载器(Supported Formats and Loaders)
|
请注意,并非所有模型格式都包含动画(尤其是OBJ,没有), 而且只有某些three.js加载器支持AnimationClip序列。
支持此动画类型加载器:
THREE.ObjectLoader、THREE.BVHLoader、THREE.ColladaLoader、THREE.FBXLoader、THREE.GLTFLoader、THREE.MMDLoader
3ds max和Maya当前无法直接导出多个动画(这意味着动画不是在同一时间线上)到一个文件中。
|
官方文档范例
let mesh;
const mixer = new THREE.AnimationMixer( mesh );
const clips = mesh.animations;
function update () {
mixer.update( deltaSeconds );
const clip = THREE.AnimationClip.findByName( clips, 'dance' );
const action = mixer.clipAction( clip );
action.play();
clips.forEach( function ( clip ) {
mixer.clipAction( clip ).play();
} );
1、添加模型
2、模型动画
工程文件结构如下图:
static:存放静态资源文件
three.js-master:为官网下载的代码包,包含所有需要用到的资源包,链接:https://github.com/mrdoob/three.js/archive/master.zip
index.html:页面代码
模型使用的是官方示例中的Soldier模型,文件位置:three.js-master\examples\models\gltf\Soldier.glb
为了方便操作我们将文件拷出来放在上图static\3dmod\gltf文件夹下,static与three.js-master同级
index.html单页代码组成
<!DOCTYPE html>
<meta charset="utf-8">
<title>My first three.js app</title>
<style>
body {
margin: 0;
</style>
</head>
<script type="importmap">
"imports": {
"three": "./three.js-master/build/three.module.js"
</script>
<script type="module">
</script>
</body>
</html>
参照官网例子:https://threejs.org/examples/#webgl_animation_skinning_blending中的场景和模型
搭建场景环境
import * as THREE from "three";
import { OrbitControls } from "./three.js-master/examples/jsm/controls/OrbitControls.js";
let scene, camera, renderer;
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer();
camera.position.set(5, 5, 5);
camera.lookAt(scene.position);
const axes = new THREE.AxesHelper(20);
scene.add(axes);
scene.background = new THREE.Color(0xa0a0a0);
scene.fog = new THREE.Fog(0xa0a0a0, 10, 30);
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
hemiLight.position.set(0, 10, 0);
scene.add(hemiLight);
const hemiLighthelper = new THREE.HemisphereLightHelper(hemiLight, 5);
scene.add(hemiLighthelper);
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false }));
mesh.rotation.x = - Math.PI / 2;
mesh.receiveShadow = true;
scene.add(mesh);
const directionalLight = new THREE.DirectionalLight(0xFFFFFF);
directionalLight.castShadow = true;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
directionalLight.position.set(0, 5, 5);
scene.add(directionalLight);
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
scene.add(directionalLightHelper);
renderer.shadowMap.enabled = true;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
导入模型,在《Three.js学习四——模型导入》中有相对详细的介绍。
let model = null;
function loadModel() {
const gltfLoader = new GLTFLoader();
gltfLoader.setPath('./static/3dmod/gltf/')
.load('Soldier.glb', function (gltf) {
gltf.scene.rotation.y = Math.PI;
gltf.scene.scale.set(1, 1, 1)
gltf.scene.traverse(function (object) {
if (object.isMesh) {
object.castShadow = true;
object.receiveShadow = true;
});
scene.add(gltf.scene);
model = gltf.scene;
}, function (res) {
});
- 使用加载器导入模型后,在加载成功后调用的函数中设置动画
- 新建一个AnimationMixer(动画混合器)
- 获取AnimationClip(动画)实例列表
- 设置播放动画,并在每一帧中更新mixer
以文中使用的GLTF加载器(GLTFLoader)为例,其解析基于glTF的ArrayBuffer或JSON字符串,并在完成后触发onLoad回调。onLoad的参数将是一个包含有已加载部分的Object:.scene、 .scenes、 .cameras、 .animations 和 .asset。
其中.scene为AnimationMixer新建时需要传入的内容,.animations 则为AnimationClip(动画)实例列表
在设置动画中使用的相关对象及方法:
AnimationMixer( rootObject : Object3D )
—— rootObject - 混合器播放的动画所属的对象。
AnimationClip( name : String, duration : Number, tracks : Array )
—— name - 此剪辑的名。duration - 持续时间 (单位秒). 如果传入负数, 持续时间将会从传入的数组中计算得到。tracks - 一个由关键帧轨道(KeyframeTracks)组成的数组。
AnimationAction( mixer : AnimationMixer, clip : AnimationClip, localRoot : Object3D )
—— mixer - 被此动作控制的 动画混合器。clip - 动画剪辑 保存了此动作当中的动画数据。localRoot - 动作执行的根对象`
animationClip.findByName ( objectOrClipArray : Object, name : String ) : AnimationClip
——
根据名称搜索动画剪辑(AnimationClip), 接收一个动画剪辑数组或者一个包含名为"animation"的数组的网格(或几何体)作为第一个参数。
animationMixer.clipAction (clip : AnimationClip, optionalRoot : Object3D) : AnimationAction
——
返回所传入的剪辑参数的AnimationAction, 根对象参数可选,默认值为混合器的默认根对象。第一个参数可以是动画剪辑(AnimationClip)对象或者动画剪辑的名称。
animationAction.play () : this
—— 让混合器激活动作。此方法可链式调用。
animationMixer.update (deltaTimeInSeconds : Number) : this
—— 推进混合器时间并更新动画,通常在渲染循环中完成, 传入按照混合器的时间比例(timeScale)缩放过的clock.getDelta
let clock = new THREE.Clock();
let mixer;
function loadModel() {
const gltfLoader = new GLTFLoader();
gltfLoader.setPath('./static/3dmod/gltf/')
.load('Soldier.glb', function (gltf) {
gltf.scene.rotation.y = Math.PI;
console.log("gltf", gltf)
gltf.scene.scale.set(1, 1, 1)
gltf.scene.traverse(function (object) {
if (object.isMesh) {
object.castShadow = true;
object.receiveShadow = true;
});
mixer = startAnimation(
gltf.scene,
gltf.animations,
gltf.animations[1].name
scene.add(gltf.scene);
model = gltf.scene;
}, function (res) {
});
* 启动特定网格对象的动画。在三维模型的动画数组中按名称查找动画
* @param skinnedMesh {THREE.SkinnedMesh} 要设置动画的网格
* @param animations {Array} 数组,包含此模型的所有动画
* @param animationName {string} 要启动的动画的名称
* @return {THREE.AnimationMixer} 要在渲染循环中使用的混合器
function startAnimation(skinnedMesh, animations, animationName) {
const m_mixer = new THREE.AnimationMixer(skinnedMesh);
const clip = THREE.AnimationClip.findByName(animations, animationName);
if (clip) {
const action = m_mixer.clipAction(clip);
action.play();
return m_mixer;
function animate() {
requestAnimationFrame(animate);
if(mixer){
mixer.update(clock.getDelta());
renderer.render(scene, camera);
<!DOCTYPE html>
<meta charset="utf-8">
<title>My first three.js app</title>
<style>
body {
margin: 0;
</style>
</head>
<script type="importmap">
"imports": {
"three": "./three.js-master/build/three.module.js"
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "./three.js-master/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "./three.js-master/examples/jsm/loaders/GLTFLoader.js";
let scene, camera, renderer;
let model = null;
let clock = new THREE.Clock();
let mixer;
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer();
camera.position.set(5, 5, 5);
camera.lookAt(scene.position);
const axes = new THREE.AxesHelper(20);
scene.add(axes);
scene.background = new THREE.Color(0xa0a0a0);
scene.fog = new THREE.Fog(0xa0a0a0, 10, 30);
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444);
hemiLight.position.set(0, 10, 0);
scene.add(hemiLight);
const hemiLighthelper = new THREE.HemisphereLightHelper(hemiLight, 5);
scene.add(hemiLighthelper);
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshPhongMaterial({ color: 0x999999, depthWrite: false }));
mesh.rotation.x = - Math.PI / 2;
mesh.receiveShadow = true;
scene.add(mesh);
const directionalLight = new THREE.DirectionalLight(0xFFFFFF);
directionalLight.castShadow = true;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 50;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10;
directionalLight.position.set(0, 5, 5);
scene.add(directionalLight);
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
scene.add(directionalLightHelper);
renderer.shadowMap.enabled = true;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
function loadModel() {
const gltfLoader = new GLTFLoader();
gltfLoader.setPath('./static/3dmod/gltf/')
.load('Soldier.glb', function (gltf) {
gltf.scene.rotation.y = Math.PI;
console.log("gltf\ngltf", gltf)
gltf.scene.scale.set(1, 1, 1)
gltf.scene.traverse(function (object) {
if (object.isMesh) {
object.castShadow = true;
object.receiveShadow = true;
});
mixer = startAnimation(
gltf.scene,
gltf.animations,
gltf.animations[1].name
scene.add(gltf.scene);
model = gltf.scene;
}, function (res) {
});
* 启动特定网格对象的动画。在三维模型的动画数组中按名称查找动画
* @param skinnedMesh {THREE.SkinnedMesh} 要设置动画的网格
* @param animations {Array} 数组,包含此模型的所有动画
* @param animationName {string} 要启动的动画的名称
* @return {THREE.AnimationMixer} 要在渲染循环中使用的混合器
function startAnimation(skinnedMesh, animations, animationName) {
const m_mixer = new THREE.AnimationMixer(skinnedMesh);
const clip = THREE.AnimationClip.findByName(animations, animationName);
if (clip) {
const action = m_mixer.clipAction(clip);
action.play();
return m_mixer;
function animate() {
requestAnimationFrame(animate);
if(mixer){
mixer.update(clock.getDelta());
renderer.render(scene, camera);
init();
loadModel();
animate();
</script>
</body>
</html>
// 需要在官网下载 example文件夹中
import * as THREE from './three.module.js';
import Stats from './jsm/libs/stats.module.js';
import { GUI } from
绑定需要动画的mash mixer = new THREE.AnimationMixer(mash)
mixer.clipAction( animate )
调用 .play()方法
如果某动画只需要播放一次
把动画拎出来 设置播放次数
code:
//动画发生在哪个物体上就绑定哪个 mash
const mixer = new THREE.AnimationMixer(mash);
const action = mixer.clipAction(gltf.animations[0])/
为了衡量画面切换速度,引入了每秒帧数FPS(Frames Per Second)的概念,是指每秒画面重绘的次数。FPS越大,则动画效果越平滑,当FPS小于20时,一般就能明显感受到画面的卡滞现象。
那么FPS是不是越大越好呢?其实也未必。当FPS足够大(比如达到60),再增加帧数人眼也不会感受到明显的变化,反而相应地就要消耗更多资源(比如电影的胶片就需要更长了,或是电脑刷新画面需要消耗计...
最近看了一篇文章,用three.js写了一个很有趣的游戏场景,看了之后发现又想好好学一下three.js。
文章:https://tympanus.net/codrops/2016/04/26/the-aviator-animating-basic-3d-scene-threejs/?utm_source=tuicool
对整个流程介绍的很是详细。拜读了这篇文章之后,决定也用t...