logo头像
Snippet 博客主题

threejs动画

前面我们所用的模型大都是静态的,没有动画,没有生命。这节我们将赋予它们生命。

动画本质是通过改变物体的旋转、缩放、位置、材质、顶点、面以及其它你所能想到的属性来实现的。这些其实在前面章节示例里或多或少已经使用了。

一、选择对象

在写示例前我们先了解一个重要操作,如何通过鼠标选中场景中的对象。

当我们在屏幕上点击鼠标时,会发生如下事情:

  1. 首先,基于屏幕上点击位置会创建一个THREE.Vecor3向量。

  2. 接着,使用vector.unproject方法将屏幕上的点击位置转换成Threejs场景中的坐标。

  3. 然后,创建THREE.Raycaster。使用它可以向场景中发射光线。

  4. 最后,我们使用raycaster.intersectObjects方法来判断指定的对象中哪些被该光线照射到了。其结果包含了所有被照射到的对象的信息,这些信息包括:

    1
    2
    3
    4
    5
    distance: 50.66666666666   // 从摄像机到被点击对象的距离
    face: THREE.Face3 // 那个面被点击了
    faceIndex: 6 // 被点击面的下标
    object: THREE.Mesh // 被点击的网格对象
    point: THREE.Vector3 // 网格对象上哪个点被点击了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<!-- chapter-09-01.html -->
<!DOCTYPE html>
<html>
<head>
<title>Selecting objects</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/Projector.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
renderer.setSize(window.innerWidth, window.innerHeight);

var planeGeometry = new THREE.PlaneGeometry(60, 20, 1, 1);
var planeMaterial = new THREE.MeshLambertMaterial({color: 0xffffff});
var plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -0.5 * Math.PI;
plane.position.x = 15;
plane.position.y = 0;
plane.position.z = 0;
scene.add(plane);

var cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
var cubeMaterial = new THREE.MeshLambertMaterial({color: 0xff0000});
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.x = -9;
cube.position.y = 3;
cube.position.z = 0;
scene.add(cube);

var sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
var sphereMaterial = new THREE.MeshLambertMaterial({color: 0x7777ff});
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.x = 20;
sphere.position.y = 0;
sphere.position.z = 2;
scene.add(sphere);

var cylinderGeometry = new THREE.CylinderGeometry(2, 2, 20);
var cylinderMaterial = new THREE.MeshLambertMaterial({color: 0x77ff77});
var cylinder = new THREE.Mesh(cylinderGeometry, cylinderMaterial);
cylinder.position.set(0, 0, 1);
scene.add(cylinder);

camera.position.x = -30;
camera.position.y = 40;
camera.position.z = 30;
camera.lookAt(scene.position);

var ambientLight = new THREE.AmbientLight(0x0c0c0c);
scene.add(ambientLight);

var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(-40, 60, -10);
scene.add(spotLight);

document.getElementById("WebGL-output").appendChild(renderer.domElement);

var tube;
var controls = new function () {
this.showRay = false;
};

var gui = new dat.GUI();
gui.add(controls, 'showRay').onChange(function (e) {
if (tube) scene.remove(tube)
});

function render() {
stats.update();
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();

// 监控鼠标按下和鼠标移动事件
document.addEventListener('mousedown', onDocumentMouseDown, false);
document.addEventListener('mousemove', onDocumentMouseMove, false);

function onDocumentMouseDown(event) {
// 1.鼠标点击位置
var vector = new THREE.Vector3(( event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
// 2.把鼠标点击位置转换为threejs场景中的坐标
vector = vector.unproject(camera);
// 3.从摄像机位置向点击位置发射一条光线
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
// 4. 获取指定数组里对象哪些被光线照射到了
var intersects = raycaster.intersectObjects([sphere, cylinder, cube]);
if (intersects.length > 0) {
console.log(intersects[0]);
intersects[0].object.material.transparent = true;
intersects[0].object.material.opacity = 0.5;
}
}

function onDocumentMouseMove(event) {
// 是否显示HREE.Raycaster射线
if (controls.showRay) {
var vector = new THREE.Vector3(( event.clientX / window.innerWidth ) * 2 - 1, -( event.clientY / window.innerHeight ) * 2 + 1, 0.5);
vector = vector.unproject(camera);
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
var intersects = raycaster.intersectObjects([sphere, cylinder, cube]);
if (intersects.length > 0) {
var points = [];
points.push(new THREE.Vector3(-30, 39.8, 30));
points.push(intersects[0].point);
var mat = new THREE.MeshBasicMaterial({color: 0xff0000, transparent: true, opacity: 0.6});
var tubeGeometry = new THREE.TubeGeometry(new THREE.SplineCurve3(points), 60, 0.001);

if (tube) scene.remove(tube);
tube = new THREE.Mesh(tubeGeometry, mat);
scene.add(tube);
}
}
}

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>

下面回归正题,我们重新回到动画上来。


二、使用Tween.js实现动画

Tween.js是一个轻量级的javascript库 https://github.com/tweenjs/tween.js,通过这个库可以很容易实现某个属性在两个值之间进行过渡,我们称这种为补间动画。官方提供了很多示例,自行去查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<!-- chapter-09-02.html -->
<!DOCTYPE html>
<html>
<head>
<title>Animation tween </title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/ColladaLoader.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/tween.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xcccccc, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;

camera.position.x = 150;
camera.position.y = 150;
camera.position.z = 150;
camera.lookAt(new THREE.Vector3(0, 20, 0));

var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(150, 150, 150);
spotLight.intensity = 2;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

var mesh;
var loader = new THREE.ColladaLoader();
loader.load("../assets/models/dae/Truck_dae.dae", function (result) {
mesh = result.scene.children[0].children[0].clone();
//mesh.scale.set(4, 4, 4);
scene.add(mesh);
// 补间动画要在模型加载完后才能开始
tween.start();
});

// 使用补间动画, 在y轴上下缓动移动
// http://sole.github.io/tween.js/examples/03_graphs.html
var posSrc = {pos: 10};
var tween = new TWEEN.Tween(posSrc).to({pos: 0}, 1000);
tween.easing(TWEEN.Easing.Sinusoidal.InOut);

var tweenBack = new TWEEN.Tween(posSrc).to({pos: 10}, 1000);
tweenBack.easing(TWEEN.Easing.Sinusoidal.InOut);
// 衔接两个动作,循环执行
tween.chain(tweenBack);
tweenBack.chain(tween);
// 执行后坐标更新
var onUpdate = function () {
var pos = this.pos;
mesh.position.y = pos;
};
tween.onUpdate(onUpdate);
tweenBack.onUpdate(onUpdate);

function render() {
stats.update();
// 更新补间动画
TWEEN.update();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>


三、使用摄像机实现动画

前面我们已经接触过了,可以通过移动摄像机的位置让整个场景动起来,我们是通过手动改变摄像机位置实现的。Threejs还提供了其它更新摄像机的控件,下面我们依次看看。

最常用的:

其它的本节不会介绍,使用和上面一样,了解下:

1. 轨迹球控制器

下面是使用这个控制器,通过鼠标左键拖动、滚轮、右键拖动分别实现场景的旋转、缩放、平移的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<!-- chapter-09-03.html -->
<!DOCTYPE html>
<html>
<head>
<title>Trackball controls </title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/OBJLoader.js"></script>
<script type="text/javascript" src="../libs/MTLLoader.js"></script>
<script type="text/javascript" src="../libs/OBJMTLLoader.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/chroma.js"></script>
<script type="text/javascript" src="../libs/TrackballControls.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;

camera.position.x = 100;
camera.position.y = 100;
camera.position.z = 300;
camera.lookAt(new THREE.Vector3(0, 0, 0));

var ambientLight = new THREE.AmbientLight(0x383838);
scene.add(ambientLight);

var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(300, 300, 300);
spotLight.intensity = 1;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

// 创建轨迹球控制器,并绑定摄像机
var trackballControls = new THREE.TrackballControls(camera);
// 想要全面了解这些属性可以参考rackballControls.js源文件
trackballControls.rotateSpeed = 1.0;
trackballControls.zoomSpeed = 1.0;
trackballControls.panSpeed = 1.0;
//trackballControls.noZoom=false;
//trackballControls.noPan=false;
trackballControls.staticMoving = true;
//trackballControls.dynamicDampingFactor=0.3;

// 加载OBJ/MTL模型
var mesh;
var loader = new THREE.OBJMTLLoader();
var texture = THREE.ImageUtils.loadTexture('../assets/textures/Metro01.JPG');
//texture.wrapS = texture.wrapT = THREE.MirroredRepeatWrapping;
loader.load('../assets/models/city.obj', '../assets/models/city.mtl', function (object) {
var scale = chroma.scale(['red', 'green', 'blue']);
setRandomColors(object, scale);
mesh = object;
scene.add(mesh);
});
//texture.repeat.set( 1 , 1);

function setRandomColors(object, scale) {
var children = object.children;
if (children && children.length > 0) {
children.forEach(function (e) {
setRandomColors(e, scale)
});
} else {
if (object instanceof THREE.Mesh) {
object.material.color = new THREE.Color(scale(Math.random()).hex());
if (object.material.name.indexOf("building") == 0) {
object.material.emissive = new THREE.Color(0x444444);
object.material.transparent = true;
object.material.opacity = 0.8;
}
}
}
}

var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
trackballControls.update(delta); // delta距离上次调用时间间隔
requestAnimationFrame(render);
webGLRenderer.render(scene, camera)
}
render();

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>

2. 飞行控制器

可行像飞行视角一样在场景中飞行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建飞行控制器
var flyControls = new THREE.FlyControls(camera);
flyControls.movementSpeed = 25;
flyControls.domElement = document.querySelector("#WebGL-output");
flyControls.rollSpeed = Math.PI / 24;
flyControls.autoForward = true;
flyControls.dragToLook = false;

// 更新
var clock = new THREE.Clock();
function render() {
var delta = clock.getDelta();
flyControls.update(delta);

webGLRenderer.clear();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera)
}

3. 翻滚控制器

翻滚场景(Q/E 左/右翻滚)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建翻滚控制器
var rollControls = new THREE.RollControls(camera);
rollControls.movementSpeed = 25;
rollControls.lookSpeed = 3;

// 更新
var clock = new THREE.Clock();
function render() {
var delta = clock.getDelta();
rollControls.update(delta);

webGLRenderer.clear();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera)
}

4. 第一视角控制器

类似第一视角射击游戏那样控制摄像机。鼠标用于控制视角,键盘用于控制角色移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 创建第一视角控制器
var camControls = new THREE.FirstPersonControls(camera);
camControls.lookSpeed = 0.4;
camControls.movementSpeed = 20;
camControls.noFly = true;
camControls.lookVertical = true;
camControls.constrainVertical = true;
camControls.verticalMin = 1.0;
camControls.verticalMax = 2.0;
camControls.lon = -150;
camControls.lat = 120;

// 更新
var clock = new THREE.Clock();
function render() {
var delta = clock.getDelta();
rollControls.update(delta);

webGLRenderer.clear();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera)
}

5. 轨道控制器

和轨迹控制器类似,只是轨道控制器强制摄像头 up方向,而轨迹控制器允许相机倒置旋转。可以用于控制场景中的对象围绕场景中心旋转和平移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
<!-- chapter-09-04.html -->
<!DOCTYPE html>
<html>
<head>
<title>Orbit controls</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/OrbitControls.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0x000, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;

// 创建球体网格对象
var sphere = createMesh(new THREE.SphereGeometry(20, 40, 40));
scene.add(sphere);

camera.position.x = -20;
camera.position.y = 30;
camera.position.z = 40;
camera.lookAt(new THREE.Vector3(00, 0, 0));

var ambiLight = new THREE.AmbientLight(0x111111);
scene.add(ambiLight);
var spotLight = new THREE.DirectionalLight(0xffffff);
spotLight.position.set(-20, 30, 40);
spotLight.intensity = 1.5;
scene.add(spotLight);

document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

// 使用外部纹理材质创建球体网格
function createMesh(geom) {
var planetTexture = THREE.ImageUtils.loadTexture("../assets/textures/planets/mars_1k_color.jpg");
var normalTexture = THREE.ImageUtils.loadTexture("../assets/textures/planets/mars_1k_normal.jpg");
// 加载外部图片纹理创建Phong材质
var planetMaterial = new THREE.MeshPhongMaterial({map: planetTexture, bumpMap: normalTexture});

var mesh = THREE.SceneUtils.createMultiMaterialObject(geom, [planetMaterial]);
return mesh;
}

// 创建轨道控制器
var orbitControls = new THREE.OrbitControls(camera);
orbitControls.autoRotate = true;

var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
orbitControls.update(delta);

requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>


四、高级动画

1. 关键帧动画

就是从动画开始到动画结束之间所有的顶点数据都会被存储下来,你需要做的就是定义好关键位置,并重复该过程。这种主要一个缺点就是对于大型网格和大型动画,模型文件会变得非常大。

Threejs库提供的使用关键字动画对象:THREE.MorphAnimMesh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
<!-- chapter-09-05.html -->
<!DOCTYPE html>
<html>
<head>
<title>Working with morph targets</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 2000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;

camera.position.x = 250;
camera.position.y = 250;
camera.position.z = 350;
camera.lookAt(new THREE.Vector3(100, 50, 0));

var spotLight = new THREE.DirectionalLight(0xffffff);
spotLight.position.set(300, 200, 300);
spotLight.intensity = 1;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

var controls = new function () {
this.keyframe = 0; // 关键帧位置
};

var gui = new dat.GUI();
gui.add(controls, "keyframe", 0, 15).step(1).onChange(function (e) {
showFrame(e);
});

var meshAnim;
var frames = [];
var currentMesh;
var loader = new THREE.JSONLoader();
// 1. 加载帧动画模型
loader.load('../assets/models/horse.js', function (geometry, mat) {
// 创建左边静态马网格
var mat = new THREE.MeshLambertMaterial(
{
morphTargets: true, // 设置为true网格才会动
vertexColors: THREE.FaceColors
});
var mesh = new THREE.Mesh(geometry, mat);
mesh.position.x = -100;
frames.push(mesh);
currentMesh = mesh;
morphColorsToFaceColors(geometry); // 修改各面颜色

// 创建动画所有关键帧网格
var mat2 = new THREE.MeshLambertMaterial(
{color: 0xffffff, vertexColors: THREE.FaceColors});
// 遍历动画所有帧,并创建为网格对象添加到frames数值中
mesh.geometry.morphTargets.forEach(function (e) {
var geom = new THREE.Geometry();
geom.vertices = e.vertices;
geom.faces = geometry.faces;
var morpMesh = new THREE.Mesh(geom, mat2);
frames.push(morpMesh);
morpMesh.position.x = -100;
});

// 确保运动时光照、阴影和颜色的准确性
geometry.computeVertexNormals();
geometry.computeFaceNormals();
geometry.computeMorphNormals();

// 2. THREE.MorphAnimMesh创建帧动画网格,右边运动马
meshAnim = new THREE.MorphAnimMesh(geometry, mat);
meshAnim.duration = 1000;
meshAnim.position.x = 200;
meshAnim.position.z = 0;
scene.add(meshAnim);

// 用第一帧创建右边的静态马
showFrame(0);
}, '../assets/models');

// 手的改变帧播放
function showFrame(idx) {
scene.remove(currentMesh);
scene.add(frames[idx]);
currentMesh = frames[idx];
console.log(currentMesh);
}

function morphColorsToFaceColors(geometry) {
if (geometry.morphColors && geometry.morphColors.length) {
var colorMap = geometry.morphColors[0];
for (var i = 0; i < colorMap.colors.length; i++) {
geometry.faces[i].color = colorMap.colors[i];
geometry.faces[i].color.offsetHSL(0, 0.3, 0);
}
}
}

var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
webGLRenderer.clear();
if (meshAnim) {
// 3. 帧动画更新
meshAnim.updateAnimation(delta * 1000);
meshAnim.rotation.y += 0.01;
}
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>

2. 骨骼动画

就是定义好网格的骨骼,并将顶点绑定到特定的骨骼上。当骨骼运动时,相连骨骼也会随着一起变换,同时骨骼上绑定的顶点也会随之移动。

Threejs库提供的创建骨骼网格的对象:THREE.SkinnedMesh。下面是一个手动变换骨骼对象的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<!-- chapter-09-06.html -->
<!DOCTYPE html>
<html>
<head>
<title>Load blender model </title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/tween.min.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;

camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 4;
camera.lookAt(new THREE.Vector3(0, 0, 0));

var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(0, 50, 30);
spotLight.intensity = 2;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

var mesh;
var loader = new THREE.JSONLoader();
// 1. 加载Blender导出的骨骼模型
loader.load('../assets/models/hand-1.js', function (geometry, mat) {
var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true}); // skinning要设置为true,否则骨骼不会动
// 2. 创建骨骼模型网格
mesh = new THREE.SkinnedMesh(geometry, mat);
mesh.rotation.x = 0.5 * Math.PI;
mesh.rotation.z = 0.7 * Math.PI;
scene.add(mesh);
// 开始运行
tween.start();
}, '../assets/models');

// 3. 手动改变骨骼变换
var onUpdate = function () {
var pos = this.pos;
console.log(mesh.skeleton);
mesh.skeleton.bones[5].rotation.set(0, 0, pos);
mesh.skeleton.bones[6].rotation.set(0, 0, pos);
mesh.skeleton.bones[10].rotation.set(0, 0, pos);
mesh.skeleton.bones[11].rotation.set(0, 0, pos);
mesh.skeleton.bones[15].rotation.set(0, 0, pos);
mesh.skeleton.bones[16].rotation.set(0, 0, pos);
mesh.skeleton.bones[20].rotation.set(0, 0, pos);
mesh.skeleton.bones[21].rotation.set(0, 0, pos);
mesh.skeleton.bones[1].rotation.set(pos, 0, 0);
};
var tween = new TWEEN.Tween({pos: -1})
.to({pos: 0}, 3000)
.easing(TWEEN.Easing.Cubic.InOut) // 缓动
.yoyo(true) // 设置为true,使得动画运动完后反着运行
.repeat(Infinity) // 重复运行
.onUpdate(onUpdate);


var clock = new THREE.Clock();
function render() {
stats.update();
TWEEN.update();
var delta = clock.getDelta();
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>


五、使用外部模型创建动画

1. 使用Blender创建骨骼动画

Blender使用threejs导出器,把.blend动画导出为.json,然后加载并播放动画。

播放外部骨骼动画使用:THREE.Animation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<!-- chapter-09-07.html -->
<!DOCTYPE html>
<html>
<head>
<title>Animation from blender</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;

camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 4;
camera.lookAt(new THREE.Vector3(0, 0, 0));

var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(0, 50, 30);
spotLight.intensity = 2;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

var mesh;
var helper;
var controls = new function () {
this.showHelper = false; // 是否显示骨骼骨架
};
var gui = new dat.GUI();
gui.add(controls, 'showHelper').onChange(function (state) {
helper.visible = state;
});

var loader = new THREE.JSONLoader();
// 加载骨骼动画模型
loader.load('../assets/models/hand-2.js', function (model, mat) {
var mat = new THREE.MeshLambertMaterial({color: 0xF0C8C9, skinning: true});
mesh = new THREE.SkinnedMesh(model, mat);
mesh.rotation.x = 0.5 * Math.PI;
mesh.rotation.z = 0.7 * Math.PI;
scene.add(mesh);

// 创建骨骼动画对象,并开始播放
var animation = new THREE.Animation(mesh, model.animation);
animation.play();

// 创建骨骼骨架
helper = new THREE.SkeletonHelper(mesh);
helper.material.linewidth = 2;
helper.visible = false;
scene.add(helper);
}, '../assets/models');

var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
if (mesh) {
// 更新骨骼动画
helper.update();
THREE.AnimationHandler.update(delta);
}
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>

2. 从Collada模型加载动画

使用方式和上面一样,只是加载模型文件格式变了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<!-- chapter-09-08.html -->
<!DOCTYPE html>
<html>
<head>
<title>Animation from collada</title>
<script type="text/javascript" src="../libs/three.js"></script>
<script type="text/javascript" src="../libs/stats.js"></script>
<script type="text/javascript" src="../libs/dat.gui.js"></script>
<script type="text/javascript" src="../libs/ColladaLoader.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<div id="Stats-output">
</div>
<div id="WebGL-output">
</div>

<script type="text/javascript">
function init() {
var stats = initStats();
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

var webGLRenderer = new THREE.WebGLRenderer();
webGLRenderer.setClearColor(new THREE.Color(0xEEEEEE, 1.0));
webGLRenderer.setSize(window.innerWidth, window.innerHeight);
webGLRenderer.shadowMapEnabled = true;

camera.position.x = 400;
camera.position.y = 50;
camera.position.z = 150;
camera.lookAt(new THREE.Vector3(0, 0, 0));

var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(300, 500, 100);
spotLight.intensity = 3;
scene.add(spotLight);
document.getElementById("WebGL-output").appendChild(webGLRenderer.domElement);

var meshAnim;
// 加载.dae模型
var loader = new THREE.ColladaLoader();
loader.load('../assets/models/monster.dae', function (collada) {
// 模型第一帧网格添加进场景
var child = collada.skins[0];
scene.add(child);
child.scale.set(0.15, 0.15, 0.15);
child.rotation.x = -0.5 * Math.PI;
child.position.x = -100;
child.position.y = -60;

// 创建动画并开始播放
var animation = new THREE.Animation(child, child.geometry.animation);
animation.play();
});

var clock = new THREE.Clock();
function render() {
stats.update();
var delta = clock.getDelta();
// 动画更新
THREE.AnimationHandler.update(delta);
requestAnimationFrame(render);
webGLRenderer.render(scene, camera);
}
render();

function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.getElementById("Stats-output").appendChild(stats.domElement);
return stats;
}
}
window.onload = init;
</script>
</body>
</html>

3. 其它

整的来说动画就两类:帧动画和骨骼动画,使用方法都差不多。其它动画文件格式加载和使用可以查看官方示例。